了解 background 如何繪圖之后,sprite 也就簡(jiǎn)單一些了
1. OAM
細(xì)心的人可能會(huì)注意到,結(jié)合到 PPU 內(nèi)存映射時(shí)會(huì)發(fā)現(xiàn),PPU 總線上沒(méi)有和 sprite 直接相關(guān)的信息
- pattern table:存儲(chǔ)圖像數(shù)據(jù)
- name table:存儲(chǔ) background 在 pattern table 的索引
- palette:調(diào)色板
以上數(shù)據(jù)沒(méi)有任何一個(gè)指明了 sprite 的信息,那么 sprite 信息是放在哪里的?
這里就要提到 OAM 了,OAM 全稱(chēng) Object Attribute Memory,位于 PPU 芯片中,一共 256 bytes,一個(gè) sprite 用 4 bytes,所以總共能表示 64 個(gè) sprite。但是 OAM 并不存在于 PPU 或 CPU 總線上,需要 CPU 通過(guò) PPU 寄存器或者 DMA 方式才能寫(xiě)入
- 寄存器寫(xiě)入
通過(guò) OAMADDR(0x2003) 和 OAMDATA(0x2004) 寫(xiě)入 OAM 數(shù)據(jù),寫(xiě)入前首先通過(guò) OAMADDR 寫(xiě)入起始地址,之后通過(guò) OAMDATA 寫(xiě)入數(shù)據(jù),數(shù)據(jù)可以連續(xù)寫(xiě)入,每寫(xiě)入一次地址自動(dòng) +1 - DMA 寫(xiě)入
通過(guò) OAMDMA(0x4014) 寫(xiě)入 CPU PAGE 地址,之后 DMA 會(huì)自動(dòng)將 CPU 整個(gè) PAGE 的數(shù)據(jù)拷貝到 OAM 中。CPU PAGE 為 256 bytes,比如往 OAMDMA 寫(xiě)入 2,則會(huì)將 CPU 總線上的 0x200 ~ 0x2FF 的數(shù)據(jù)拷貝到 OAM。另外,DMA 會(huì)占用 512 個(gè) CPU 時(shí)鐘(奇數(shù) CPU 周期還會(huì)再加一個(gè)時(shí)鐘,前期可以先不考慮)
DMA 寫(xiě)入速度快于寄存器寫(xiě)入,所以追求效率的時(shí)候會(huì)采用此方式
2. Sprite 數(shù)據(jù)
一個(gè) Sprite 在 OAM 中需要 4 bytes:
- Byte 0:
Sprite 的 Y 坐標(biāo) - Byte 1:
該字節(jié)類(lèi)似于 name table,sprite 有 2 種模式:76543210 |||||||| |||||||+- Bank ($0000 or $1000) of tiles +++++++-- Tile number of top of sprite (0 to 254; bottom half gets the next tile)- 8 x 8
整個(gè) byte 類(lèi)似于 name table,由 PPUCTRL 的 bit 3 選取 bank 之后,加上自身數(shù)據(jù) x 16 得到偏移量 - 8 x 16
該模式下 PPUCTRL 的 bit 3 不再起作用,bank 由 bit 0 決定,并且偏移量不再是 x 16,而是 x 32,具體參考 http://wiki.nesdev.com/w/index.php/PPU_OAM,講得非常清楚
- 8 x 8
- Byte 2:
bit 0-1 決定高 2 bit 的 palette,類(lèi)似于 attribute table 的功能76543210 |||||||| ||||||++- Palette (4 to 7) of sprite |||+++--- Unimplemented ||+------ Priority (0: in front of background; 1: behind background) |+------- Flip sprite horizontally +-------- Flip sprite vertically
bit 5 決定優(yōu)先級(jí),如果 sprite 像素和 background 像素都不是透明像素的情況下(即 palette index % 4 != 0),則決定了到底顯示 sprite 還是 background
bit 6-7 決定是否翻轉(zhuǎn)像素,比如人物往右走設(shè)置為不翻轉(zhuǎn),往左走則設(shè)置為垂直翻轉(zhuǎn) - Byte 3:
Sprite 的 X 坐標(biāo)
3. Sprite 0 hits
Sprite 有一個(gè)非常特殊的地方,叫 精靈 0 命中,如果不實(shí)現(xiàn)這個(gè)功能的話,很多游戲都沒(méi)法正常運(yùn)行,比如馬里奧 1,沒(méi)實(shí)現(xiàn)的情況下,會(huì)一直卡在主界面
該功能的作用是,如果 PPU 在渲染的時(shí)候,如果 background 的不透明像素與 sprite 0 的不透明像素重疊的時(shí)候,會(huì)產(chǎn)生 sprite 0 hits,并且在當(dāng)前幀只會(huì)產(chǎn)生一次
同時(shí),產(chǎn)生 hit 也是有條件的,具體參考:http://wiki.nesdev.com/w/index.php/PPU_OAM#Sprite_zero_hits
那么它的作用是什么?主要用來(lái)分割屏幕。之前在 background 時(shí)介紹過(guò),CPU 通過(guò) PPUSCROLL 來(lái)控制背景移動(dòng),但是如果希望屏幕上半部分靜止,下半部分移動(dòng)呢?比如馬里奧 1:

可以看到頂部的狀態(tài)欄始終是靜止的,如果 CPU 不知道 PPU 繪制到哪里的情況下,這個(gè)功能沒(méi)有辦法實(shí)現(xiàn)。通過(guò) sprite 0,放置一個(gè) sprite 到需要的地方,產(chǎn)生 hit 之后,CPU 再修改 PPUSCROLL,就能達(dá)到屏幕分割的效果了
4. 時(shí)序
這個(gè)圖前面 2 章看過(guò)好幾遍了

相對(duì)于 background,sprite 時(shí)序簡(jiǎn)單一些
sprite 只在 pre-render line 和 visible line 求值,在 visible line 與 background 一起渲染
PPU 內(nèi)部有一塊 Secondary OAM 內(nèi)存,大小為 4 * 8 = 32 bytes,用來(lái)存儲(chǔ)當(dāng)前掃描線上的 8 個(gè) sprite,這也從側(cè)面說(shuō)明:PPU 最多支持 8 個(gè) sprite 在一條 scanline 上
- Scanline 的 1 - 64 周期,會(huì)清空 Secondary OAM,將 Secondary OAM 的值全寫(xiě)為 0xFF
- Scanline 的 65 - 256 周期,對(duì)下一條 scanline 的 Secondary OAM 求值,遍歷 OAM 內(nèi)存,將符合對(duì)應(yīng) scanline 的 OAM 寫(xiě)入 Secondary OAM
- Scanline 的 257 - 320 周期,通過(guò) Secondary OAM,計(jì)算對(duì)應(yīng) scanline 的 OAM 像素值,用于之后的渲染
詳細(xì)操作參考:http://wiki.nesdev.com/w/index.php/PPU_sprite_evaluation
5. 總結(jié)
至此 PPU 的原理已經(jīng)全過(guò)了一遍了,sprite 內(nèi)存數(shù)據(jù)和顯示這里也就不展示了,和 09 章對(duì) background 數(shù)據(jù)舉例的流程相同。另外代碼也就不舉例了,PPU 相比 CPU 確實(shí)復(fù)雜太多,沒(méi)法切分為小段代碼。最好結(jié)合 fceux 強(qiáng)大的調(diào)試功能,結(jié)合源碼查看