Gruzińskie Noce 07, czyli Nerdy Nights po polsku
« dnia: Maj 25, 2016, 13:14:11 »
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łe
Stał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.
 

Podprogramy

W 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 kontrolera
Potrafią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 Gry
Silnik 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 Gry
Jeś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ąc
http://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 7

Podręcznik ASM 6502 dla Atari [PL]:
http://atarionline.pl/biblioteka/materialy_ksiazkowe/Asembler%206502%20(v2).pdf