Obsah
1. Zobrazení pozadí herní scény na konzoli NES
2. Datové struktury použité pro definici pozadí (background)
3. Smazání tabulky jmen i tabulky atributů
7. Shrnutí: kód pro inicializaci herní konzole a zahájení vykreslování
8. Úplný zdrojový kód prvního demonstračního příkladu
9. Pozadí zobrazené po překladu a spuštění příkladu
10. Zobrazení prvních 256 dlaždic z ROM
11. Současné zobrazení pozadí i spritů
12. Úplný zdrojový kód druhého demonstračního příkladu
13. Scéna zobrazená po překladu a spuštění příkladu a odstranění „smetí“
14. Priorita zobrazení spritů a pozadí
15. Realizace změny priority zobrazení spritů
16. Ovládání priority tlačítkem ovladače
17. Úplný zdrojový kód třetího demonstračního příkladu
18. Scéna zobrazená po překladu a spuštění příkladu
19. Repositář s demonstračními příklady
1. Zobrazení pozadí herní scény na konzoli NES
V předchozí trojici článků o vývoji her a dem pro herní konzoli NES [1] [2] [3] jsme se seznámili se způsobem zobrazení pohyblivých objektů na obrazovce – takzvaných spritů. Ovšem ještě si musíme říct, jakým způsobem je zobrazeno statické či scrollující pozadí (background). Poněkud paradoxně je zobrazení pozadí složitější než zobrazení spritů a proto se tomuto tématu věnujeme až dnes. Pochopitelně si popíšeme i to, jak lze řídit, zda se má vybraný sprite zobrazit před pozadím nebo až za ním (tedy oproti zvolené konstantní barvě podkladu).
Obrázek 1: Super Mario Bros ve verzi pro NES. Vidíme zde herní scénu složenou z pozadí i z několika spritů.
2. Datové struktury použité pro definici pozadí (background)
Z předchozích článků již víme, že u herní konzole NES se obraz posílaný na televizor skládá ze dvou částí: pozadí (background) a pohyblivých spritů. Nejprve si stručně popíšeme, jakým způsobem se definuje pozadí. Při použití televizní normy PAL je rozlišení obrazu rovno 256×240 pixelům, zatímco u normy SECAM bylo horních osm řádků a spodních osm řádků zatemněných, tj. rozlišení bylo sníženo na 256×224 pixelů. Teoreticky sice bylo možné vytvořit klasický framebuffer, v němž by bylo celé pozadí uloženo, ale při šestnáctibarevném obrazu, tj. při použití čtyř bitů na pixel, by musela být kapacita takto vytvořeného framebufferu poměrně velká: 28 kilobajtů (navíc je 16 „globálních“ barev relativně malé množství).
Konstruktéři čipu PPU tedy namísto toho využili technologii, s níž jsme se seznámili i u dalších typů herních konzolí: namísto framebufferu byly v obrazové paměti uloženy vzorky o velikosti 8×8 pixelů, které byly skládány do mřížky 32×30 dlaždic, což přesně odpovídá již zmíněnému rozlišení 256×240 pixelů (32×8=256, 30×8=240).
Základní datovou strukturou pro popis pozadí je struktura nazvaná Name Table o velikosti 960 bajtů, která obsahuje indexy všech tvarů tvořících dlaždicovitý obraz 32×30 dlaždic. Mohlo by se tedy zdát, že jedinou další potřebnou strukturou je tabulka všech vzorků (bitmap), z nichž každá má velikost 8×8 pixelů – ostatně naprosto stejně jsme postupovali u spritů. Situace je však v tomto případě poněkud složitější, protože na pozadí lze vykreslit až šestnáct různých barev. V tabulce vzorků (Pattern Table) jsou pro každý pixel vyhrazeny dva bity, tj. lze rozlišit čtyři možnosti/barvy.
Kromě toho existuje ještě tabulka atributů (Attribute Table) o velikosti 64 bajtů, která obsahuje horní dva bity pro oblast o velikosti 4×4 dlaždice, tj. 32×32 pixelů. Díky existenci této druhé tabulky je skutečně možné – i když s mnoha omezeními – použít na pozadí šestnáct různých barev – a to při minimálních paměťových nárocích. Důležité přitom je, že tabulka vzorků může být přemapována do ROM na paměťovém modulu, takže je možné poměrně jednoduchým způsobem například animovat celou scénu pouhou změnou „ukazatele“ na tuto tabulku (což mnohé hry skutečně dělaly).
3. Smazání tabulky jmen i tabulky atributů
Tabulka jmen je z pohledu PPU (nikoli CPU!) uložena od adresy $2000 a má délku přesně 960 bajtů (32×30). Ihned za touto tabulkou se nachází tabulka s atributy. Obě tabulky můžeme snadno smazat kódem popsaným v této kapitole. Nejprve si připravíme symbolická jména pro důležité adresy:
; Jména řídicích registrů použitých v kódu PPUSTATUS = $2002 PPUADDR = $2006 ; Další důležité adresy NAME_TABLE_0 = $2000
V dalším kroku nastavíme registry PPU pro zahájení přenosu:
; vynulování tabulek jmen
.proc clear_nametables
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
Dále si připravíme pracovní registry. Dvojice registrů X a Y budou tvořit 16bitový čítač, a to z toho důvodu, že budeme zapisovat více než 256 bajtů. Do tabulky jmen uložíme hodnoty $24, což v našem případě představuje dlaždici (tile) s modrou oblohou:
ldx #$08 ; počitadlo stránek (8)
ldy #$00 ; X a Y tvoří 16bitový čítač
lda #$24 ; dlaždice číslo $24 představuje oblohu
Následuje programová smyčka se zápisem hodnoty $24 přes několik paměťových stránek:
:
sta PPUDATA ; zápis indexu dlaždice
dey ; snížení hodnoty počitadla
bne :- ; skok dokud se nezaplní celá stránka
dex ; snížení hodnoty počitadla stránek
bne :- ; skok dokud se nezaplní 8 stránek
rts ; návrat ze subrutiny
.endproc
4. Vyplnění tabulky jmen
Nyní je všech 960 bajtů tabulky jmen nastaveno na hodnotu $24, což odpovídá modré obloze. Následující podprogram vyplní prvních 128 bajtů této tabulky odlišnými hodnotami získanými z dat uložených od adresy nametabledata:
; tabulka jmen
nametabledata:
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; první řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; druhý řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$45,$45,$24,$24,$45,$45,$45,$45,$45,$45,$24,$24 ; třetí řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24 ; různé cihly (horní polovina)
.byte $24,$24,$24,$24,$47,$47,$24,$24,$47,$47,$47,$47,$47,$47,$24,$24 ; čtvrtý řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24 ; různé cihly (spodní polovina)
128 bajtů odpovídá čtyřem řádkům v mřížce 32×30 „superdlaždic“, tedy dlaždic o velikosti 16×16 pixelů. Samotný přenos dat je snadný a vystačíme si zde pouze s využitím jednoho čítače (méně dat než 256 bajtů):
; načtení tabulky jmen
.proc load_nametable
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$00 ; počitadlo
:
lda nametabledata,X
sta PPUDATA ; zápis indexu dlaždice
inx
cpx #$80 ; chceme přenést 128 bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
5. Vyplnění tabulky atributů
Tabulka atributů má velikost 64 bajtů a obsahuje horní dva bity (výběr barvové palety) pro oblast o velikosti 4×4 dlaždice, tj. 32×32 pixelů. Vzhledem k tomu, že se v této tabulce pracuje vždy s dvojicemi bitů, je vhodné její obsah reprezentovat v bitové podobě (prefix % značí v assembleru binární hodnoty):
; tabulka atributů
attributedata:
.byte %00000000, %00010000, %00100000, %00000000, %00000000, %00000000, %00000000, %00110000
...
...
...
Formát:
7654 3210 |||| |||| |||| ||++- index barvy (bity 2 a 3) pro levý horní kvadrant |||| ++--- index barvy (bity 2 a 3) pro pravý horní kvadrant ||++------ index barvy (bity 2 a 3) pro levý dolní kvadrant ++-------- index barvy (bity 2 a 3) pro pravý dolní kvadrant
Tato tabulka začíná od adresy $23c0 (pochopitelně opět z pohledu PPU, nikoli CPU):
; Další důležité adresy ATTRIB_TABLE_0 = $23c0
Samotný přenos atributů je založen na kódu, který již dobře známe – naplnění registru s adresou PPU následovaný zápisem dat do registru PPUDATA. Prozatím nám bude stačit přenos osmi bajtů, protože „kostky“ pozadí jsou vyplněny na prvních čtyřech řádcích:
; načtení atributů
.proc load_attributes
lda PPUSTATUS ; reset záchytného registru
lda #>ATTRIB_TABLE_0 ; horní bajt adresy $23c0
sta PPUADDR
lda #<ATTRIB_TABLE_0 ; spodní bajt adresy $23c0
sta PPUADDR
ldx #$00 ; počitadlo smyčky
:
lda attributedata,X
sta PPUDATA ; zápis indexu
inx ; zvýšení hodnoty počitadla
cpx #$08 ; provádíme kopii osmi bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
6. Povolení zobrazení pozadí
Nyní nám již zbývá pouze nakonfigurovat PPU takovým způsobem, aby pozadí skutečně zobrazil. K tomu je nutné změnit obsah registrů PPUCTRL a PPUMASK.
V registru PPUCTRL povolíme NMI a nastavíme adresu pozadí (poslední a pátý bit):
7 bit 0
---- ----
VPHB SINN
|||| ||||
|||| ||++- Base nametable address
|||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
|||| |+--- VRAM address increment per CPU read/write of PPUDATA
|||| | (0: add 1, going across; 1: add 32, going down)
|||| +---- Sprite pattern table address for 8x8 sprites
|||| (0: $0000; 1: $1000; ignored in 8x16 mode)
|||+------ Background pattern table address (0: $0000; 1: $1000)
||+------- Sprite size (0: 8x8 pixels; 1: 8x16 pixels – see PPU OAM#Byte 1)
|+-------- PPU master/slave select
| (0: read backdrop from EXT pins; 1: output color on EXT pins)
+--------- Generate an NMI at the start of the
vertical blanking interval (0: off; 1: on)
V registru PPUMASK nastavíme bit povolující zobrazení pozadí:
7 bit 0 ---- ---- BGRs bMmG |||| |||| |||| |||+- Greyscale (0: normal color, 1: produce a greyscale display) |||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide |||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide |||| +---- 1: Show background |||+------ 1: Show sprites ||+------- Emphasize red (green on PAL/Dendy) |+-------- Emphasize green (red on PAL/Dendy) +--------- Emphasize blue
V programovém kódu to vypadá následovně:
cli ; vynulování bitu I - povolení přerušení
lda #%10010000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00011110 ; povolení zobrazení pozadí a současně i spritů
sta PPUMASK
7. Shrnutí: kód pro inicializaci herní konzole a zahájení vykreslování
Shrňme si nyní, které operace je nutné provést při inicializaci (RESETu) herní konzole tak, aby se zahájilo vykreslování pozadí. Pro zajímavost jsou některé operace realizovány formou makra, jiné formou subrutin (podprogramů):
; Obslužná rutina pro RESET
.proc reset
; nastavení stavu CPU
setup_cpu
; nastavení řídicích registrů
ldx #$40
stx $4017 ; zákaz IRQ z APU
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení tabulek jmen
jsr clear_nametables ; zavolání subrutiny
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; načtení tabulky jmen
jsr load_nametable ; zavolání subrutiny
; načtení atributů
jsr load_attributes ; zavolání subrutiny
cli ; vynulování bitu I - povolení přerušení
lda #%10010000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00001110 ; povolení zobrazení pozadí, nikoli ovšem spritů
sta PPUMASK
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
8. Úplný zdrojový kód prvního demonstračního příkladu
Úplný zdrojový kód dnešního prvního demonstračního příkladu (v pořadí již dvacátého pátého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example25.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example25.nes:
; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice pozadí a zobrazení pozadí
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Taktéž založeno na https://nerdy-nights.nes.science/#main_tutorial-3
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
; ---------------------------------------------------------------------
; Jména řídicích registrů použitých v kódu
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
DMC_FREQ = $4010
OAM_DMA = $4014
; Další důležité adresy
PALETTE = $3f00
NAME_TABLE_0 = $2000
ATTRIB_TABLE_0 = $23c0
; ---------------------------------------------------------------------
; Definice maker
; ---------------------------------------------------------------------
.macro setup_cpu
; nastavení stavu CPU
sei ; zákaz přerušení
cld ; vypnutí dekadického režimu (není podporován)
ldx #$ff
txs ; vrchol zásobníku nastaven na 0xff (první stránka)
.endmacro
.macro wait_for_frame
: bit PPUSTATUS ; test obsahu registru PPUSTATUS
bpl :- ; skok, pokud je příznak N nulový
.endmacro
.macro clear_ram
lda #$00 ; vynulování registru A
: sta $000, x ; vynulování X-tého bajtu v nulté stránce
sta $100, x
sta $200, x
sta $300, x
sta $400, x
sta $500, x
sta $600, x
sta $700, x ; vynulování X-tého bajtu v sedmé stránce
inx ; přechod na další bajt
bne :- ; po přetečení 0xff -> 0x00 konec smyčky
.endmacro
.macro ppu_data_palette_address
lda PPUSTATUS ; reset záchytného registru
lda #>PALETTE ; nastavení adresy pro barvovou paletu $3f00
sta PPUADDR
lda #<PALETTE ; nižší bajt adresy
sta PPUADDR
.endmacro
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
; Size of PRG in units of 16 KiB.
prg_npage = 2
; Size of CHR in units of 8 KiB.
chr_npage = 1
; INES mapper number.
mapper = 0
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
.segment "HEADER"
.byte $4e, $45, $53, $1a
.byte prg_npage
.byte chr_npage
.byte ((mapper & $0f) << 4) | (mirroring & 1)
.byte mapper & $f0
.segment "ZEROPAGE"
.segment "STARTUP"
.segment "CODE"
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
.segment "CHR0a"
.segment "CHR0b"
.code
; ---------------------------------------------------------------------
; Programový kód rutin pro NMI, RESET a IRQ volaných automaticky CPU
;
; viz též https://www.pagetable.com/?p=410
; ---------------------------------------------------------------------
; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
.proc nmi
lda #$02 ; horní bajt adresy pro přenos + zahájení přenosu
sta OAM_DMA
rti ; návrat z přerušení
.endproc
; Obslužná rutina pro IRQ (maskovatelné přerušení)
.proc irq
rti ; návrat z přerušení
.endproc
; Obslužná rutina pro RESET
.proc reset
; nastavení stavu CPU
setup_cpu
; nastavení řídicích registrů
ldx #$40
stx $4017 ; zákaz IRQ z APU
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení tabulek jmen
jsr clear_nametables ; zavolání subrutiny
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; načtení tabulky jmen
jsr load_nametable ; zavolání subrutiny
; načtení atributů
jsr load_attributes ; zavolání subrutiny
cli ; vynulování bitu I - povolení přerušení
lda #%10010000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00001110 ; povolení zobrazení pozadí, nikoli ovšem spritů
sta PPUMASK
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
; vynulování tabulek jmen
.proc clear_nametables
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$08 ; počitadlo stránek (8)
ldy #$00 ; X a Y tvoří 16bitový čítač
lda #$24 ; dlaždice číslo $24 představuje oblohu
:
sta PPUDATA ; zápis indexu dlaždice
dey ; snížení hodnoty počitadla
bne :- ; skok dokud se nezaplní celá stránka
dex ; snížení hodnoty počitadla stránek
bne :- ; skok dokud se nezaplní 8 stránek
rts ; návrat ze subrutiny
.endproc
; nastavení barvové palety
.proc load_palette
ppu_data_palette_address
; $3f00-$3f0f - paleta pozadí
; $3f10-$3f1f - paleta spritů
ldx #$00 ; vynulovat počitadlo a offset
:
lda palette, x ; načíst bajt s offsetem
sta PPUDATA ; zápis barvy do PPU
inx ; zvýšit počitadlo/offset
cpx #32 ; limit počtu barev
bne :- ; opakovat smyčku 32x
rts ; návrat ze subrutiny
.endproc
; načtení tabulky jmen
.proc load_nametable
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$00 ; počitadlo
:
lda nametabledata,X
sta PPUDATA ; zápis indexu dlaždice
inx
cpx #$80 ; chceme přenést 128 bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
; načtení atributů
.proc load_attributes
lda PPUSTATUS ; reset záchytného registru
lda #>ATTRIB_TABLE_0 ; horní bajt adresy $23c0
sta PPUADDR
lda #<ATTRIB_TABLE_0 ; spodní bajt adresy $23c0
sta PPUADDR
ldx #$00 ; počitadlo smyčky
:
lda attributedata,X
sta PPUDATA ; zápis indexu
inx ; zvýšení hodnoty počitadla
cpx #$08 ; provádíme kopii osmi bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
; tabulka jmen
nametabledata:
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; první řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; druhý řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$45,$45,$24,$24,$45,$45,$45,$45,$45,$45,$24,$24 ; třetí řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24 ; různé cihly (horní polovina)
.byte $24,$24,$24,$24,$47,$47,$24,$24,$47,$47,$47,$47,$47,$47,$24,$24 ; čtvrtý řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24 ; různé cihly (spodní polovina)
; tabulka atributů
attributedata:
.byte %00000000, %00010000, %00100000, %00000000, %00000000, %00000000, %00000000, %00110000
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
.segment "CHARS"
.incbin "mario.chr"
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------
9. Pozadí zobrazené po překladu a spuštění příkladu
Po překladu a spuštění předchozího demonstračního příkladu by se měla objevit tato herní scéna:
Obrázek 2: Pozadí zobrazené po překladu a spuštění příkladu.
10. Zobrazení prvních 256 dlaždic z ROM
Nepatrnou úpravou zdrojového kódu si můžeme nechat zobrazit prvních 256 dlaždic tak, jak jsou definovány v ROM:
Obrázek 3: Prvních 256 dlaždic získaných z ROM.
Úprava vypadá takto:
; načtení tabulky jmen
.proc load_nametable
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$00 ; počitadlo
:
stx ; samotné počitadlo bude použito jako index dlaždice
sta PPUDATA ; zápis indexu dlaždice
inx ; automaticky nastaví příznak zero při přetečení
; nemusíme tedy volat instrukci cpx
bne :-
rts ; návrat ze subrutiny
.endproc
11. Současné zobrazení pozadí i spritů
V naprosté většině her (snad až na úvodní obrazovku nebo obrazovku „Game over“) se současně zobrazují jak sprity, tak i pozadí. To vlastně není nic těžkého – musíme pouze vyplnit všechny potřebné paměťové regiony PPU a následně zobrazení povolit. Opět se podívejme na řídicí registr PPUMASK, který obsahuje jednotlivé bity, které musíme nastavit. Nastavované bity jsou zvýrazněny:
7 bit 0 ---- ---- BGRs bMmG |||| |||| |||| |||+- Greyscale (0: normal color, 1: produce a greyscale display) |||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide |||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide |||| +---- 1: Show background |||+------ 1: Show sprites ||+------- Emphasize red (green on PAL/Dendy) |+-------- Emphasize green (red on PAL/Dendy) +--------- Emphasize blue
V kódu bude toto nastavení vypadat následovně:
cli ; vynulování bitu I - povolení přerušení
lda #%10010000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00011110 ; povolení zobrazení pozadí a současně i spritů
sta PPUMASK
Dále pochopitelně musíme do programového kódu vrátit všechna makra a subrutiny použité pro zobrazení spritů.
12. Úplný zdrojový kód druhého demonstračního příkladu
Úplný zdrojový kód dnešního druhého demonstračního příkladu (v pořadí již dvacátého šestého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example26.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example26.nes:
; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice spritu a zobrazení spritů s Mariem
; Definice pozadí a zobrazení pozadí
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Taktéž založeno na https://nerdy-nights.nes.science/#main_tutorial-3
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
; ---------------------------------------------------------------------
; Jména řídicích registrů použitých v kódu
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
DMC_FREQ = $4010
OAM_DMA = $4014
; Další důležité adresy
PALETTE = $3f00
NAME_TABLE_0 = $2000
ATTRIB_TABLE_0 = $23c0
; ---------------------------------------------------------------------
; Definice maker
; ---------------------------------------------------------------------
.macro setup_cpu
; nastavení stavu CPU
sei ; zákaz přerušení
cld ; vypnutí dekadického režimu (není podporován)
ldx #$ff
txs ; vrchol zásobníku nastaven na 0xff (první stránka)
.endmacro
.macro wait_for_frame
: bit PPUSTATUS ; test obsahu registru PPUSTATUS
bpl :- ; skok, pokud je příznak N nulový
.endmacro
.macro clear_ram
lda #$00 ; vynulování registru A
: sta $000, x ; vynulování X-tého bajtu v nulté stránce
sta $100, x
sta $200, x
sta $300, x
sta $400, x
sta $500, x
sta $600, x
sta $700, x ; vynulování X-tého bajtu v sedmé stránce
inx ; přechod na další bajt
bne :- ; po přetečení 0xff -> 0x00 konec smyčky
.endmacro
.macro ppu_data_palette_address
lda PPUSTATUS ; reset záchytného registru
lda #>PALETTE ; nastavení adresy pro barvovou paletu $3f00
sta PPUADDR
lda #<PALETTE ; nižší bajt adresy
sta PPUADDR
.endmacro
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
; Size of PRG in units of 16 KiB.
prg_npage = 2
; Size of CHR in units of 8 KiB.
chr_npage = 1
; INES mapper number.
mapper = 0
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
.segment "HEADER"
.byte $4e, $45, $53, $1a
.byte prg_npage
.byte chr_npage
.byte ((mapper & $0f) << 4) | (mirroring & 1)
.byte mapper & $f0
.segment "ZEROPAGE"
.segment "STARTUP"
.segment "CODE"
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
.segment "CHR0a"
.segment "CHR0b"
.code
; ---------------------------------------------------------------------
; Programový kód rutin pro NMI, RESET a IRQ volaných automaticky CPU
;
; viz též https://www.pagetable.com/?p=410
; ---------------------------------------------------------------------
; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
.proc nmi
lda #$02 ; horní bajt adresy pro přenos + zahájení přenosu
sta OAM_DMA
rti ; návrat z přerušení
.endproc
; Obslužná rutina pro IRQ (maskovatelné přerušení)
.proc irq
rti ; návrat z přerušení
.endproc
; Obslužná rutina pro RESET
.proc reset
; nastavení stavu CPU
setup_cpu
; nastavení řídicích registrů
ldx #$40
stx $4017 ; zákaz IRQ z APU
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení tabulek jmen
jsr clear_nametables ; zavolání subrutiny
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; nastavení spritů
jsr load_sprites ; zavolání subrutiny
; načtení tabulky jmen
jsr load_nametable ; zavolání subrutiny
; načtení atributů
jsr load_attributes ; zavolání subrutiny
cli ; vynulování bitu I - povolení přerušení
lda #%10010000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00011110 ; povolení zobrazení pozadí a současně i spritů
sta PPUMASK
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
; vynulování barvové palety
.proc clear_palette
ppu_data_palette_address
ldx #$20 ; počitadlo barev v paletě: 16+16
lda #$00 ; vynulování každé barvy
:
sta PPUDATA ; zápis barvy
dex ; snížení hodnoty počitadla
bne :-
rts ; návrat ze subrutiny
.endproc
; vynulování tabulek jmen
.proc clear_nametables
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$08 ; počitadlo stránek (8)
ldy #$00 ; X a Y tvoří 16bitový čítač
lda #$24 ; dlaždice číslo $24 představuje oblohu
:
sta PPUDATA ; zápis indexu dlaždice
dey ; snížení hodnoty počitadla
bne :- ; skok dokud se nezaplní celá stránka
dex ; snížení hodnoty počitadla stránek
bne :- ; skok dokud se nezaplní 8 stránek
rts ; návrat ze subrutiny
.endproc
; nastavení barvové palety
.proc load_palette
ppu_data_palette_address
; $3f00-$3f0f - paleta pozadí
; $3f10-$3f1f - paleta spritů
ldx #$00 ; vynulovat počitadlo a offset
:
lda palette, x ; načíst bajt s offsetem
sta PPUDATA ; zápis barvy do PPU
inx ; zvýšit počitadlo/offset
cpx #32 ; limit počtu barev
bne :- ; opakovat smyčku 32x
rts ; návrat ze subrutiny
.endproc
; načtení spritů
.proc load_sprites
ldx #0 ; vynulování počitadla
:
lda spritedata,X ; budeme přesouvat data z této oblasti
sta $0200,X ; uložení do paměti spritů
inx ; zvýšení hodnoty počitadla
cpx #32 ; každý sprite má 4 bajty: y-coord, tile, attributy, y-coord * 8 spritů = 32
bne :-
rts ; návrat ze subrutiny
.endproc
; načtení tabulky jmen
.proc load_nametable
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$00 ; počitadlo
:
lda nametabledata,X
sta PPUDATA ; zápis indexu dlaždice
inx
cpx #$80 ; chceme přenést 128 bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
; načtení atributů
.proc load_attributes
lda PPUSTATUS ; reset záchytného registru
lda #>ATTRIB_TABLE_0 ; horní bajt adresy $23c0
sta PPUADDR
lda #<ATTRIB_TABLE_0 ; spodní bajt adresy $23c0
sta PPUADDR
ldx #$00 ; počitadlo smyčky
:
lda attributedata,X
sta PPUDATA ; zápis indexu
inx ; zvýšení hodnoty počitadla
cpx #$08 ; provádíme kopii osmi bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
; data pro osm spritů
spritedata:
.byte $10, $00, $00, $08 ; y-coord, tile number, attributes, x-coord
.byte $10, $01, $00, $10
.byte $18, $02, $00, $08
.byte $18, $03, $00, $10
.byte $20, $04, $00, $08
.byte $20, $05, $00, $10
.byte $28, $06, $00, $08
.byte $28, $07, $00, $10
; tabulka jmen
nametabledata:
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; první řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; druhý řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$45,$45,$24,$24,$45,$45,$45,$45,$45,$45,$24,$24 ; třetí řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24 ; různé cihly (horní polovina)
.byte $24,$24,$24,$24,$47,$47,$24,$24,$47,$47,$47,$47,$47,$47,$24,$24 ; čtvrtý řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24 ; různé cihly (spodní polovina)
; tabulka atributů
attributedata:
.byte %00000000, %00010000, %00100000, %00000000, %00000000, %00000000, %00000000, %00110000
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
.segment "CHARS"
.incbin "mario.chr"
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------
13. Scéna zobrazená po překladu a spuštění příkladu a odstranění „smetí“
Po překladu a spuštění předchozího příkladu by se měla na obrazovce NESu (tedy většinou v okně emulátoru) zobrazit tato scéna složená z osmi spritů a pozadí:
Obrázek 4: Herní scéna zobrazená po překladu a spuštění příkladu.
lda #%00011000 ; povolení zobrazení pozadí a současně i spritů
sta PPUMASK
Obrázek 5: Herní scéna zobrazená po překladu a spuštění upraveného příkladu.
14. Priorita zobrazení spritů a pozadí
Již v předchozích článcích jsme si řekli, že třetí bajt z celé čtyřbajtové struktury s metainformacemi o spritu obsahuje jedno bitové pole a tři samostatné bity, které řídí způsob zobrazení daného spritu (a to zcela nezávisle na ostatních spritech):
7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | +-+- Index barvové palety | | | | | | | | | +-+-+----- Nepoužito | | | | | +----------- Priorita (0: před pozadím; 1: za pozadím) | | | +------------- Horizontální zrcadlení spritu | +--------------- Vertikální zrcadlení spritu
Podívejme se nyní na to, jaký vliv má pátý bit na způsob zobrazení herní scény. V případě, že má daný sprite nastavenou prioritu „před pozadím“, je vždy zobrazen před všemi barvami pozadí. Pokud naopak má nastavenou prioritu „za pozadím“, je sprite viditelný pouze na těch pixelech, kde je barva pozadí nastavena na nulu. Pro všechny tři ostatní barvy pozadí je sprite skrytý. Důvodem, proč existuje výjimka pro barvu pozadí číslo 0, je zřejmá – v opačném případě by sprite nebyl viditelný nikde, neboť pozadí pokrývá celou plochu obrazovky.
15. Realizace změny priority zobrazení spritů
Změnu priority zobrazení spritů můžeme realizovat makrem, které již bylo popsáno minule. Konkrétně se jedná o makro, které dokáže změnit (invertovat) zvolený bit či bity v paměťovém bloku, přičemž změna nebude provedena v sousedních bajtech, ale s využitím zadané mezery (gap):
.macro flip_bit_block address, count, gap, mask
ldx #0 ; inicializace offsetu
:
lda address, x ; maskování hodnoty
eor #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
Konkrétně budeme měnit osm bajtů v bloku 8×4=32 bajtů. Bitová maska pro inverzi bitů je nastavena takovým způsobem, aby byl invertován pátý bit:
flip_bit_block ATTRS, 8, 4, %00100000
16. Ovládání priority tlačítkem ovladače
Ještě se podívejme na to, jak budeme měnit prioritu zobrazení spritů vůči pozadí. Tuto změnu bude možné provést tlačítkem B na ovladači, a to díky následujícímu kódu volanému při každém NMI (zobrazení snímku na obrazovce):
read_button ; stisk tlačítka B bude sloužit pro přepínání atributů spritů
beq button_b_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_b_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %00100000
17. Úplný zdrojový kód třetího demonstračního příkladu
Úplný zdrojový kód dnešního posledního demonstračního příkladu (v pořadí již dvacátého sedmého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example27.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example27.nes:
; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice spritu a zobrazení spritů s Mariem
; Definice pozadí a zobrazení pozadí
; Využití symbolických jmen adres.
; Pomocná makra pro pohyb spritu.
; Změna dalších vlastností spritů s využitím tlačítek A a B
;
; Založeno na příkladu https://github.com/depp/ctnes/tree/master/nesdev/01
; Taktéž založeno na https://nerdy-nights.nes.science/#main_tutorial-3
; Viz též článek na https://www.moria.us/blog/2018/03/nes-development
; Audio https://raw.githubusercontent.com/iliak/nes/master/doc/apu_ref.txt
; ---------------------------------------------------------------------
; Jména řídicích registrů použitých v kódu
PPUCTRL = $2000
PPUMASK = $2001
PPUSTATUS = $2002
PPUADDR = $2006
PPUDATA = $2007
DMC_FREQ = $4010
OAM_DMA = $4014
; Další důležité adresy
PALETTE = $3f00
NAME_TABLE_0 = $2000
ATTRIB_TABLE_0 = $23c0
; Ovladače
JOYPAD1 = $4016
JOYPAD2 = $4017
; Čítače
COUNTER1 = $00fe
COUNTER2 = $00ff
; ---------------------------------------------------------------------
; Definice maker
; ---------------------------------------------------------------------
.macro setup_cpu
; nastavení stavu CPU
sei ; zákaz přerušení
cld ; vypnutí dekadického režimu (není podporován)
ldx #$ff
txs ; vrchol zásobníku nastaven na 0xff (první stránka)
.endmacro
.macro wait_for_frame
: bit PPUSTATUS ; test obsahu registru PPUSTATUS
bpl :- ; skok, pokud je příznak N nulový
.endmacro
.macro clear_ram
lda #$00 ; vynulování registru A
: sta $000, x ; vynulování X-tého bajtu v nulté stránce
sta $100, x
sta $200, x
sta $300, x
sta $400, x
sta $500, x
sta $600, x
sta $700, x ; vynulování X-tého bajtu v sedmé stránce
inx ; přechod na další bajt
bne :- ; po přetečení 0xff -> 0x00 konec smyčky
.endmacro
.macro ppu_data_palette_address
lda PPUSTATUS ; reset záchytného registru
lda #>PALETTE ; nastavení adresy pro barvovou paletu $3f00
sta PPUADDR
lda #<PALETTE ; nižší bajt adresy
sta PPUADDR
.endmacro
.macro increment_block address, count, gap
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro decrement_block address, count, gap
ldx #0 ; inicializace offsetu
:
dec address, x ; zvýšit pozici spritu o jedničku
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro increment_block_mask address, count, gap, mask
ldx #0 ; inicializace offsetu
:
inc address, x ; zvýšit pozici spritu o jedničku
lda address, x ; maskování hodnoty
and #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro flip_bit_block address, count, gap, mask
ldx #0 ; inicializace offsetu
:
lda address, x ; maskování hodnoty
eor #mask
sta address, x
txa ; přesun offsetu do akumulátoru
clc
adc #gap ; zvýšení o hodnotu gap (4, další sprite)
tax ; přesun nového offsetu zpět do registru X
cmp #count*gap ; porovnání, zda jsme již dosáhli posledního spritu
bne :- ; pokud ne, skok na začátek smyčky
.endmacro
.macro read_button
lda JOYPAD1 ; stav tlačítka
and #%00000001 ; maskovat všechny bity kromě prvního
.endmacro
; ---------------------------------------------------------------------
; Definice hlavičky obrazu ROM
; ---------------------------------------------------------------------
; Size of PRG in units of 16 KiB.
prg_npage = 2
; Size of CHR in units of 8 KiB.
chr_npage = 1
; INES mapper number.
mapper = 0
; Mirroring (0 = horizontal, 1 = vertical)
mirroring = 1
.segment "HEADER"
.byte $4e, $45, $53, $1a
.byte prg_npage
.byte chr_npage
.byte ((mapper & $0f) << 4) | (mirroring & 1)
.byte mapper & $f0
.segment "ZEROPAGE"
.segment "STARTUP"
.segment "CODE"
; ---------------------------------------------------------------------
; Blok paměti s definicí dlaždic 8x8 pixelů
; ---------------------------------------------------------------------
.segment "CHR0a"
.segment "CHR0b"
.code
; ---------------------------------------------------------------------
; Programový kód rutin pro NMI, RESET a IRQ volaných automaticky CPU
;
; viz též https://www.pagetable.com/?p=410
; ---------------------------------------------------------------------
; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
.proc nmi
lda #$02 ; horní bajt adresy pro přenos + zahájení přenosu
sta OAM_DMA
lda #$01
sta JOYPAD1 ; načtení stavu všech osmi tlačítek do záchytného registru
lda #$00
sta JOYPAD1 ; začátek načítání jednotlivých bitů se stavy tlačítek v tomto pořadí:
;
; 1) A
; 2) B
; 3) Select
; 4) Start
; 5) Up
; 6) Down
; 7) Left
; 8) Right
XPOS = $0203 ; adresa buňky paměti s x-ovou souřadnicí spritu
YPOS = $0200 ; adresa buňky paměti y x-ovou souřadnicí spritu
ATTRS = $0202 ; adresa buňky paměti s atributy spritu
read_button ; stisk tlačítka A bude sloužit pro přepínání barvy spritů
beq button_a_not_pressed ; není stisknuto? => skok
dec COUNTER1
bne button_a_not_pressed
lda #10
sta COUNTER1
increment_block_mask ATTRS, 8, 4, 3
button_a_not_pressed:
read_button ; stisk tlačítka B bude sloužit pro přepínání atributů spritů
beq button_b_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_b_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %00100000
button_b_not_pressed:
read_button ; stisk tlačítka Select bude sloužit pro přepínání atributů spritů
beq button_select_not_pressed ; není stisknuto? => skok
dec COUNTER2
bne button_select_not_pressed
lda #10
sta COUNTER2
flip_bit_block ATTRS, 8, 4, %10000000
button_select_not_pressed:
read_button ; stav tlačítka Start jen načteme a ingorujeme
read_button ; stav tlačítka Up
beq up_not_pressed ; není stisknuto? => skok
decrement_block YPOS, 8, 4
up_not_pressed:
read_button ; stav tlačítka Down
beq down_not_pressed ; není stisknuto? => skok
increment_block YPOS, 8, 4
down_not_pressed:
read_button ; stav tlačítka Left
beq left_not_pressed ; není stisknuto? => skok
decrement_block XPOS, 8, 4
left_not_pressed:
read_button ; stav tlačítka Right
beq right_not_pressed ; není stisknuto? => skok
increment_block XPOS, 8, 4
right_not_pressed:
rti ; návrat z přerušení
.endproc
; Obslužná rutina pro IRQ (maskovatelné přerušení)
.proc irq
rti ; návrat z přerušení
.endproc
; Obslužná rutina pro RESET
.proc reset
; nastavení stavu CPU
setup_cpu
; nastavení řídicích registrů
ldx #$40
stx $4017 ; zákaz IRQ z APU
ldx #$00
stx PPUCTRL ; nastavení PPUCTRL = 0 (NMI)
stx PPUMASK ; nastavení PPUMASK = 0
stx DMC_FREQ ; zákaz DMC IRQ
ldx #$40
stx $4017 ; interrupt inhibit bit
; čekání na vnitřní inicializaci PPU (dva snímky)
wait_for_frame
wait_for_frame
; vymazání obsahu RAM
clear_ram
; čekání na další snímek
wait_for_frame
; nastavení tabulek jmen
jsr clear_nametables ; zavolání subrutiny
; nastavení barvové palety
jsr load_palette ; zavolání subrutiny
; nastavení spritů
jsr load_sprites ; zavolání subrutiny
; načtení tabulky jmen
jsr load_nametable ; zavolání subrutiny
; načtení atributů
jsr load_attributes ; zavolání subrutiny
cli ; vynulování bitu I - povolení přerušení
lda #%10010000
sta PPUCTRL ; při každém VBLANK se vyvolá NMI (důležité!)
lda #%00011110 ; povolení zobrazení pozadí a současně i spritů
sta PPUMASK
lda #10 ; inicializace čítačů
sta COUNTER1
sta COUNTER2
; vlastní herní smyčka je prozatím prázdná
game_loop:
jmp game_loop ; nekonečná smyčka (později rozšíříme)
.endproc
; vynulování barvové palety
.proc clear_palette
ppu_data_palette_address
ldx #$20 ; počitadlo barev v paletě: 16+16
lda #$00 ; vynulování každé barvy
:
sta PPUDATA ; zápis barvy
dex ; snížení hodnoty počitadla
bne :-
rts ; návrat ze subrutiny
.endproc
; vynulování tabulek jmen
.proc clear_nametables
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$08 ; počitadlo stránek (8)
ldy #$00 ; X a Y tvoří 16bitový čítač
lda #$24 ; dlaždice číslo $24 představuje oblohu
:
sta PPUDATA ; zápis indexu dlaždice
dey ; snížení hodnoty počitadla
bne :- ; skok dokud se nezaplní celá stránka
dex ; snížení hodnoty počitadla stránek
bne :- ; skok dokud se nezaplní 8 stránek
rts ; návrat ze subrutiny
.endproc
; nastavení barvové palety
.proc load_palette
ppu_data_palette_address
; $3f00-$3f0f - paleta pozadí
; $3f10-$3f1f - paleta spritů
ldx #$00 ; vynulovat počitadlo a offset
:
lda palette, x ; načíst bajt s offsetem
sta PPUDATA ; zápis barvy do PPU
inx ; zvýšit počitadlo/offset
cpx #32 ; limit počtu barev
bne :- ; opakovat smyčku 32x
rts ; návrat ze subrutiny
.endproc
; načtení spritů
.proc load_sprites
ldx #0 ; vynulování počitadla
:
lda spritedata,X ; budeme přesouvat data z této oblasti
sta $0200,X ; uložení do paměti spritů
inx ; zvýšení hodnoty počitadla
cpx #32 ; každý sprite má 4 bajty: y-coord, tile, attributy, y-coord * 8 spritů = 32
bne :-
rts ; návrat ze subrutiny
.endproc
; načtení tabulky jmen
.proc load_nametable
lda PPUSTATUS ; reset záchytného registru
lda #>NAME_TABLE_0 ; horní bajt adresy
sta PPUADDR
lda #<NAME_TABLE_0 ; spodní bajt adresy
sta PPUADDR
ldx #$00 ; počitadlo
:
lda nametabledata,X
sta PPUDATA ; zápis indexu dlaždice
inx
cpx #$80 ; chceme přenést 128 bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
; načtení atributů
.proc load_attributes
lda PPUSTATUS ; reset záchytného registru
lda #>ATTRIB_TABLE_0 ; horní bajt adresy $23c0
sta PPUADDR
lda #<ATTRIB_TABLE_0 ; spodní bajt adresy $23c0
sta PPUADDR
ldx #$00 ; počitadlo smyčky
:
lda attributedata,X
sta PPUDATA ; zápis indexu
inx ; zvýšení hodnoty počitadla
cpx #$08 ; provádíme kopii osmi bajtů
bne :-
rts ; návrat ze subrutiny
.endproc
; samotná barvová paleta
palette:
.byte $22, $29, $1a, $0F, $22, $36, $17, $0F, $22, $30, $21, $0F, $22, $27, $17, $0F ; barvy pozadí
.byte $22, $16, $27, $18, $22, $1A, $30, $27, $22, $16, $30, $27, $22, $0F, $36, $17 ; barvy spritů
; data pro osm spritů
spritedata:
.byte $10, $00, $00, $08 ; y-coord, tile number, attributes, x-coord
.byte $10, $01, $00, $10
.byte $18, $02, $00, $08
.byte $18, $03, $00, $10
.byte $20, $04, $00, $08
.byte $20, $05, $00, $10
.byte $28, $06, $00, $08
.byte $28, $07, $00, $10
; tabulka jmen
nametabledata:
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; první řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24 ; druhý řádek: nebe
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
.byte $24,$24,$24,$24,$45,$45,$24,$24,$45,$45,$45,$45,$45,$45,$24,$24 ; třetí řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24 ; různé cihly (horní polovina)
.byte $24,$24,$24,$24,$47,$47,$24,$24,$47,$47,$47,$47,$47,$47,$24,$24 ; čtvrtý řádek
.byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24 ; různé cihly (spodní polovina)
; tabulka atributů
attributedata:
.byte %00000000, %00010000, %00100000, %00000000, %00000000, %00000000, %00000000, %00110000
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
.segment "CHARS"
.incbin "mario.chr"
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------
18. Scéna zobrazená po překladu a spuštění příkladu
Pokud příklad přeložíme a spustíme, zobrazí se jak pozadí, tak i osmice spritů tvořících figurku Maria. Tlačítkem B je možné měnit prioritu zobrazení spritu vůči pozadí:
Obrázek 6: Sprity zobrazené před pozadím (zde konkrétně zdí).
Obrázek 7: Sprity zobrazené za pozadím (zde konkrétně zdí).
popř. jiná část stejné obrazovky:
Obrázek 8: Sprity zobrazené před pozadím (zde konkrétně zdí).
Obrázek 9: Sprity zobrazené za pozadím (zde konkrétně zdí).
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v assembleru, které jsou určené pro překlad pomocí assembleru ca65 (jenž je součástí cc65), byly uložen do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
20. Odkazy na Internetu
- The Thirty Million Line Problem
https://www.youtube.com/watch?v=kZRE7HIO3vk - NesDev.org
https://www.nesdev.org/ - The Sprite Attribute Byte
https://www.patater.com/nes-asm-tutorials/day-17/ - How to Program an NES game in C
https://nesdoug.com/ - Cycle reference chart
https://www.nesdev.org/wiki/Cycle_reference_chart - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - NES game development in 6502 assembly – Part 1
https://kibrit.tech/en/blog/nes-game-development-part-1 - NES (Nintendo Entertainment System) controller pinout
https://pinoutguide.com/Game/NES_controller_pinout.shtml - NES Controller Shift Register
https://www.allaboutcircuits.com/uploads/articles/nes-controller-arduino.png?v=1469416980041 - „Game Development in Eight Bits“ by Kevin Zurawel
https://www.youtube.com/watch?v=TPbroUDHG0s&list=PLcGKfGEEONaBjSfQaSiU9yQsjPxxDQyV8&index=4 - Game Development for the 8-bit NES: A class by Bob Rost
http://bobrost.com/nes/ - Game Development for the 8-bit NES: Lecture Notes
http://bobrost.com/nes/lectures.php - NES Graphics Explained
https://www.youtube.com/watch?v=7Co_8dC2zb8 - NES GAME PROGRAMMING PART 1
https://rpgmaker.net/tutorials/227/?post=240020 - NES 6502 Programming Tutorial – Part 1: Getting Started
https://dev.xenforo.relay.cool/index.php?threads/nes-6502-programming-tutorial-part-1-getting-started.858389/ - Minimal NES example using ca65
https://github.com/bbbradsmith/NES-ca65-example - List of 6502-based Computers and Consoles
https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/ - History of video game consoles (second generation): Wikipedia
http://en.wikipedia.org/wiki/History_of_video_game_consoles_(second_generation) - 6502 – the first RISC µP
http://ericclever.com/6500/ - 3 Generations of Game Machine Architecture
http://www.atariarchives.org/dev/CGEXPO99.html - bee – The Multi-Console Emulator
http://www.thebeehive.ws/ - Nerdy Nights Mirror
https://nerdy-nights.nes.science/ - The Nerdy Nights ca65 Remix
https://github.com/ddribin/nerdy-nights - NES Development Day 1: Creating a ROM
https://www.moria.us/blog/2018/03/nes-development - How to Start Making NES Games
https://www.matthughson.com/2021/11/17/how-to-start-making-nes-games/ - ca65 Users Guide
https://cc65.github.io/doc/ca65.html - cc65 Users Guide
https://cc65.github.io/doc/cc65.html - ld65 Users Guide
https://cc65.github.io/doc/ld65.html - da65 Users Guide
https://cc65.github.io/doc/da65.html - Nocash NES Specs
http://nocash.emubase.de/everynes.htm - Nintendo Entertainment System
http://cs.wikipedia.org/wiki/NES - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - NesDev
http://nesdev.parodius.com/ - 2A03 technical reference
http://nesdev.parodius.com/2A03%20technical%20reference.txt - NES Dev wiki: 2A03
http://wiki.nesdev.com/w/index.php/2A03 - Ricoh 2A03
http://en.wikipedia.org/wiki/Ricoh_2A03 - 2A03 pinouts
http://nesdev.parodius.com/2A03_pinout.txt - 27c3: Reverse Engineering the MOS 6502 CPU (en)
https://www.youtube.com/watch?v=fWqBmmPQP40 - “Hello, world” from scratch on a 6502 — Part 1
https://www.youtube.com/watch?v=LnzuMJLZRdU - A Tour of 6502 Cross-Assemblers
https://bumbershootsoft.wordpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/ - Nintendo Entertainment System (NES)
https://8bitworkshop.com/docs/platforms/nes/ - Question about NES vectors and PPU
https://archive.nes.science/nesdev-forums/f10/t4154.xhtml - How do mapper chips actually work?
https://archive.nes.science/nesdev-forums/f9/t13125.xhtml - INES
https://www.nesdev.org/wiki/INES - NES Basics and Our First Game
http://thevirtualmountain.com/nes/2017/03/08/nes-basics-and-our-first-game.html - Where is the reset vector in a .nes file?
https://archive.nes.science/nesdev-forums/f10/t17413.xhtml - CPU memory map
https://www.nesdev.org/wiki/CPU_memory_map - How to make NES music
http://blog.snugsound.com/2008/08/how-to-make-nes-music.html - Nintendo Entertainment System Architecture
http://nesdev.icequake.net/nes.txt - MIDINES
http://www.wayfar.net/0×f00000_overview.php - FamiTracker
http://famitracker.com/ - nerdTracker II
http://nesdev.parodius.com/nt2/ - How NES Graphics work
http://nesdev.parodius.com/nesgfx.txt - NES Technical/Emulation/Development FAQ
http://nesdev.parodius.com/NESTechFAQ.htm - Adventures with ca65
https://atariage.com/forums/topic/312451-adventures-with-ca65/ - example ca65 startup code
https://atariage.com/forums/topic/209776-example-ca65-startup-code/ - 6502 PRIMER: Building your own 6502 computer
http://wilsonminesco.com/6502primer/ - 6502 Instruction Set
https://www.masswerk.at/6502/6502_instruction_set.html - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Single-board computer
https://en.wikipedia.org/wiki/Single-board_computer - www.6502.org
http://www.6502.org/ - 6502 PRIMER: Building your own 6502 computer – clock generator
http://wilsonminesco.com/6502primer/ClkGen.html - Great Microprocessors of the Past and Present (V 13.4.0)
http://www.cpushack.com/CPU/cpu.html - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/ - Mikrořadiče a jejich použití v jednoduchých mikropočítačích
https://www.root.cz/clanky/mikroradice-a-jejich-pouziti-v-jednoduchych-mikropocitacich/ - Mikrořadiče a jejich aplikace v jednoduchých mikropočítačích (2)
https://www.root.cz/clanky/mikroradice-a-jejich-aplikace-v-jednoduchych-mikropocitacich-2/ - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Comparison of instruction set architectures
https://en.wikipedia.org/wiki/Comparison_of_instruction_set_architectures - Day 1 – Beginning NES Assembly
https://www.patater.com/nes-asm-tutorials/day-1/ - Day 2 – A Source Code File's Structure
https://www.patater.com/nes-asm-tutorials/day-2/ - Assembly Language Misconceptions
https://www.youtube.com/watch?v=8_0tbkbSGRE - How Machine Language Works
https://www.youtube.com/watch?v=HWpi9n2H3kE