Jakub "Ilmenit" Dębski napisał:
Piszemy grę w C, część 2
W poprzedniej części kursu nauczyliśmy się podstaw używania CC65.
Wspomniałem o niezgodności CC65 ze standardem języka oraz
standardem wytyczonym przez popularne kompilatory. Główne
niezgodności są trzy.
Najważniejsza z nich dotyczy typów ze znakiem i bez znaku (signed,
unsigned). Przy pominięciu w deklaracji zmiennej znakowości, CC65
ustala, że jest on unsigned, w przeciwieństwie do większości
kompilatorów C. Tak więc "char a;" znaczy "unsigned char a;", a nie
"signed char a;". Dla określenia zmiennej ze znakiem musimy użyć
"signed". Jest to główna rzecz, na którą należy zwrócić uwagę
kompilując w CC65 kody tworzone dla innych kompilatorów. Druga
różnica dotyczy ścieżek w poleceniu preprocesora #include "". W
CC65 wszystkie takie ścieżki dołączane są względem nie aktualnego
pliku, ale katalogu, w którym został uruchomiony kompilator.
Trzecia niezgodność dotyczy typów zmiennoprzecinkowych – w
kompilatorze CC65 ich brak, choć można znaleźć w necie kilka ich
implementacji (nie potrzebowałem, więc nie testowałem).
Obsługa duszków
Duszki (zwane Player-Missile Graphics, PMG) w małym Atari są bardzo
ograniczone w stosunku do innych platform. W prosty sposób można
przesuwać je tylko w poziomie, zaś do przesunięcia w pionie
konieczne jest kopiowanie pamięci, w której się znajdują. W naszej
grze duszki wykorzystamy do płynnego przesuwania paletki oraz
piłki, ponieważ w użytym wcześniej trybie znakowym przesuwają się
one skokowo. PMG dokładniej opisane jest w DeRe Atari (po angielsku
w
AtariArchive i
po polsku w
Bibliotece Atarowca serwisu AtariOnline.pl).
Pamięć duszków jest liniowa i zajmuje 1024 lub 2048 bajtów zależnie
od tego, czy używamy duszków w podwójnej czy pojedynczej
rozdzielczości. UWAGA: pamięć ta MUSI być wyrównana do 1024, co
znaczy, że musi zaczynać się od adresu będącego wielokrotnością
1024 (400h). Język C nie umożliwia deklarowania tablic pod
określonymi adresami, ale w CC65 można to uzyskać na kilka
sposobów. Można zadeklarować większą tablicę statyczną lub
przydzielić ją dynamicznie, po czym użyć wyrównanego adresu, zaś
niewykorzystany obszar pamięci dodać do sterty za pomocą funkcji
_heapadd() (poniższy kod jest autorstwa Shawna Jeffersona):
void __fastcall__ *aligned_malloc(size_t size, unsigned
int bound)
{
char *d, *b;
unsigned int lo_free, hi_free, bnd;
bnd = bound - 1;
//*** Allocate an aligned block of memory ***//
d = (char *) malloc(size + bnd); // allocate mem
if (!d) return(NULL); // mem not avail
b = (char *) ((((unsigned int) d) + bnd) & ~bnd); // aligned
pointer
lo_free = b - d; // memory not used
hi_free = bnd - lo_free;
_heapadd(d, lo_free); // free low
_heapadd(b + bound, hi_free); // free high
return(b);
}
Wadą jest to, że przy użyciu dynamicznej pamięci kompilator nie
przeprowadza dobrej optymalizacji, ponieważ adres nie jest znany w
czasie kompilacji. Drugą wadą jest fragmentacja pamięci - obszary
zwolnione można ponownie zaalokować, ale nie są one w ciągłym
miejscu pamięci, więc nie można zalokować dużych obszarów. Ogólnie
odradzam używanie malloc() i free() w CC65 i polecam wykorzystać w
ich miejsce tablice globalne – Atari ma zbyt mało pamięci na
dynamiczne nią zarządzanie.
Najwygodniej byłoby ustalić stałe miejsce w pamięci, na przykład
adres 4000h i wykorzystać kolejne 1024 lub 2048 bajtów. Musimy
znaleźć (lub zrobić) miejsce w pamięci, które nie jest
wykorzystywane przez nasz program. Takim miejscem jest na przykład
pamięć pod ROM, do której możemy się dostać za pomocą rejestru
PORTB ($D301).
Możemy również zmodyfikować plik konfiguracyjny, który
modyfikowaliśmy w poprzedniej części kursu przy zmianie trybu
graficznego. Plik ten zawiera informacje dla kompilatora, co i
gdzie ma zostać umieszczone w pamięci. Potrzebujemy zatem zrobić
"dziurę" w pamięci na nasze dane. Możemy albo zmodyfikować
istniejące segmenty, albo dodać kolejny, ale najprostszym sposobem
jest przeniesienie w górę miejsca, od którego zaczyna się
program.
Domyślnym „dolnym” adresem programu jest STARTADDRESS: default =
$2E00; Wartość ta została wybrana tak, aby możliwe było załadowanie
różnych sterowników DOS-a (
źródło). Aby mieć więcej
pamięci dla naszego programu możemy bezpiecznie obniżyć ją do
adresu $2000. My jednak potrzebujemy zrobić coś przeciwnego, czyli
zabrać programowi pamięć. W tym celu ustalamy adres początku
programu na $2800, dzięki czemu mamy 2KB ($800 bajtów) pamięci od
adresu $2000 do $2800.
Wiemy już gdzie będzie znajdować się pamięć duszków, trzeba je
teraz zainicjować. Ponieważ deklarowanie za każdym razem adresów
specyficznych dla Atari jest irytujące, wykorzystajmy plik
nagłówkowy
przygotowany przez Jakuba Husaka, który rozszerzyłem o kilka
przydatnych definicji związanych z obsługą duszków.
Za pomocą rejestru SDMCTL ustalamy tryb pracy ANTICa. Pamięć
duszków wskazujemy za pomocą rejestru PMBASE. Do włączenia PMG
służy rejestr GRACTL. Priorytet wyświetlania duszków określa
GPRIOR. Wszystko oczywiście opisane w mapie pamięci. Kolory
obiektów ustalamy za pomocą PCOLRx. Jeżeli nie wiesz, jaka wartość
odpowiada kolorowi, możesz użyć jednego z załączonych programów
(rgb.zip).
Każdy obiekt PMG zajmuje 128 bajtów pamięci w rozdzielczości
dwuliniowej i 256 bajtów w rozdzielczości jednoliniowej. Pamięć
poszczególnych obiektów dostępna jest pod adresami:
Rozdzielczość dwuliniowa |
Początek |
Pociski |
PMBASE+0x180 |
Gracz 0 |
PMBASE+0x200 |
Gracz 1 |
PMBASE+0x280 |
Gracz 2 |
PMBASE+0x300 |
Gracz 3 |
PMBASE+0x380 |
Rozdzielczość jednoliniowa |
Początek |
Pociski |
PMBASE+0x300 |
Gracz 0 |
PMBASE+0x400 |
Gracz 1 |
PMBASE+0x500 |
Gracz 2 |
PMBASE+0x600 |
Gracz 3 |
PMBASE+0x700 |
Pozycja pozioma duszków ustawiana jest za pomocą rejestrów HPOSPx,
zaś pionowa według pozycji w pamięci obiektu. Widoczność obiektów
opisana jest
tutaj.
Zatem nasza procedura inicjalizująca grafikę PMG wygląda
następująco:
#define pmg_memory 0x2000
#define pmg_memory_ptr ((unsigned char *) 0x2000)
char paddle_gfx[]={ 0x3C, 0x7E, 0xFF, 0xFF };
char ball_gfx[]={ 0, 0x2, 0x2, 0x5, 0x5, 0x2, 0x2, 0 };
void place_ball()
{
// +0x500 for the player 1,
memcpy ( pmg_memory_ptr+0x500+ball_y ,
ball_gfx,sizeof(ball_gfx));
POKE(HPOSP1,ball_x); // horizontal position
}
void init_pmg()
{
POKE(SDMCTL,DMACTL_ENABLE_PLAYER_DMA | DMACTL_ENABLE_MISSLE_DMA |
DMACTL_NORMAL_PLAYFIELD | DMACTL_SINGLE_LINE_RESOLUTION |
DMACTL_DMA_FETCH_INSTRUCTION);
// PMBASE points to our PMG memory page (page has 256 bytes)
POKE(PMBASE,pmg_memory/256);
// Zero pmg memory
memset(pmg_memory_ptr,0,2048);
// turn on PMG for players and missiles
POKE(GRACTL,PMG_PLAYERS | PMG_MISSILES);
// set PMG priority
POKE(GPRIOR,1);
// init paddle
POKE(PCOLR0,0xFF);
POKE(SIZEP0,SIZEP_DOUBLE);
POKE(HPOSP0,paddle_pos); // paddle position
// +0x400 for the player 0, +192 for vertical position on the
screen
memcpy(pmg_memory_ptr+0x400+192 , paddle_gfx ,
sizeof(paddle_gfx));
// init ball
POKE(PCOLR1,0x5F);
POKE(SIZEP1,SIZEP_SINGLE);
place_ball();
}
Aby przykład był bardziej przejrzysty, w przykładzie
PiszemyGre6.zip została uproszczona obsługa piłki:
Nie jest mi znany działający pod Windows edytor grafiki duszków,
choć są takie programy działające na małym Atari (nie używałem,
więc nie polecę konkretnego).
Zmiana zestawu znaków
Do przygotowania własnego zestawu znaków można użyć programu
"Atari
Font Maker" lub dla bardziej doświadczonych mającego znacznie
większe możliwości
"Graph2Font". Można również korzystać
z bogatej kolekcji fontów
dostępnych w serwisie AtariOnline.pl. W tym przykładzie
wykorzystamy dostępny w powyższej kolekcji font XFTOOL.FNT, który
lekko zmodyfikujemy dodając 4 rodzaje cegiełek do niszczenia,
począwszy od znaku 64, dwa znaki na cegiełkę:
Do ustawienia własnego zestawu znaków wykorzystujemy rejestr CHBAS.
Wskazuje on na stronę pamięci, która zawiera zestaw znaków (1024
bajty = 128 znaków * 8 bajtów na znak). UWAGA: podobnie jak przy
duszkach początek pamięci zestawu znaków musi być wyrównany do 1024
($400). W przypadku duszków mogliśmy w prosty sposób korzystać z
pamięci pod adresem $2000, ponieważ były tam niezainicjowane dane.
Sytuacja nie jest tak prosta, gdy chcemy wykorzystać wyrównaną
pamięć z danymi. W CC65 powinien do tego służyć
plik konfiguracji linkera,
ale w przypadku platformy Atari nie działa on wystarczająco
elastycznie (
źródło). W małych
projektach można w prosty sposób zabrać wyrównany fragment pamięci
RAM dodając własny segment w pliku konfiguracyjnym:
SEGMENTS {
...
FONT: load = RAM, type = rw, align=$400, define=yes;
...
}
a następnie w pliku asemblerowym dołączyć (.incbin) plik binarny do
tego segmentu:
.export _font_base
.segment "FONT"
_font_base:
.incbin "XFTOOL.FNT"
Jeżeli nie chcemy używać do tego asemblera, musimy użyć programu
"BIN2C" i zmienić
plik z fontem na tablicę języka C, a następnie dołączyć taką
tablicę w kodzie za pomocą:
#pragma dataseg (push,"FONT")
#include "font.h"
#pragma dataseg (pop)
Teraz w celu zmiany zestawu znaków pozostanie jedynie ustawić
CHBAS:
POKE(CHBAS,((unsigned int)
&font_base)/256);
Plik
PiszemyGre7.zip
Niestety, ten sposób dołączania plików pod wyrównane adresy ma tę
wadę, że zabiera dużo cennej pamięci, co ma znaczenie przy
większych projektach. Wystarczy spojrzeć do środka powstałego pliku
xex, żeby zobaczyć dużą dziurę w jego środku odpowiadającą za
umieszczenie danych w wyrównanym miejscu. Sposób jest jednak
prosty, więc w małych programach można go stosować. Lepszy sposób
umieszczenia zewnętrznych danych pod wybranymi adresami pokażę przy
odtwarzaniu muzyki i dźwięków.
Wszystkie pliki do powyższego odcinka kursu znajdują się
tutaj oraz
w
tym wątku na forum.