Tej nocy: Większość to organizacja kodu i struktura Twojej gry. Zmienne Jak już mówiliśmy podczas Gruzińskiej nocy #1, zmienne są przechowywane w pamięci RAM i można je sobie zmieniać w dowolnym momencie. Dane Sprite'ów są w całości zmiennymi. Będziesz potrzebował kolejnych zmiennych do śledzenia wielu rzeczy, np. aktualnie zdobytych punktów itp. Żeby coś takiego zrobić, należy najpierw powiedzieć NESASM'owi w jakim miejscu w pamięci RAM ma przechowywać te zmienne. Robi się to dyrektywami .rsset oraz .rs.
.rsset służy do określenia początkowego adresu dla zmiennych. Potem .rs rezerwuje miejsce dla nich. Zazwyczaj rezerwowany jest 1B(bajt) ale można to zmieniać. Za każdym kolejnym użyciem .rs adres rezerwacji rośnie kolejno, więc nie trzeba ustawiać kolejnego początku poprzez .rsset.
.rsset $0000 ;zacznij obstawiać zmienne od miejsca w pamięci 0
score1 .rs 1 ;ustaw punkty dla gracza 1 w miejscu $0000
score2 .rs 1 ;ustaw punkty dla gracza 2 w miejscu $0001
buttons1 .rs 1 ;ustaw dane pada gracza 1 w miejscu $0002
buttons2 .rs 1 ;ustaw dane pada gracza 2 w miejscu $0003
Jak już ustawisz adres dla konkretnej zmiennej, nie musisz znać jej adresu w pamięci. Możesz po prostu odnosić się do niego używając nazwy zmiennej. Możesz oczywiście dopisać kolejne zmienne do tych powyżej a assembler automatycznie nada im kolejne adresy w pamięci.
StałeStałe to liczby, których nie zmieniasz. Ich zadaniem jest zwiększenie przejrzystości kodu. Przykładową stałą w Pong'u są zewnętrzne ściany. Musisz porównać pozycję piłeczki z miejscem ściany, żeby ją odbić. A że ściany się nie ruszają, więc całkiem dobrze by było opisać je jako stałe. Łatwiej jest porównać coś z LEWĄŚCIANĄ niż z $F6
Żeby zadeklarować stałą używamy znaku równości:
RIGHTWALL = $ 02 ; zrób coś, jeśli piłeczka zetknie się z którąś z tych ścian
TOPWALL = $ 20
BOTTOMWALL = $D8
LEFTWALL = $F6
Assembler znajdzie/podmieni tekst na odpowiednie wartości podczas budowania twojego kodu.
PodprogramyW miarę zwiększania się twojego kodu dużo łatwiej będzie ci się poruszać po podprogramach (coś a la akapity z nazwą) niż używać kodu ciągnącego się z góry do dołu. Podprogramów możemy używać ile i jak nam się podoba, mogą być wywołane w dowolnym momencie. A tak to wygląda:
RESET:
SEI ; wyłącz IRQs
CLD ; wyłącz tryb dziesiętny
vblankwait1: ; pierwsze czekanie na vblank, by upewnić się, że PPU jest gotów
BIT $2002
BPL vblankwait1
clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
vblankwait2: ; drugie czekanie na vblank, by upewnić się, że PPU jest gotów
BIT $2002
BPL vblankwait2
Patrzaj pan, vblankwait jest użyty dwa razy, więc możnaby go określić przez podprogram. Najpierw kod vblankwait'a wyciągamy poza ciąg kodu:
vblankwait: ; wait for vblank
BIT $2002
BPL vblankwait
RESET:
SEI ; wyłącz IRQs
CLD ; wyłącz tryb dziesiętny
clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
A teraz jeszcze trzeba wywołać to, co wyciągneliśmy. Robimy to instrukcją JSR (Jump to SubRoutine).
RESET:
SEI ; wyłącz IRQs
CLD ; wyłącz tryb dziesiętny
JSR vblankwait ;; skocz do pierwszego czekania na vblanka
clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
JSR vblankwait ;; skocz do drugiego czekania na vblanka
Po zakończeniu się podprogramu, powinien wrócić do miejsca w którym został wywołany. A wraca przez instrukcję RTS(ReTurn from Subroutine).
vblankwait: ; czekaj na vblank <--------
BIT $2002
BPL vblankwait
----- RTS
/
| RESET:
| SEI ; disable IRQs
| CLD ; disable decimal mode
|
| JSR vblankwait ;;skocz do vblankwait #1 --/
|
\--> clrmem:
LDA #$FE
STA $0200, x
INX
BNE clrmem
JSR vblankwait ;; skacze do vblankwait jeszcze raz, potem wraca tu
Lepszy odczyt kontroleraPotrafiąc już używać podprogramów możesz usprawnić sprawdzanie stanu kontrolera (pada). Poprzednio kontroler był odczytywany podczas przetwarzania. Podczas różnych stanów gry trzeba by wielokrotnie używać tego samego kodu odczytu stanu kontrolera. Upraszczamy to przez jeden podprogram. Zapisuje on dane konkretnego przycisku pod zmienną. A zmienna ta może być odczytywana później już bez potrzeby odczytywania całego kontrolera.
ReadController:
LDA #$01
STA $4016
LDA #$00
STA $4016
LDX #$08
ReadControllerLoop:
LDA $4016
LSR A ; bit0 -> Przenieś
ROL buttons ; bit0 <- Przenieś
DEX
BNE ReadControllerLoop
RTS
Ten kod używa dwóch nowych instrukcji. LSR (Logical Shift Right) - bierze każdy jeden bit z A i przesuwa go o jedną pozycję w prawo. Bit7 (pierwszy z lewej) jest uzupełniany zerem, a bit0 jest przesunięty do Carry Flag (czyli Flagi Przenoszenia)
bit number 7 6 5 4 3 2 1 0 carry
originalne dane 1 0 0 1 1 0 1 1 0
\ \ \ \ \ \ \ \
przesunięte dane 0 1 0 0 1 1 0 1 1
Każda pozycja bitu jest potęgą dwójki, więc wykonanie LSR to to samo co dzielenie przez 2
I nowa instrukcja#2 to ROL (ROtate Left) będące odwrotnością LSR. Każdy bit przesuwany jest w lewo o jedno miejsce. Carry Flag przybiera wartość 0. Wykonanie ROL to to samoco mnożenie przez 2.
Instrukcje te są sprytnie używane razem do odczytywania kontrolera:
-Kiedy przycisk jest wczytywany, jego dane są w bicie 0.
-LSR wrzuca te dane do Carry
-ROL z powrotem przesuwa dane przycisku i ustawia Carry z powrotem na 0
Accumulator buttons data
bit: 7 6 5 4 3 2 1 0 Carry 7 6 5 4 3 2 1 0 Carry
read button A 0 0 0 0 0 0 0 A 0 0 0 0 0 0 0 0 0 0
LSR A 0 0 0 0 0 0 0 0 A 0 0 0 0 0 0 0 0 A
ROL buttons 0 0 0 0 0 0 0 0 A 0 0 0 0 0 0 0 A 0
read button B 0 0 0 0 0 0 0 B 0 0 0 0 0 0 0 0 A 0
LSR A 0 0 0 0 0 0 0 0 B 0 0 0 0 0 0 0 A B
ROL buttons 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0
read button SEL 0 0 0 0 0 0 0 SEL 0 0 0 0 0 0 0 0 A 0
LSR A 0 0 0 0 0 0 0 0 SEL 0 0 0 0 0 0 0 A SEL
ROL buttons 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B SEL 0
read button STA 0 0 0 0 0 0 0 STA 0 0 0 0 0 0 0 0 A 0
LSR A 0 0 0 0 0 0 0 0 STA 0 0 0 0 0 0 0 A STA
ROL buttons 0 0 0 0 0 0 0 0 0 0 0 0 0 A B SEL STA 0
I dalej pętla wykonuje się aż do ośmiu razy, żwby wczytać wszystkie przyciski z kontrolera. Na koniec w każdym z bitów jest status jednego z przycisków:
bit: 7 6 5 4 3 2 1 0
button: A B select start up down left right
Układ GrySilnik Ponga będzie używał zwykłego, prostego schematu gier na NES'a. Najpierw wykonywana jest cała inicjalizacja. Czyli czyszczenie RAMu, ustawianie PPU, ładowanie grafik ekranu tytułowego. Potem wpada w nieskończoną pętlę czekającą na NMI. Kiedy wskakuje NMI, PPU jest gotowe do aktualizacji wszyskich grafik. Jest na to dość mało czasu, więc jako pierwszy ładowane jest kod sprite'ów DMA. Kiedy wszystkie grafiki mamy za sobą, zaczyna się ten właściwy silnik gry. Wczytywane są kontrolery, potem przetwarzanie gry. Pozycje sprite'ów są aktualizowane do pamięci RAM ale nie są aktualizowane aż do następnego NMI. Kiedy silnik gry zakończy swoje działania wracamy do nieskończonej pętli.
Kod inicjalizacji -> Nieskończona Pętla -> NMI -> Auktualizacjia Grafiki -> Wczytanie Przycisków -> Silnik Gry --\
^ |
\--------------------------------------------------------------------------------------------/
Stan GryJeśli jesteśmy w "stanie" ekranu tytułowego nie ma potrzeby przetwarzania kodu odpowiadającego na ruch piłeczki. Dla Ponga będą trzy stany gry. Na ilustracji jest przedstawione co każdy ze stanów robi i co powinno być ustawione, żeby przejść do kolejnego stanu.
->Stan Tytułowy /--> Stan Grania /--> Stan Game Over
/ czekaj na przycisk start --/ rusz kulę / czekaj na przycisk start -\
| rusz paletkę | \
| sprawdź kolizje / |
| spr. czy punkcty = 15 -/ |
\ /
\-------------------------------------------------------------------------------------------/
Następnym krokiem jest dodanie szczegółów to tego, co już sobie rozrysowaliśmy. Dziel i rządź, te sprawy. Część z elementów gry takich jak punkty dla drugiego gracza będą dodane później. Bez punktów jednak nie będziemy mogli przejść do stanu Game Over, ale na wszystko przyjdzie czas!
Stan Tytułowy:
jeśli wciśnięty start
wyłącz ekran
ładuj ekran gry
ustaw pozycję piłeczki/paletek
idź do Stanu Grania
włącz ekran
Stan Grania:
ruszaj piłeczką
jeśli piłeczka rusza się w prawo
dodaj prędkość x do pozycji x piłeczki
jeśli wpółrzędna x piłeczki > x ściany prawej
odbij, piłeczka rusza się teraz w lewo
jeśli piłeczka rusza się w lewo
odejmij prędkość x do pozycji x piłeczki
jeśli wpółrzędna x piłeczki < x ściany lewej
odbij, piłeczka rusza się teraz w prawo
jeśli piłeczka rusza się w górę
odejmij prędkość y od pozycji y piłeczki
jeśli wpółrzędna y piłeczki < y ściany górnej
odbij, piłeczka rusza się teraz w dół
jeśli piłeczka rusza się w dół
dodaj prędkość y do pozycji y piłeczki
jeśli wpółrzędna y piłeczki > y ściany dolnej
odbij, piłeczka rusza się teraz w górę
jeśli wciśnięta strzałka w górę
jeśli góra paletki > górnej paletki
rusz górę i dół paletki w górę
jeśli wciśnięta strzałka w dół
jeśli dół paletki < dolna ściana
rusz górę i dół paletki w dół
jeśli x kulki < x paletki
jeśli y kulki < y góry paletki
jeśli y kulki < y dołu paletki
odbij, kulka porusza się w lewo
Stan Game Over:
jeśli wciśnięty start
wyłącz ekran
wczytaj ekran tytułowy
idź do Stanu Tytułowego
włącz ekran
Podsumowująchttp://www.nespowerpak.com/nesasm/pong1.zip
Pobierz i rozpakuj pong1.zip. Kod Stanu Grania i poruszania piłeczką znajduje się pong1.asm. Upewnij się, że pliki mario.chr oraz pong1.bat są w tym samym folderze NESASM3, potem dwuklik w pong1.bat. Włączy się NESASM3 i powinien utworzyć pong1.nes. Otwórz go w FCEUXD SP by zobaczyć poruszaną piłeczkę!
Pozostałe fragmenty kodu nie są dokończone. Spróbuje je napisać samodzielnie. Główne braki to poruszanie paletki i kolizje piłeczki z paletkami. Możesz też dodać Stan Intro, ekran tytułowy itp. przy pomocy porad co do tła z poprzedniej nocy.
Literatura uzupełniająca:Oryginał [ENG]:
Nerdy Nights week 7Podręcznik ASM 6502 dla Atari [PL]:http://atarionline.pl/biblioteka/materialy_ksiazkowe/Asembler%206502%20(v2).pdf