附录,neslib库
Shiru为NES开发编写了neslib代码。这些都是关于一切如何运作的详细说明。我稍后会添加示例代码。我主要使用的是neslib的略微修改版本
http://shiru.untergrund.net/files/nes/chase.zip
(本地下载)
示例代码
http://shiru.untergrund.net/files/src/cc65_nes_examples.zip
(本地下载)
此链接有一个neslib版本,适用于最新版本的cc65(截至2016年)版本2.15
http://forums.nesdev.com/viewtopic.php?p=154078#p154078
pal_all(const char *data);
const unsigned char game_palette [] = {...} //定义一个32字节的字符数组
pal_all(game_palette);
-传入一个完整的32byte的调色板指针
-复制32byte到缓冲区
-随时调用,但只在v-blank时候更新
pal_bg(bg_palette); //仅16字节,背景
pal_spr(sprite_palette); //仅限16个字节,sprites
-跟pal_all一样,但只有16个字节
pal_col(unsigned char index,unsigned char color);
-精灵或背景,设置任意1个调色板的1个颜色
-随时调用,但只在v-blank时候更新
-index = 0 – 31 (0-15 bg, 16-31 sprite)
#define RED 0x16
pal_col(0, RED); //将背景颜色设置为红色
pal_col(0, 0x30); //将背景颜色设置为白色= 0x30
pal_col() 旋转颜色(超级玛丽中的硬币变色) 或者闪烁的精灵
注意:调色板缓冲区设置为0x1c0-0x1df(参看示例代码)
PAL_BUF = $01c0,在crt0.s中定义
- 这是在硬件堆栈中。如果子程序调用深度超过16,它将开始覆盖缓冲区,可能导致错误的颜色或游戏崩溃
pal_clear(void);//将所有颜色设置为黑色,可随时执行
pal_bright(unsigned char bright); //使所有颜色变亮或变暗
- 0-8,4 =正常,3 2 1更暗,5 6 7更亮
- 0是黑色,4是正常,8是白色
pal_bright(4); //正常亮度
注意:在init期间必须至少调用一次 pal_bright() 它设置指向颜色的指针,需要为调色板更新设置颜色。(在crt0.s中)
Shiru在Chase源代码游戏中具有淡化功能
void pal_fade_to(unsigned to)
{
if(!to) music_stop();
while(bright!=to)
{
delay(4);
if(bright
else --bright;
pal_bright(bright);
}
if(!bright)
{
ppu_off();
set_vram_update(NULL);
scroll(0,0);
}
}
pal_spr_bright(unsigned char bright);
-仅设置精灵亮度
pal_bg_bright(unsigned char bright);
-设置背景亮度,使用0-8,与 pal_bright() 相同
ppu_wait_nmi(void);
- 等待下一帧
ppu_wait_frame(void);
-每5帧等一个额外的帧,对于NTSC电视
-不使用它,我删除它
-可能有错误的分屏
ppu_off(void);
-关闭屏幕
ppu_on_all(void);
-重新打开精灵和背景
ppu_on_bg(void);
-仅打开背景,不影响精灵
ppu_on_spr(void);
-只打开sprite,不影响背景
ppu_mask(unsigned char mask);
-手动设置2001寄存器,请参阅nesdev wiki
-可用于设置颜色强调或灰度模式
ppu_mask(0x1e); //正常模式,屏幕打开
ppu_mask(0x1f); //灰度模式,屏幕打开
ppu_mask(0xfe); //屏幕打开,所有颜色强调位设置,使屏幕变暗
ppu_system(void); //PAL返回0,NTSC返回0
- 在init期间,它会执行一些定时代码,并确定正在运行的电视系统类型。这是一种访问该信息的方法,如果你想对每种类型的电视进行不同的编程
-例如:a = ppu_system();
oam_clear(void); //清除OAM缓冲区,使所有精灵消失
OAM_BUF =$0200,在crt0.s中定义
oam_size(unsigned char size); //将精灵大小设置为8×8或8×16模式
oam_size(0); // 8×8 mode
oam_size(1); // 8×16 mode
注意:在每个循环开始时,将sprid设置为0
sprid = 0; ,然后每次将精灵推送到OAM缓冲区时,它返回下一个索引值(sprid)
oam_spr(unsigned char x,unsigned char y,unsigned char chrnum,unsigned char attr,unsigned char sprid);
-返回sprid (当前OAM缓冲区的索引)
-sprid是缓冲区中精灵的数量乘以4(每个精灵4个字节)
sprid = oam_spr(1,2,3,0,sprid);
-将一个精灵放在X = 1,Y = 2,使用tile#3,palette#0,我们正在使用sprid来跟踪缓冲区中的索引
sprid = oam_spr (1,2,3,0|OAM_FLIP_H,sprid); //相同,但水平翻转精灵
sprid = oam_spr (1,2,3,0|OAM_FLIP_V,sprid); //相同,但垂直翻转精灵
sprid = oam_spr (1,2,3,0|OAM_FLIP_H|OAM_FLIP_V,sprid); //相同,但水平和垂直翻转精灵
sprid = oam_spr (1,2,3,0|OAM_BEHIND,sprid); //精灵将在背景后面,但在通用背景颜色的前面(第一个bg调色板条目)
oam_meta_spr(unsigned char x,unsigned char y,unsigned char sprid,const unsigned char *data);
-返回sprid(当前OAM缓冲区的索引)
-sprid是缓冲区中精灵的数量乘以4(每个精灵4个字节)
sprid = oam_meta_spr(1,2,sprid, metasprite1)
metasprite1[] = …; //metasprite的定义,chars数组
-metasprite是精灵的集合
-你不能轻易翻转它
-你可以使用nes屏幕工具制作metasprite
-每个tile为4个字节的数组=
-x offset,y offset,tile,attribute(每个tile调色板/flip)
-你必须传递一个指向这个数据数组的指针
-数据不能超过 128 (0x80)
-每个循环(帧)期间你将精灵推送到OAM缓冲区
-在v-blank期间自动转到OAM(nmi代码的一部分)
oam_hide_rest(unsigned char sprid);
-在每个循环结束时将其余的精灵移出屏幕
-这是必需做的,如果你没有在每个循环开始时清除精灵
-如果屏幕上的精灵数量正好是64,那么值会回到0,这个函数会意外地将所有精灵移出屏幕(通过0会将所有精灵移出屏幕)
-如果由于某种原因你传递了一个不能被4整除的值(如3),这个函数会在无限循环中使游戏崩溃
-在每个循环开始时调用 oam_clear(), 不要再用 oam_hide_rest()
music_play(unsigned char song);
-发送一个歌曲编号,它设置一个指向歌曲开头的指针,在v-blank他将自动播放
music_play(0); //播放0号音乐
music_stop(void); //停止播放歌曲,再想播放需要调用 music_play(),但这样会从头开始播放音乐
music_pause(unsigned char pause); //暂停一首歌、取消暂停继续播放
music_pause(1); //暂停
music_pause(0); //继续播放
sfx_play(unsigned char sound,unsigned char channel); //设置一个指向音效开头的指针,它将自动播放
sfx_play(0, 0); //播放音效#0,优先级#0
-通道3的优先级超过2.
- 3> 2> 1> 0.如果2个声音效果冲突,将播放更高的优先级。
sample_play(unsigned char sample); //播放DMC音效
sample_play(0); //播放DMC示例#0
pad_poll(unsigned char pad);
-读取控制器
-通过 0或1 获取不同手柄的值
-每帧只能调用一次
pad1 = pad_poll(0); //读取控制器#1,存储在pad1
pad1 = pad_poll(1); //读取控制器#2,存储在pad2中
pad_trigger(unsigned char pad); //新按下的按钮,而不是按住的状态
a = pad_trigger(0); //取控制器#1,仅在此帧新按下
b = pad_trigger(1); //取控制器#2,仅在此帧新按下
-这实际上调用了pad_poll() //但只返回新的按键,而不是按住的按钮
pad_state(unsigned char pad);
-get last poll without polling again
-do pad_poll() first, every frame
-this is so you have a consistent value all frame
-can do this multiple times per frame and will still get the same info
pad1 = pad_state(0); // controller #1, get last poll
pad2 = pad_state(1); // controller #2, get last poll
注意:按钮定义与我使用的按钮定义相反,因为它们以向右移位而不是向左移动存储
// scrolling //
你预先定义了2个int(每个2个字节),ScrollX和ScrollY。
你需要手动将它们保持在0到0x01ff之间(y为0x01df,只有240条扫描线,而不是256条)
在示例代码9中,shiru执行此操作
–-y;
if(y<0) y=240*2-1; //将Y保持在两个名称表的总高度内
scroll(unsigned int x,unsigned int y);
-设置x和y滚动。可以随时执行,这些数字不会转到2005寄存器直到下一个v-blank
-高位更改基本名称表,寄存器2000 (在下一个v-blank)
-假设您已正确设置镜像,它将滚动进入下一个nametable
scroll(scroll_X,scroll_Y);
*注意,我不使用这个滚动功能,但我自己的类似功能,你不需要将Y保持在0和$ 1df之间。
split(unsigned int x,unsigned int y);
-waits for sprite zero hit, then changes the x scroll
-will only work if you have a sprite currently in the OAM at the zero position, and it’s somewhere on-screen with a non-transparent portion overlapping the non-transparent portion of a BG tile.
-i’m not sure why it asks for y, since it doesn’t change the y scroll
-it’s actually very hard to do a mid-screen y scroll change, so this is probably for the best
-warning: all CPU time between the function call and the actual split point will be wasted!
-don’t use ppu_wait_frame() with this, you might have glitches
*note I changed split in my example code to only take 1 arguement (x).
Tile banks
-there are 2 sets of 256 tiles loaded to the ppu, ppu addresses 0-0x1fff
-sprites and bg can freely choose which tileset to use, or even both use the same set
bank_spr(unsigned char n); // which set of tiles for sprites
bank_spr(0); // use the first set of tiles
bank_spr(1); // use the second set of tiles
bank_bg(unsigned char n); // which set of tiles for background
bank_bg(0); // use the first set of tiles
bank_bg(1); // use the second set of tiles
rand8(void); // get a random number 0-255
a = rand8(); // a is char
rand16(void); // get a random number 0-65535
a = rand16(); // a is int
set_rand(unsigned int seed); // send an int (2 bytes) to seed the rng
-note, crt0 init code auto sets the seed to 0xfdfd
-you might want to use another seeding method, if randomness is important, like checking FRAME_CNT1 at the time of START pressed on title screen
set_vram_update(unsigned char *buf);
-sets a pointer to an array (a VRAM update buffer, somewhere in the RAM)
-when rendering is ON, this is how BG updates are made
usage…
set_vram_update(Some_ROM_Array); // sets a pointer to the data in ROM
(or)
memcpy(update_list,updateListData,sizeof(updateListData));
– copies data from ROM to a buffer, the buffer is called ‘update_list’
set_vram_update(update_list); // sets a pointer, and a flag to auto-update during the next v-blank
also…
set_vram_update(NULL);
-to disable updates, call this function with NULL pointer
The vram buffer should be filled like this…
Non-sequential:
-non-sequential means it will set a PPU address, then write 1 byte
-MSB, LSB, 1 byte data, repeat
-sequence terminated in 0xff (NT_UPD_EOF)
MSB = high byte of PPU address
LSB = low byte of PPU address
Sequential:
-sequential means it will set a PPU address, then write more than 1 byte to the ppu
-left to right (or) top to bottom
-MSB|NT_UPD_HORZ, LSB, # of bytes, a list of the bytes, repeat
or
-MSB|NT_UPD_VERT, LSB, # of bytes, a list of the bytes, repeat
-NT_UPD_HORZ, means it will write left to right, wrapping around to the next line
-NT_UPD_VERT, means is will write top to bottom, but a new address needs to be set after it reaches the bottom of the screen, as it will never wrap to the next column over
-sequence terminated in 0xff (NT_UPD_EOF)
#define NT_UPD_HORZ 0x40 = sequential
#define NT_UPD_VERT 0x80 = sequential
#define NT_UPD_EOF 0xff
Example of 4 sequential writes, left to right, starting at screen position x=1,y=2
tile #’s are 5,6,7,8
{
MSB(NTADR_A(1,2))|NT_UPD_HORZ,
LSB(NTADR_A(1,2)),
4, // 4 writes
5,6,7,8, // tile #’s
NT_UPD_EOF
};
Interestingly, it will continually write the same data, every v-blank, unless you send a NULL pointer like this…
set_vram_update(NULL);
…though, it may not make much difference.
The data set (aka vram buffer) must not be > 256 bytes, including the ff at the end of the data, and should not push more than…I don’t know, maybe * bytes of data to the ppu, since this happens during v-blank and not during rendering off, time is very very limited.
* Max v-ram changes per frame, with rendering on, before BAD THINGS start to happen…
sequential max = 97 (no palette change this frame),
74 (w palette change this frame)
non-sequential max = 40 (no palette change this frame),
31 (w palette change this frame)
the buffer only needs to be…
3 * 40 + 1 = 121 bytes in size
…as it can’t push more bytes than that, during v-blank.
(this hasn’t been tested on hardware, only FCEUX)
// all following vram functions only work when display is disabled
vram_adr(unsigned int adr);
-sets a PPU address
(sets a start point in the background for writing tiles)
vram_adr(NAMETABLE_A); // start at the top left of the screen
vram_adr(NTADR_A(x,y));
vram_adr(NTADR_A(5,6)); // sets a start position x=5,y=6
vram_put(unsigned char n); // puts 1 byte there
-use vram_adr(); first
vram_put(6); // push tile # 6 to screen
vram_fill(unsigned char n,unsigned int len); // repeat same tile * LEN
-use vram_adr(); first
-might have to use vram_inc(); first (see below)
vram_fill(1, 0x200); // tile # 1 pushed 512 times
vram_inc(unsigned char n); // mode of ppu
vram_inc(0); // data gets pushed into vram left to right (wraping to next line)
vram_inc(1); // data gets pushed into vram top to bottom (only works for 1 column (30 bytes), then you have to set another address).
-do this BEFORE writing to the screen, if you need to change directions
vram_read(unsigned char *dst,unsigned int size);
-reads a byte from vram
-use vram_adr(); first
-dst is where in RAM you will be storing this data from the ppu, size is how many bytes
vram_read(0x300, 2); // read 2 bytes from vram, write to RAM 0x300
NOTE, don’t read from the palette, just use the palette buffer at 0x1c0
vram_write(unsigned char *src,unsigned int size);
-write some bytes to the vram
-use vram_adr(); first
-src is a pointer to the data you are writing to the ppu
-size is how many bytes to write
vram_write(0x300, 2); // write 2 bytes to vram, from RAM 0x300
vram_write(TEXT,sizeof(TEXT)); // TEXT[] is an array of bytes to write to vram.
(For some reason this gave me an error, passing just an array name, had to cast to char * pointer)
vram_write((unsigned char*)TEXT,sizeof(TEXT));
vram_unrle(const unsigned char *data);
-pass it a pointer to the RLE data, and it will push it all to the PPU.
-this unpacks compressed data to the vram
-this is what you should actually use…this is what NES screen tool outputs best.
vram_unrle(titleRLE);
usage:
-first, disable rendering, ppu_off();
-set vram_inc(0) and vram_adr()
-wait for start of frame, with ppu_wait_nmi();
-call vram_unrle();
-then turn rendering back on, ppu_on_all()
-only load 1 nametable worth of data, per frame
NOTE:
-nmi is turned on in init, and never comes off
memcpy(void *dst,void *src,unsigned int len);
-moves data from one place to another…usually from ROM to RAM
memcpy(update_list,updateListData,sizeof(updateListData));
memfill(void *dst,unsigned char value,unsigned int len);
-fill memory with a value
memfill(0x200, 0, 0x100);
-to fill 0x200-0x2ff with zero…that is 0x100 bytes worth of filling
delay(unsigned char frames); // waits a # of frames
delay(5); // wait 5 frames
TECHNICAL NOTES, ON ASM BITS IN NESLIB.S:
-vram (besides the palette) is only updated if VRAM_UPDATE + NAME_UPD_ENABLE are set…
-ppu_wait_frame (or) ppu_wait_nmi, sets ‘UPDATE’
-set_vram_update, sets ‘ENABLE’
-set_vram_update(0); disables the vram ‘UPDATE’
-I guess you can’t set a pointer to the zero page address 0x0000, or it will never update.
-music only plays if FT_SONG_SPEED is set, play sets it, stop resets it, pause sets it to negative (ORA #$80), unpause clears that bit