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