Vývoj her pro herní konzoli NES: plynulý posun pozadí (scrolling)

8. 9. 2022
Doba čtení: 39 minut

Sdílet

 Autor: Depositphotos
Užitečnou vlastností čipu PPU v konzoli NES je podpora pro scrolling celého pozadí. Tato technika je použita v mnoha hrách, například v Super Mario Bros či ve hře Castlevania. Dnes si ukážeme, jak na scrolling v assembleru.

Obsah

1. Vývoj her pro herní konzoli NES: scrolling

2. Vliv atributu mirroring v hlavičce ROM

3. Oprava obslužné rutiny RESET: povolení NMI až na samotném konci rutiny

4. Tabulka jmen a tabulka atributů pro vykreslení pozadí přes (téměř) celou obrazovku

5. Tabulka atributů

6. Nastavení scrollování pozadí před každým vykreslením snímku

7. Úplný zdrojový kód prvního demonstračního příkladu

8. Ukázka pozadí vykresleného prvním demonstračním příkladem

9. Realizace scrollingu pozadí v NMI rutině

10. Změna offsetů pro scrolling s využitím ovladače

11. Úplný zdrojový kód druhého demonstračního příkladu

12. Ukázky posunutého pozadí

13. Zobrazení aktuální hodnoty offsetů přímo na pozadí

14. Získání a uložení tvarů dvou hexadecimálních cifer

15. Integrace nového kódu do obslužné rutiny NMI

16. Úplný zdrojový kód třetího demonstračního příkladu

17. Ukázky posunutého pozadí se zobrazením offsetů

18. Od assembleru k jazyku C

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Vývoj her pro herní konzoli NES: scrolling

V předchozích částech tohoto seriálu jsme si postupně ukázali, jak se na herní konzoli NES tvoří hudba a zvuky, dále jsme si popsali i odzkoušeli způsob vykreslení spritů, včetně změny jejich vlastností (pozice, priorita vůči pozadí atd.). Minule jsme si řekli, jak se vykresluje pozadí herní scény. Statické pozadí se ovšem používá jen v některých hrách; z těch známějších například ve hře Donkey Kong a samozřejmě i v Tetrisu. Ovšem mnohé další hry dokážou pozadím scrollovat, tj. pohybovat jím v horizontálním a/nebo i ve vertikálním směru. Příkladem jsou různé střílečky, ale i například populární hra Castlevania.

Obrázek 1: Úvodní obrazovka hry Castlevania I na NESu.

Nejjednodušší je „skokový“ posun pozadí o šestnáct pixelů jakýmkoli směrem, protože v tomto případě stačí jen přepsat hodnoty v tabulce jmen i tabulce atributů (blokový přenos cca 1kB dat). Ovšem již posun o osm pixelů může kolidovat s tabulkami atributů, protože atributy jsou uloženy vždy pro čtyři sousední dlaždice. Nás ovšem bude zajímat plynulý posun pozadí o jednotlivé pixely, což je hardwarem herní konzole NES taktéž umožněno.

Obrázek 2: Pěkně navržená grafika hry Castlevania I (obrázek má v tomto případě pouze 15 barev).

Obrázek 3: Castlevania I na NES.

Obrázek 4: Castlevania I na NES.

Obrázek 5: Castlevania I na NES.

2. Vliv atributu mirroring v hlavičce ROM

Na to, jak bude scrolling probíhat, má vliv bitový atribut mirroring ukládaný do hlavičky ROM s hrou:

; 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

Změnou tohoto atributu lze ovlivnit, jak se budou při scrollingu chovat ty části scény, které leží „mimo původní obrazovku“, přesněji řečeno mimo hodnoty uložené ve jmenné tabulce. Při nastavení vertikálního zrcadlení bude situace vypadat následovně, zatímco u horizontálního zrcadlení bude vypadat takto. Prozatím si však vystačíme pouze s jednou jmennou tabulkou s tím, že při scrollingu postupně budeme sledovat hodnoty v tabulkách obsahujících jen prázdné dlaždice.

3. Oprava obslužné rutiny RESET: povolení NMI až na samotném konci rutiny

Na rozdíl od předchozích demonstračních příkladů, v nichž v rutině RESET neprobíhaly dlouhotrvající operace, nyní musíme vykreslit rozsáhlejší pozadí. Z tohoto důvodu je nutné zajistit, aby se NMI (tedy přerušení zavolané před vykreslením dalšího snímku) zavolalo až na samotném konci rutiny RESET, ideálně až v nekonečné herní smyčce. To lze zařídit snadno – přesunem kódu pro povolení přerušení až před samotnou smyčku:

; Obslužná rutina pro RESET
 
.proc reset
        ...
        ...
        ...
        lda #%00001110        ; povolení zobrazení pozadí, nikoli ovšem spritů
        sta PPUMASK
 
        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é!)
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
Poznámka: opomenutí této opravy vás může stát hodně času při ladění „náhodných chyb“; ostatně přesně toto se stalo autorovi tohoto článku :-)

4. Tabulka jmen a tabulka atributů pro vykreslení pozadí přes (téměř) celou obrazovku

Na rozdíl od minule popsaných příkladů budeme dnes potřebovat vykreslit pozadí přes prakticky celou obrazovku. Víme již, že definice pozadí vyžaduje vyplněnou tabulku jmen (32×30=960 bajtů s indexy jednotlivých „dlaždic“). Navíc se vždy pro každou čtveřici 2×2 dlaždic vyplňují i dva bity v tabulce atributů, které ovlivňují vybranou barvovou paletu. Velikost této tabulky je 16×15/4=60 bajtů, i když se většinou vyplňuje celých 64 bajtů (což je i náš případ).

Samotné pozadí lze teoreticky navrhnout na čtverečkovaném papíru, ale k dispozici jsou i specializované editory pro NES. Následující definice pozadí vznikla ručně, prakticky jen na papíře (tedy tak, jak za starých dobrých časů) s využitím dlaždic zobrazených v Tilemonsteru (ovšem snadno si můžete vyplnit pozadí všemi dlaždicemi a vycházet přímo z nich):

; tabulka jmen
nametabledata:
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
 
    .byte $24,$1b,$18,$18,$1d,$24,$25,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24

5. Tabulka atributů

Následuje příslušná tabulka atributů, přičemž jsem zde použil zápis binárních hodnot, neboť význam mají vždy dvojice sousedních bitů:

; tabulka atributů
attributedata:
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111 ; žluto-hnědá paleta
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; zelená paleta
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
    .byte %10101010, %10101010, %10101010, %10111111, %11111111, %10101010, %11111111, %10101010
    .byte %01010101, %10101010, %01010101, %11101111, %11111111, %01010101, %11111111, %01010101
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; jen obloha - průhledné pixely
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000

6. Nastavení scrollování pozadí před každým vykreslením snímku

Jeden z řídicích registrů čipu PPU (tedy grafického řadiče NESu) je registr nazvaný SCROLL, jenž z pohledu mikroprocesoru leží na adrese $2005:

; Jména řídicích registrů použitých v kódu
PPUCTRL         = $2000
PPUMASK         = $2001
PPUSTATUS       = $2002
SCROLL          = $2005
PPUADDR         = $2006
PPUDATA         = $2007
DMC_FREQ        = $4010
OAM_DMA         = $4014

Do tohoto řídicího registru je možné před vykreslením snímku zapsat dvě po sobě jdoucí hodnoty. První hodnotou je horizontální posun pozadí v pixelech, druhou hodnotou vertikální posun pozadí, taktéž měřený v pixelech. Pokud tedy scrolling nechceme používat, měl by konec rutiny NMI (volané v průběhu VBLANKu, jak již dobře víme), vypadat takto:

; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
 
.proc nmi
        lda #0
        sta SCROLL        ; zákaz scrollingu
        sta SCROLL
        lda #$02          ; horní bajt adresy pro přenos + zahájení přenosu
        sta OAM_DMA
        rti               ; návrat z přerušení
.endproc
Poznámka: ve skutečnosti je možné měnit hodnotu tohoto registru i během vykreslování, čehož se používá pro rozdělení pozadí na statickou část a scrollující část. Podrobnosti si vysvětlíme později – jedná se totiž o kód, který vyžaduje přesné načasování.

7. Ú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 osmého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example28.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example28.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
SCROLL          = $2005
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 #0
        sta SCROLL        ; zákaz scrollingu
        sta SCROLL
        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
 
        lda #%00001110        ; povolení zobrazení pozadí, nikoli ovšem spritů
        sta PPUMASK
 
        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é!)
 
        ; 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
        bne :-
 
        ldx #$00           ; počitadlo
:
        lda nametabledata+256,X
        sta PPUDATA        ; zápis indexu dlaždice
        inx
        bne :-
 
        ldx #$00           ; počitadlo
:
        lda nametabledata+512,X
        sta PPUDATA        ; zápis indexu dlaždice
        inx
        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 #0               ; počitadlo 64 bajtů
 
:
        lda attributedata,X  ; načtení čtyř atributů
        sta PPUDATA          ; zápis indexu
        inx                  ; snížení hodnoty počitadla
        cpx #64
        bne :-               ; opakování smyčky
 
        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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
 
    .byte $24,$1b,$18,$18,$1d,$24,$25,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
 
; tabulka atributů
attributedata:
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111 ; žluto-hnědá paleta
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; zelená paleta
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
    .byte %10101010, %10101010, %10101010, %10111111, %11111111, %10101010, %11111111, %10101010
    .byte %01010101, %10101010, %01010101, %11101111, %11111111, %01010101, %11111111, %01010101
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; jen obloha - průhledné pixely
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
.segment "CHARS"
    .incbin "mario.chr"
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

8. Ukázka pozadí vykresleného prvním demonstračním příkladem

Po překladu dnešního prvního demonstračního příkladu příkazem:

$ make example28.nes

Můžeme tento příklad spustit v emulátoru herní konzole NES, například v Nestopii:

$ nestopia example28.nes

Výsledkem by měla být tato obrazovka se statickým pozadím:

Obrázek 6: Výsledek běhu dnešního prvního demonstračního příkladu.

9. Realizace scrollingu pozadí v NMI rutině

Podívejme se nyní na způsob realizace scrollingu celého pozadí. Nejprve si nadeklarujeme pojmenované buňky, které budou obsahovat informaci o tom, o kolik pixelů se má pozadí posunout, a to jak v horizontálním směru, tak i ve směru vertikálním. Tyto buňky můžeme alokovat například na začátek druhé stránky paměti (dostupné CPU, nikoli PPU) nebo (pro kratší a rychlejší kód) do nulté stránky paměti. Buňky si pojmenujeme, například následujícím způsobem:

; Adresy globálních proměnných
XSCROLL         = $0203       ; adresa buňky paměti pro scrolling ve směru x-ové osy
YSCROLL         = $0200       ; adresa buňky paměti pro scrolling ve směru y-ové osy

Nyní bude v assembleru instrukce:

        lda XSCROLL

znamenat „načti do akumulátoru obsah paměťové buňky $0203“

a instrukce:

        sta XSCROLL

bude naopak znamenat „ulož obsah akumulátoru do paměťové buňky $0203“.

Prakticky na samotném konci obslužné rutiny NMI je nutné hodnoty obou offsetů uložit do řídicího registru SCROLL, a to přesně v uvedeném pořadí:

; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
 
.proc nmi
        ...
        ...
        ...
        lda XSCROLL           ; nastavení scrollingu
        sta SCROLL
        lda YSCROLL
        sta SCROLL
 
        lda #$02              ; horní bajt adresy pro přenos + zahájení přenosu
        sta OAM_DMA
        rti                   ; návrat z přerušení
.endproc
Poznámka: samotný registr SCROLL je uložen na této adrese:
SCROLL          = $2005

10. Změna offsetů pro scrolling s využitím ovladače

Samotná modifikace offsetů, tedy hodnot uložených na adresách XSCROLL a YSCROLL, je vlastně triviální, protože jsme podobný kód viděli při posunu spritů. Budeme testovat stav všech tlačítek ovladače a pokud se bude detekovat stisk tlačítek nahoru, dolů, doprava či doleva, provedeme pouhou změnu hodnoty buňky XSCROLL nebo YSCROLL. V případě mikroprocesoru MOS 6502 lze tuto operaci provést velmi snadno jedinou instrukcí INC nebo DEC, tj. bez nutnosti načítání hodnoty do akumulátoru, provedení příslušné operace a uložení výsledku zpět do operační paměti.

Nejprve je nutné realizovat načtení stavu všech osmi tlačítek na ovladači do záchytného registru:

        lda #$01
        sta JOYPAD1           ; načtení stavu všech osmi tlačítek do záchytného registru

Následně se stav všech tlačítek přenese do posuvné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

Tlačítka A, B, Select a Start budeme ignorovat, ovšem z posuvného registru je musíme vyčíst:

        read_button           ; detekce stisku tlačítka A
        read_button           ; detekce stisku tlačítka B
        read_button           ; detekce stisku tlačítka Select
        read_button           ; detekce stisku tlačítka Start

Pouze směrová tlačítka ovlivní scrolling. Tyto operace jsou přímočaré:

        read_button           ; detekce stisku tlačítka Up
        beq up_not_pressed    ; není stisknuto? => skok
        inc YSCROLL           ; zvýšení offsetu
 
up_not_pressed:
        read_button           ; detekce stisku tlačítka Down
        beq down_not_pressed  ; není stisknuto? => skok
        dec YSCROLL           ; snížení offsetu
 
down_not_pressed:
        read_button           ; detekce stisku tlačítka Left
        beq left_not_pressed  ; není stisknuto? => skok
        inc XSCROLL           ; zvýšení offsetu
 
left_not_pressed:
        read_button           ; detekce stisku tlačítka Right
        beq right_not_pressed ; není stisknuto? => skok
        dec XSCROLL           ; snížení offsetu
 
right_not_pressed:

11. Ú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 devátého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example29.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example29.nes:

; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice pozadí a zobrazení pozadí
; Scrolling 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
SCROLL          = $2005
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
 
; Adresy globálních proměnných
XSCROLL         = $0203       ; adresa buňky paměti pro scrolling ve směru x-ové osy
YSCROLL         = $0200       ; adresa buňky paměti pro scrolling ve směru y-ové osy
 
 
 
; ---------------------------------------------------------------------
; 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 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 #$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
 
        read_button           ; detekce stisku tlačítka A
        read_button           ; detekce stisku tlačítka B
        read_button           ; detekce stisku tlačítka Select
        read_button           ; detekce stisku tlačítka Start
 
        read_button           ; detekce stisku tlačítka Up
        beq up_not_pressed    ; není stisknuto? => skok
        inc YSCROLL           ; zvýšení offsetu
 
up_not_pressed:
        read_button           ; detekce stisku tlačítka Down
        beq down_not_pressed  ; není stisknuto? => skok
        dec YSCROLL           ; snížení offsetu
 
down_not_pressed:
        read_button           ; detekce stisku tlačítka Left
        beq left_not_pressed  ; není stisknuto? => skok
        inc XSCROLL           ; zvýšení offsetu
 
left_not_pressed:
        read_button           ; detekce stisku tlačítka Right
        beq right_not_pressed ; není stisknuto? => skok
        dec XSCROLL           ; snížení offsetu
 
right_not_pressed:
        lda XSCROLL           ; nastavení scrollingu
        sta SCROLL
        lda YSCROLL
        sta SCROLL
 
        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
 
        lda #%00001110        ; povolení zobrazení pozadí, nikoli ovšem spritů
        sta PPUMASK
 
        lda #0                ; vynulovat počitadlo spritů
        sta XSCROLL
        sta YSCROLL
 
        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é!)
 
        ; 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
        bne :-
 
        ldx #$00           ; počitadlo
:
        lda nametabledata+256,X
        sta PPUDATA        ; zápis indexu dlaždice
        inx
        bne :-
 
        ldx #$00           ; počitadlo
:
        lda nametabledata+512,X
        sta PPUDATA        ; zápis indexu dlaždice
        inx
        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 #0               ; počitadlo 64 bajtů
 
:
        lda attributedata,X  ; načtení čtyř atributů
        sta PPUDATA          ; zápis indexu
        inx                  ; snížení hodnoty počitadla
        cpx #64
        bne :-               ; opakování smyčky
 
        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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
 
    .byte $24,$1b,$18,$18,$1d,$24,$25,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
 
; tabulka atributů
attributedata:
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111 ; žluto-hnědá paleta
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; zelená paleta
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
    .byte %10101010, %10101010, %10101010, %10111111, %11111111, %10101010, %11111111, %10101010
    .byte %01010101, %10101010, %01010101, %11101111, %11111111, %01010101, %11111111, %01010101
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; jen obloha - průhledné pixely
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
.segment "CHARS"
    .incbin "mario.chr"
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

12. Ukázky posunutého pozadí

Podívejme se nyní na to, jak mohou vypadat scény vykreslené dnešním druhým demonstračním příkladem. Povšimněte si, že pro některé posuny (offsety) se vykreslí pouze modrá obrazovka, tj. vlastně druhá obrazovka uložená v paměti PPU:

Obrázek 7: Posun pozadí doleva.

Obrázek 8: Posun pozadí doprava.

Obrázek 9: Posun pozadí nahoru.

Obrázek 10: Posun pozadí dolů.

Poznámka: oproti předchozímu příkladu byl příznakový bit mirror znegován.

13. Zobrazení aktuální hodnoty offsetů přímo na pozadí

Herní konzole NES nám neumožňuje snadnou realizaci funkce typu printf nebo alespoň puts, ovšem při práci s objekty, jejichž hodnoty se mohou měnit, je vhodné nějakým způsobem zajistit zobrazení příslušných hodnot přímo na pozadí. To se týká mj. i offsetů pro posun pozadí, což jsou dvě hodnoty v rozsahu od 0 do 255. Každou z těchto hodnot zobrazíme jako dvojici hexadecimálních číslic na plochu pozadí. Proč hexadecimálních? Je tomu tak z toho důvodu, že převod hodnoty 0..255 na dvojici hexadecimálních číslic je v assembleru snadný, resp. mnohem snadnější, než pokusy o dělení deseti atd.

Jakmile vypočteme jednu z cifer 0-F, můžeme ji zobrazit ve formě dlaždice s indexem taktéž 0-F, a to díky tomu, že prvních deset dlaždic „náhodou“ obsahuje právě číslice následované znaky abecedy:

Obrázek 11: Dlaždice, které lze použít pro výpis zpráv na pozadí.

Subrutina pro zobrazení obou offsetů může vypadat takto. Povšimněte si volby horního a spodního bajtu tak, aby byly offsety zobrazeny zhruba uprostřed:

; zobrazení offsetů
.proc display_scroll_offsets
        lda PPUSTATUS         ; reset záchytného registru
        lda #>NAME_TABLE_0+2  ; horní bajt adresy
        sta PPUADDR
        lda #<NAME_TABLE_0+10 ; spodní bajt adresy
        sta PPUADDR
 
        draw_two_digits XSCROLL
 
        lda #$24              ; mezera (tvar oblohy)
        sta PPUDATA           ; uložení tvaru (cifry) do pozadí
 
        draw_two_digits YSCROLL
 
        rts
.endproc
Poznámka: mezi dvojicí číslic je uložena dlaždice obsahující jen barvu oblohy, tedy vlastně mezera.

14. Získání a uložení tvarů dvou hexadecimálních cifer

V předchozí subrutině bylo voláno makro draw_two_digits, které pro hodnotu uloženou na zvolené adrese address získá nejprve horní číslici a uloží ji do registru PPUDATA a posléze totéž provede s dolní číslicí. Bitové (logické) posuny lze provádět pouze o jediný bit, takže je kód nepatrně delší:

; zobrazení dvou hexadecimálních cifer na pozadí
.macro draw_two_digits address
        lda address           ; přečíst offset pro scrolling
        lsr a                 ; získání horního nibblu
        lsr a
        lsr a
        lsr a
        sta PPUDATA           ; uložení tvaru (cifry) do pozadí
 
        lda address           ; opět přečíst offset pro scrolling
        and #$0f              ; získání horního nibblu
        sta PPUDATA           ; uložení tvaru (cifry) do pozadí
.endmacro

15. Integrace nového kódu do obslužné rutiny NMI

Jak čtení tlačítek ovladače, tak i zobrazení hodnoty horizontálního a vertikálního posunu (XSCROLL a YSCROLL), včetně nastavení nových posunů, můžeme realizovat přímo v obslužné rutině NMI. Jedná se totiž o relativně krátké množství kódu, které je možné „stihnout“ právě v okamžiku, než začne vykreslování nového snímku. U složitějších her to však již možné nebude, takže v NMI se pouze nastaví nové hodnoty a přepočet se provede v průběhu vykreslování dalšího snímku (což již však budeme z důvodu větší čitelnosti provádět raději v céčku):

; Obslužná rutina pro NMI (nemaskovatelné přerušení, vertical blank)
 
.proc nmi
        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
 
        read_button           ; detekce stisku tlačítka A
        read_button           ; detekce stisku tlačítka B
        read_button           ; detekce stisku tlačítka Select
        read_button           ; detekce stisku tlačítka Start
 
        read_button           ; detekce stisku tlačítka Up
        beq up_not_pressed    ; není stisknuto? => skok
        inc YSCROLL           ; zvýšení offsetu
 
up_not_pressed:
        read_button           ; detekce stisku tlačítka Down
        beq down_not_pressed  ; není stisknuto? => skok
        dec YSCROLL           ; snížení offsetu
 
down_not_pressed:
        read_button           ; detekce stisku tlačítka Left
        beq left_not_pressed  ; není stisknuto? => skok
        inc XSCROLL           ; zvýšení offsetu
 
left_not_pressed:
        read_button           ; detekce stisku tlačítka Right
        beq right_not_pressed ; není stisknuto? => skok
        dec XSCROLL           ; snížení offsetu
 
right_not_pressed:
 
        jsr display_scroll_offsets
 
        lda XSCROLL           ; nastavení scrollingu
        sta SCROLL
        lda YSCROLL
        sta SCROLL
 
        lda #$02              ; horní bajt adresy pro přenos + zahájení přenosu
        sta OAM_DMA
        rti                   ; návrat z přerušení
.endproc

16. Ú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ž třicátého příkladu pro NES) je dostupný na adrese https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example30.asm. Pro překlad a slinkování tohoto příkladu je zapotřebí i Makefile a příkaz make example30.nes:

; ---------------------------------------------------------------------
; Kostra programu pro herní konzoli NES
; Nastavení barvové palety, zvýšení intenzity barvy
; Setup PPU přes makro
; Definice pozadí a zobrazení pozadí.
; Scrolling pozadí s využitím ovladače.
;
; 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
SCROLL          = $2005
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
 
; Adresy globálních proměnných
XSCROLL         = $0203       ; adresa buňky paměti pro scrolling ve směru x-ové osy
YSCROLL         = $0200       ; adresa buňky paměti pro scrolling ve směru y-ové osy
 
 
 
; ---------------------------------------------------------------------
; 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 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 = 0
 
.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
        jsr display_scroll_offsets
 
        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
 
        read_button           ; detekce stisku tlačítka A
        read_button           ; detekce stisku tlačítka B
        read_button           ; detekce stisku tlačítka Select
        read_button           ; detekce stisku tlačítka Start
 
        read_button           ; detekce stisku tlačítka Up
        beq up_not_pressed    ; není stisknuto? => skok
        inc YSCROLL           ; zvýšení offsetu
 
up_not_pressed:
        read_button           ; detekce stisku tlačítka Down
        beq down_not_pressed  ; není stisknuto? => skok
        dec YSCROLL           ; snížení offsetu
 
down_not_pressed:
        read_button           ; detekce stisku tlačítka Left
        beq left_not_pressed  ; není stisknuto? => skok
        inc XSCROLL           ; zvýšení offsetu
 
left_not_pressed:
        read_button           ; detekce stisku tlačítka Right
        beq right_not_pressed ; není stisknuto? => skok
        dec XSCROLL           ; snížení offsetu
 
right_not_pressed:
 
        lda XSCROLL           ; nastavení scrollingu
        sta SCROLL
        lda YSCROLL
        sta SCROLL
 
        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
 
        lda #%00001110        ; povolení zobrazení pozadí, nikoli ovšem spritů
        sta PPUMASK
 
        lda #0                ; vynulovat počitadlo spritů
        sta XSCROLL
        sta YSCROLL
 
        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é!)
 
        ; vlastní herní smyčka je prozatím prázdná
game_loop:
        jmp game_loop           ; nekonečná smyčka (později rozšíříme)
.endproc
 
; zobrazení dvou hexadecimálních cifer na pozadí
.macro draw_two_digits address
        lda address           ; přečíst offset pro scrolling
        lsr a                 ; získání horního nibblu
        lsr a
        lsr a
        lsr a
        sta PPUDATA           ; uložení tvaru (cifry) do pozadí
 
        lda address           ; opět přečíst offset pro scrolling
        and #$0f              ; získání horního nibblu
        sta PPUDATA           ; uložení tvaru (cifry) do pozadí
.endmacro
 
; zobrazení offsetů
.proc display_scroll_offsets
        lda PPUSTATUS         ; reset záchytného registru
        lda #>NAME_TABLE_0+2  ; horní bajt adresy $23c0
        sta PPUADDR
        lda #<NAME_TABLE_0+10 ; spodní bajt adresy $23c0
        sta PPUADDR
 
        draw_two_digits XSCROLL

        lda #$24              ; mezera (tvar oblohy)
        sta PPUDATA           ; uložení tvaru (cifry) do pozadí
 
        draw_two_digits YSCROLL
 
        rts
.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
        bne :-
 
        ldx #$00           ; počitadlo
:
        lda nametabledata+256,X
        sta PPUDATA        ; zápis indexu dlaždice
        inx
        bne :-
 
        ldx #$00           ; počitadlo
:
        lda nametabledata+512,X
        sta PPUDATA        ; zápis indexu dlaždice
        inx
        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 #0               ; počitadlo 64 bajtů
 
:
        lda attributedata,X  ; načtení čtyř atributů
        sta PPUDATA          ; zápis indexu
        inx                  ; snížení hodnoty počitadla
        cpx #64
        bne :-               ; opakování smyčky
 
        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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$D0,$E8,$D1,$D0,$D1,$DE,$D1,$D8,$D0,$D1,$26,$29,$29,$DE,$D1,$D0,$D1,$D0,$D1,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$42,$42,$DB,$42,$DB,$42,$DB,$DB,$42,$26,$29,$29,$DB,$42,$DB,$42,$DB,$42,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$DB,$DE,$DF,$DB,$DB,$DB,$26,$29,$29,$DE,$DF,$DB,$DB,$E4,$E5,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DE,$43,$DB,$42,$DB,$DB,$DB,$26,$29,$29,$DB,$42,$DB,$DB,$E6,$E3,$26,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$DB,$DB,$DB,$DB,$42,$DB,$DB,$DB,$D4,$D9,$26,$29,$29,$DB,$DB,$D4,$D9,$D4,$D9,$E7,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$95,$95,$95,$95,$95,$95,$95,$95,$97,$98,$78,$78,$78,$95,$95,$97,$98,$97,$98,$95,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
 
    .byte $24,$1b,$18,$18,$1d,$24,$25,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
    .byte $24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$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,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24
    .byte $24,$24,$a5,$a6,$24,$24,$24,$24,$24,$24,$24,$24,$53,$54,$24,$24,$24,$a7,$a8,$24,$45,$45,$24,$24,$45,$45,$45,$45,$53,$54,$24,$24
    .byte $24,$24,$a7,$a8,$47,$47,$24,$24,$47,$47,$47,$47,$55,$56,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$24,$55,$56,$24,$24
 
; tabulka atributů
attributedata:
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111 ; žluto-hnědá paleta
    .byte %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111, %11111111
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; zelená paleta
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
    .byte %10101010, %10101010, %10101010, %10111111, %11111111, %10101010, %11111111, %10101010
    .byte %01010101, %10101010, %01010101, %11101111, %11111111, %01010101, %11111111, %01010101
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000 ; jen obloha - průhledné pixely
    .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
 
 
 
; ---------------------------------------------------------------------
; Tabulka vektorů CPU
; ---------------------------------------------------------------------
 
.segment "VECTORS"
.addr nmi
.addr reset
.addr irq
 
 
 
.segment "CHARS"
    .incbin "mario.chr"
 
 
 
; ---------------------------------------------------------------------
; Finito
; ---------------------------------------------------------------------

17. Ukázky posunutého pozadí se zobrazením offsetů

Opět se podívejme na to, jak mohou vypadat scény vykreslené dnešním třetím demonstračním příkladem:

Obrázek 11: Posun pozadí doleva.

Obrázek 12: Posun pozadí doprava.

Obrázek 13: Posun pozadí nahoru.

Obrázek 14: Posun pozadí dolů.

Obrázek 15: Vliv nastavení hodnoty zrcadlení na to, jak bude vypadat plocha odkrytá při scrollingu.

ict ve školství 24

18. Od assembleru k jazyku C

Jak je z demonstračních příkladů, které jsme si prozatím v tomto seriálu ukázali, patrné, je možné s využitím assembleru napsat i relativně složitý kód. V minulosti se ostatně v assembleru psaly celé hry. Je to umožněno mj. i možností pojmenovat si všechna důležitá paměťová místa i hodnoty, dále možností deklarovat subrutiny (podprogramy) a v neposlední řadě nesmíme zapomenout ani na význam maker, zejména maker s parametry. I přesto je však tvorba větších aplikací přímo v assembleru komplikovaná. Z tohoto důvodu si příště ukážeme práci s překladačem programovacího jazyka C, jehož varianta pro mikroprocesor MOS 6502 se jmenuje cc65. Na rozdíl do překladačů jazyka C pro moderní rychlé mikroprocesory je však nutné při použití cc65 přemýšlet o tom, jak se konkrétní jazykové konstrukce přeloží do sekvence instrukcí. V opačném případě by výsledný kód byl příliš velký a především pomalý, což není v případě herní konzole NES akceptovatelné (nelze říct „kupte si lepší CPU“).

Poznámka: ze stejného důvodu se na NESu nepoužívají interpretované jazyky typu BASIC.

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ář:

# Příklad Stručný popis Adresa
1 example01.asm zdrojový kód příkladu tvořeného kostrou aplikace pro NES https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example01.asm
2 example02.asm použití standardní konfigurace linkeru pro konzoli NES https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example02.asm
3 example03.asm symbolická jména řídicích registrů PPU https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example03.asm
4 example04.asm zjednodušený zápis lokálních smyček v assembleru https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example04.asm
5 example05.asm zvukový výstup s využitím prvního „square“ kanálu https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example05.asm
6 example06.asm použití maker bez parametrů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example06.asm
       
7 example07.asm nastavení barvové palety, zvýšení intenzity zvolené barvové složky https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example07.asm
8 example08.asm využití operátorů < a > https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example08.asm
9 example09.asm vymazání barvové palety realizované makrem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example09.asm
10 example10.asm vymazání barvové palety realizované podprogramem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example10.asm
11 example11.asm nastavení barvové palety pozadí i spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example11.asm
12 example12.asm refaktoring předchozího příkladu makrem https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example12.asm
       
13 example13.asm zobrazení spritů tvořících Maria https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example13.asm
14 example14.asm posun spritů, aby se zdůraznila jejich nezávislost https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example14.asm
15 example15.asm větší množství spritů na obrazovce rozdělených do řádků https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example15.asm
16 example16.asm větší množství spritů na obrazovce na jediném řádku https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example16.asm
17 example17.asm pohyb jednoho spritu pomocí ovladače https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example17.asm
18 example18.asm odvozeno z předchozího příkladu, symbolická jména adres https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example18.asm
19 example19.asm odvozeno z předchozího příkladu, pomocná makra pro pohyb spritu https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example19.asm
20 example20.asm pohyb spritu je založen na instrukcích INCDEC https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example20.asm
21 example21.asm přesun celého Maria (8 spritů) https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example21.asm
22 example22.asm (rychlá) změna barvové palety spritů tlačítkem A https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example22.asm
23 example23.asm realizace čítače pro snížení frekvence změn barvové palety https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example23.asm
24 example24.asm horizontální a vertikální zrcadlení spritů řízené hráčem stiskem tlačítek https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example24.asm
25 example25.asm definice pozadí a zobrazení pozadí https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example25.asm
26 example26.asm zobrazení pozadí i spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example26.asm
27 example27.asm pohyb spritu řízený ovladačem, změna atributů spritů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example27.asm
28 example28.asm definice pozadí přes téměř celou obrazovku https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example28.asm
29 example29.asm scrolling pozadí s využitím ovladače https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example29.asm
30 example30.asm zobrazení hodnot offsetů pro pozadí https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/example30.asm
       
31 link.cfg konfigurace segmentů pro linker ld65 https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/link.cfg
32 Makefile Makefile pro překlad a slinkování všech příkladů https://github.com/tisnik/8bit-fame/blob/master/NES-ca65/Makefile
Poznámka: pro slinkování a spuštění dnešních demonstračních příkladů potřebujete i soubor mario.chr. Ten je stažen automaticky po zadání make example16make example23.

20. Odkazy na Internetu

  1. The Thirty Million Line Problem
    https://www.youtube.com/wat­ch?v=kZRE7HIO3vk
  2. NesDev.org
    https://www.nesdev.org/
  3. The Sprite Attribute Byte
    https://www.patater.com/nes-asm-tutorials/day-17/
  4. How to Program an NES game in C
    https://nesdoug.com/
  5. Cycle reference chart
    https://www.nesdev.org/wi­ki/Cycle_reference_chart
  6. Getting Started Programming in C: Coding a Retro Game with C Part 2
    https://retrogamecoders.com/getting-started-with-c-cc65/
  7. NES game development in 6502 assembly – Part 1
    https://kibrit.tech/en/blog/nes-game-development-part-1
  8. NES (Nintendo Entertainment System) controller pinout
    https://pinoutguide.com/Ga­me/NES_controller_pinout.shtml
  9. NES Controller Shift Register
    https://www.allaboutcircu­its.com/uploads/articles/nes-controller-arduino.png?v=1469416980041
  10. „Game Development in Eight Bits“ by Kevin Zurawel
    https://www.youtube.com/wat­ch?v=TPbroUDHG0s&list=PLcGKfGE­EONaBjSfQaSiU9yQsjPxxDQyV8&in­dex=4
  11. Game Development for the 8-bit NES: A class by Bob Rost
    http://bobrost.com/nes/
  12. Game Development for the 8-bit NES: Lecture Notes
    http://bobrost.com/nes/lectures.php
  13. NES Graphics Explained
    https://www.youtube.com/wat­ch?v=7Co_8dC2zb8
  14. NES GAME PROGRAMMING PART 1
    https://rpgmaker.net/tuto­rials/227/?post=240020
  15. 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/
  16. Minimal NES example using ca65
    https://github.com/bbbradsmith/NES-ca65-example
  17. List of 6502-based Computers and Consoles
    https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/
  18. History of video game consoles (second generation): Wikipedia
    http://en.wikipedia.org/wi­ki/History_of_video_game_con­soles_(second_generation)
  19. 6502 – the first RISC µP
    http://ericclever.com/6500/
  20. 3 Generations of Game Machine Architecture
    http://www.atariarchives.or­g/dev/CGEXPO99.html
  21. bee – The Multi-Console Emulator
    http://www.thebeehive.ws/
  22. Nerdy Nights Mirror
    https://nerdy-nights.nes.science/
  23. The Nerdy Nights ca65 Remix
    https://github.com/ddribin/nerdy-nights
  24. NES Development Day 1: Creating a ROM
    https://www.moria.us/blog/2018/03/nes-development
  25. How to Start Making NES Games
    https://www.matthughson.com/2021/11/17/how-to-start-making-nes-games/
  26. ca65 Users Guide
    https://cc65.github.io/doc/ca65.html
  27. cc65 Users Guide
    https://cc65.github.io/doc/cc65.html
  28. ld65 Users Guide
    https://cc65.github.io/doc/ld65.html
  29. da65 Users Guide
    https://cc65.github.io/doc/da65.html
  30. Nocash NES Specs
    http://nocash.emubase.de/everynes.htm
  31. Nintendo Entertainment System
    http://cs.wikipedia.org/wiki/NES
  32. Nintendo Entertainment System Architecture
    http://nesdev.icequake.net/nes.txt
  33. NesDev
    http://nesdev.parodius.com/
  34. 2A03 technical reference
    http://nesdev.parodius.com/2A03%20techni­cal%20reference.txt
  35. NES Dev wiki: 2A03
    http://wiki.nesdev.com/w/in­dex.php/2A03
  36. Ricoh 2A03
    http://en.wikipedia.org/wi­ki/Ricoh_2A03
  37. 2A03 pinouts
    http://nesdev.parodius.com/2A03_pi­nout.txt
  38. 27c3: Reverse Engineering the MOS 6502 CPU (en)
    https://www.youtube.com/wat­ch?v=fWqBmmPQP40
  39. “Hello, world” from scratch on a 6502 — Part 1
    https://www.youtube.com/wat­ch?v=LnzuMJLZRdU
  40. A Tour of 6502 Cross-Assemblers
    https://bumbershootsoft.wor­dpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/
  41. Nintendo Entertainment System (NES)
    https://8bitworkshop.com/doc­s/platforms/nes/
  42. Question about NES vectors and PPU
    https://archive.nes.science/nesdev-forums/f10/t4154.xhtml
  43. How do mapper chips actually work?
    https://archive.nes.science/nesdev-forums/f9/t13125.xhtml
  44. INES
    https://www.nesdev.org/wiki/INES
  45. NES Basics and Our First Game
    http://thevirtualmountain­.com/nes/2017/03/08/nes-basics-and-our-first-game.html
  46. Where is the reset vector in a .nes file?
    https://archive.nes.science/nesdev-forums/f10/t17413.xhtml
  47. CPU memory map
    https://www.nesdev.org/wi­ki/CPU_memory_map
  48. How to make NES music
    http://blog.snugsound.com/2008/08/how-to-make-nes-music.html
  49. Nintendo Entertainment System Architecture
    http://nesdev.icequake.net/nes.txt
  50. MIDINES
    http://www.wayfar.net/0×f00000_o­verview.php
  51. FamiTracker
    http://famitracker.com/
  52. nerdTracker II
    http://nesdev.parodius.com/nt2/
  53. How NES Graphics work
    http://nesdev.parodius.com/nesgfx.txt
  54. NES Technical/Emulation/Development FAQ
    http://nesdev.parodius.com/NES­TechFAQ.htm
  55. Adventures with ca65
    https://atariage.com/forum­s/topic/312451-adventures-with-ca65/
  56. example ca65 startup code
    https://atariage.com/forum­s/topic/209776-example-ca65-startup-code/
  57. 6502 PRIMER: Building your own 6502 computer
    http://wilsonminesco.com/6502primer/
  58. 6502 Instruction Set
    https://www.masswerk.at/6502/6502_in­struction_set.html
  59. Chip Hall of Fame: MOS Technology 6502 Microprocessor
    https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor
  60. Single-board computer
    https://en.wikipedia.org/wiki/Single-board_computer
  61. www.6502.org
    http://www.6502.org/
  62. 6502 PRIMER: Building your own 6502 computer – clock generator
    http://wilsonminesco.com/6502pri­mer/ClkGen.html
  63. Great Microprocessors of the Past and Present (V 13.4.0)
    http://www.cpushack.com/CPU/cpu.html
  64. Jak se zrodil procesor?
    https://www.root.cz/clanky/jak-se-zrodil-procesor/
  65. Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
    https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/
  66. 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/
  67. 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/
  68. 25 Microchips That Shook the World
    https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world
  69. Comparison of instruction set architectures
    https://en.wikipedia.org/wi­ki/Comparison_of_instructi­on_set_architectures
  70. Day 1 – Beginning NES Assembly
    https://www.patater.com/nes-asm-tutorials/day-1/
  71. Day 2 – A Source Code File's Structure
    https://www.patater.com/nes-asm-tutorials/day-2/
  72. Assembly Language Misconceptions
    https://www.youtube.com/wat­ch?v=8_0tbkbSGRE
  73. How Machine Language Works
    https://www.youtube.com/wat­ch?v=HWpi9n2H3kE
  74. Super Mario Bros. (1985) Full Walkthrough NES Gameplay [Nostalgia]
    https://www.youtube.com/wat­ch?v=rLl9XBg7wSs
  75. [Longplay] Castlevania (NES) – All Secrets, No Deaths
    https://www.youtube.com/wat­ch?v=mOTUVXrAOE8
  76. Herní série Castlevania
    https://www.root.cz/clanky/historie-vyvoje-pocitacovych-her-24-cast-hry-pro-konzoli-nes/#k07

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.