Artur Poznański
PROGRAMOWANIE
GIER
Z DIRECTX
DLA CHĘTNYCH
Kraków 2001
1. WPROWADZENIE
Jak wynika z tytułu zajmę się programowaniem gier z użyciem biblioteki Microsoftu o nazwie DirectX. Temat jest dość poważny, będzie więc o stopień trudności wyżej niż w poprzednim tekście. Tradycyjnie zacznę od wymagań stawianych czytelnikom:
znajomość C/C++
ogólna znajomość Win32 API
kompilator MS Visual C++ (5.0 lub nowszy)
biblioteka MS DirectX SDK (7.0 lub nowsza)
podstawowa znajomość języka angielskiego
chęć, cierpliwość, dużo czasu, zainteresowanie tematem itp...
Pokrótce omówię te punkty.
Chcąc programować poważne gry, trzeba znać ten język. C++ stał się praktycznie standardem przemysłowym.
Większość gier chodzi pod Windows (95/98/Me), więc trzeba orientować się jak działają aplikacje pod tym systemem. Polecam zapoznanie się z moim tekstem pt: „Programowanie w Windows 95 dla chętnych”.
Obsługa kompilatora to kolejny wymóg. Podstawowe wskazówki można znaleźć w tekście podanym powyżej.
Najnowszą wersję biblioteki DirectX można ściągnąć ze strony WWW Microsoftu. Wersja 8.0 znajduje się na styczniowym kompakcie miesięcznika PC Chip (1/2001).
Większość komentarzy, nazwy zmiennych oraz funkcji w plikach są po angielsku
(własne - po polsku)
Jeszcze na koniec moja konfiguracja:
Windows 98
Visual C++ 5.0
DirectX 7.0 SDK
2. INSTALACJA DIRECTX
Po zainstalowaniu DirectX SDK w każdym projekcie VC++ trzeba dodać:
ścieżki do katalogów (Tools ->Options -> Directories i np. C:\MSSDK\LIB oraz C:\MSSDK\INCLUDE)
odpowiednie biblioteki (Project ->Settings -> Link, w okno Object/Library modules wpisać ddraw.lib, dxguid.lib).
3. WYPISANIE TEKSTU NA EKRANIE
Nasz pierwszy przykład wypisze tradycyjne „Witaj mistrzu!” na środku ekranu. Wpierw jednak ustawimy rozdzielczość 640x480 i głębię kolorów na 16. Co to znaczy 640x480 ? Rozdzielczość ekranu jest to ilość pikseli w poziomie ekranu na ilość pikseli w pionie. Inne typowe rozdzielczości to: 320 x 200, 800 x 600, 1280 x 1024 itp.
Głębia kolorów to ilość bitów użytych na jeden kolor piksela. Może być:
4 (16 kolorów)
8 (256 kolorów)
16 (ok. 65 tys. kolorów - tzw. High Color)
24 (ok. 16 mln. kolorów - tzw. True Color)
Rozpakuj plik przyk1.zip dołączony do tekstu. Znajdziesz tam 4 pliki
GameMain.cpp - wyświetlenie napisu, mazanie tła
WinBase.cpp - tworzenie okna i obsługa komunikatów Windows
InitTerm.cpp - tworzenie i usuwanie powierzchni DirectDraw
Globals.h - zmienne globalne
Po utworzeniu projektu, dodaniu tych plików, kompilacji i uruchomieniu pojawi się nasz znany napis. Koniec następuje po naciśnięciu klawisza ESCAPE.
4. KRÓTKI WSTĘP DO DIRECTX
Jak już wspomniałem, DirectX stanowi zbiór bibliotek firmy Microsoft. Pozwala na szybszy dostęp do urządzeń takich jak karta graficzna, dźwiękowa, a także niezależność od sprzętu. W skład wchodzą takie elementy jak (wersja 7.0):
DirectDraw - rysowanie 2D
Direct3D - rysowanie 3D
DirectInput - obsługa myszy, klawiatury, joysticka
DirectSound - obsługa dźwięku
DirectMusic - obsługa muzyki
DirectPlay - obsługa komputerów w sieci
DirectSetup - instalacja DirectX
5. DIRECTDRAW I POWIERZCHNIE
Omówię jedynie główną ideę tworzenia animacji przy użyciu DirectDraw. Tworzymy dwa obszary pamięci na obraz ekranu. Są to tzw. powierzchnie. Ich nazwy to:
powierzchnia podstawowa - to co zwykle widzimy na ekranie
tylny bufor - tutaj rysujemy
Cała sztuka płynnej animacji polega na szybkiej zamianie tych dwóch powierzchni za pomocą funkcji Flip. Zajrzyj do funkcji Game_Main, a zobaczysz coś takiego:
(...)
hRet = G.lpDDSBack->GetDC(&hDC); // pobieramy kontekst urządzenia
if (FAILED(hRet)) return; // dla tylnego bufora
SetBkColor(hDC, RGB(0,0,0)); // ustawiamy kolor tła dla tekstu
SetTextColor(hDC, RGB(255, 255, 255)); // ustawiamy kolor tekstu
TextOut(hDC, 270, 200, "Witaj mistrzu!", 14); // wyświetlamy tekst
G.lpDDSBack->ReleaseDC(hDC); // zwalniamy kontekst urządzenia
// dla tylnego bufora
while (TRUE)
{
hRet = G.lpDDSPrimary->Flip(NULL, 0); // tu zamieniamy tylny
if (hRet == DD_OK) break; // bufor z powierzchnią
if (hRet == DDERR_SURFACELOST) // podstawową czyli go
{ // wyświetlamy na ekranie
(...)
ĆWICZENIA-1
Warto teraz przejść do własnych eksperymentów z kodem. Oto dwa zadania jakie wymyśliłem. Dla wprawy postaraj się je wykonać:
Zmień wyświetlany tekst (np. „ESC - wyjście”), oraz kolor (np. na żółty), na koniec ustaw napis w lewym górnym rogu ekranu
Przestaw rozdzielczość na 800x600 i 8-bitowy kolor.
6. ODMIERZANIE CZASU
Ważną rzeczą jest to, aby gra chodziła z tą samą prędkością na szybkich jak i na wolniejszych komputerach. Jak można odmierzać czas, pokażę na następnym przykładzie. Zmodyfikujmy nieco kod, aby nasz słynny napis co sekundę zmieniał swój kolor. Nowy kod jest w przyk2.zip. Poniżej znajduje się interesujący nas fragment pliku GameMain.cpp.
(...)
int r,g,b; // zmienne na losowe składowe koloru
bool bStart = false; // kiedy rusza timer
DWORD dwTimer; // tu jest nasza zmienna
void Game_Main()
{
HRESULT hRet;
HDC hDC;
if (!bStart)
{
dwTimer = GetTickCount();// Pierwsze pobranie czasu
bStart = true;
r = g = b = 255; // Początkowy kolor (biały)
}
if (G.bQuitting) return;
EraseBackground();
if (GetTickCount()>=dwTimer+1000) // 1000 ms = 1 sekunda
{
r = rand() % 255; // losowanie składowej czerwonej
g = rand() % 255; // zielonej
b = rand() % 255; // i niebieskiej
dwTimer = GetTickCount(); // nowe pobranie czasu
}
(...)
Funkcja GetTickCount podaje nam w milisekundach czas od momentu uruchomienia Windows. Wpierw pobieramy dzięki niej czas, a potem sprawdzamy aktualny z poprzednim powiększonym o 1000 milisekund, czyli jedną sekundę. Następnie funkcją rand losujemy sobie liczby z zakresu 0 do 255 będące składowymi koloru. Na koniec ponownie pobieramy aktualny czas po czym następuje rysowanie napisu.
ĆWICZENIA-2
Poniżej przedstawiam przykładowe zadania do wykonania.
Spraw by napis pojawiał się co pół sekundy, w różnych miejscach ekranu.
Wypisz na ekranie licznik i zmniejszaj jego wartość co sek. z 10 do 0, po czym niech
program zakończy działanie.(*) (* - trudniejsze)
7. OBLICZANIE FPS
Czeka na nas kolejne zadanie. Spróbujmy sprawdzić jak szybko odświeżany jest obraz, czyli ile klatek na sekundę jest aktualnie wyświetlanych. Dodaj trzy zmienne typu int: sekundy , klatki i fps. Następnie przerób nieco poprzedni kod na poniższy (przyk3.zip):
(...)
if (!bStart)
{
dwTimer = GetTickCount();
bStart = true;
sekundy = klatki = 0; // zerujemy początkowe wartości
}
if (G.bQuitting) return;
EraseBackground();
if (GetTickCount()>=dwTimer+1000) // 1000 ms. = 1 sekunda
{
if (sekundy < 10) // liczymy do 10
sekundy++;
else
{
sekundy = 0;
klatki = 0;
}
if (sekundy!=0 && klatki!=0)
fps = klatki/sekundy; // fps = ang. frames per second
dwTimer = GetTickCount();
}
klatki++;
(...)
Należy również zmienić sposób wyświetlania. szText jest 80-elemetnową tablicą znaków.
(...)
wsprintf(szText, "FPS: %d", fps);
TextOut(hDC, 10, 10, szText, strlen(szText));
(...)
Im więcej klatek na sekundę (FPS) tym lepiej, chociaż płynna animacja jest już przy ok. 30 FPS. Zmień rozdzielczość na wyższą, a przekonasz się, że FPS nieco zmaleje. Włącz w tle jakiś program (np. WinAmp) i zobacz jak zmieni się ta wartość.
ĆWICZENIA-3
Dodaj wyświetlanie sekund oraz ilości klatek.
Zwiększ dokładność wartości FPS do dwóch miejsc po przecinku.
8. PROSTA ANIMACJA
Tym razem zrobimy prostą animację polegającą na tym, że po ekranie będzie poruszał się biały kwadrat (16x16 pikseli). Będzie on odbijał się od ścian ekranu jak piłka. (przyk4.zip):
(...)
if (!bStart) // początkowe wartości
{
pilka.x = SCREEN_WIDTH/2-SZEROKOSC_PILKI/2;
pilka.y = SCREEN_HEIGHT/2-WYSOKOSC_PILKI/2;
lot_pilki = DOL_PRAWO;
bStart = true; // ruszyliśmy
}
if (G.bQuitting) return;
EraseBackground();
porusz_pilka();
hRet = G.lpDDSBack->GetDC(&hDC); // pobieramy kontekst urządzenia
if (FAILED(hRet)) return; // dla tylnego bufora
Rectangle(hDC, // rysujemy prostokąt
pilka.x, // lewy górny róg
pilka.y,
pilka.x+SZEROKOSC_PILKI, // prawy dolny róg
pilka.y+WYSOKOSC_PILKI);
G.lpDDSBack->ReleaseDC(hDC); // zwalniamy kontekst urządzenia
// dla tylnego bufora
(...)
Stworzyliśmy funkcję porusz_pilka, która wywołuje jedną z czterech funkcji w zależności od wartości zmiennej lot_pilki. W pliku globals.h dodaliśmy trzy nowe stałe:
(...)
#define SZEROKOSC_PILKI 16
#define WYSOKOSC_PILKI 16
#define SPEED 2
(...)
Stała SPEED określa nam o ile pikseli ma się przesunąć kwadrat co klatkę.
ĆWICZENIA-4
Zmień wielkość piłki oraz zwiększ jej prędkość.
Dodaj jeszcze dwie piłki (*)
9. WYŚWIETLANIE BITMAP
Biały kwadrat zastąpimy niebieską kulką. Jej wygląd będzie znajdował się w pliku Resource.bmp. Rozpakuj plik przyk5.zip. Są tam trzy nowe pliki:
Utils.h - nagłówek dla Utils.cpp
Utils.cpp - funkcje do obsługi bitmap
Resource.bmp - bitmapa z piłką
W dwóch poprzednich zaszły zmiany
Globals.h - dodano jedną powierzchnię
InitTerm.cpp - w funkcjach jest obsługa tej nowej powierzchni i załadowanie
do niej pliku Resource.bmp
Główna zmiana w funkcji Game_Main wygląda tak:
(...)
EraseBackground();
if (G.bQuitting) return;
porusz_pilka();
// rysujemy piłkę
G.lpDDSBack->BltFast(pilka.x,pilka.y,G.lpDDSRes,NULL,
DDBLTFAST_NOCOLORKEY | DDBLTFAST_WAIT);
(...)
Pierwsze dwa parametry funkcji BltFast określają, gdzie funkcja ma rysować (podajemy współrzędne piłki). Kolejne dwa określają, co funkcja ma rysować. Nasza bitmapa jest załadowana na trzeciej powierzchni i jej nazwę podaliśmy jako trzeci parametr. Czwartym jest rozmiar ładowanej powierzchni, ale my chcemy załadować całość więc daliśmy NULL. Podsumowując można stwierdzić, że nasz obrazek jest ładowany w trzech etapach.
z pliku do powierzchni zasobów (tej trzeciej)
z powierzchni zasobów do tylnego bufora
z tylnego bufora do powierzchni podstawowej.
ĆWICZENIA-5
Zmień wygląd piłki
Stwórz drugą, własną bitmapę i pozamieniaj w kodzie nazwy pliku na utworzony.
10. OBSŁUGA KLAWISZY
Dodajmy dwie paletki (jako białe prostokąty) oraz sterowanie ich położeniem za pomocą funkcji DirectInput (przyk6.zip). Lewą paletką można sterować klawiszami [A] oraz [Z], zaś prawą [UP] (Góra) i [DOWN] (Dół). Projekt wymaga dołączenia m.in. biblioteki dinput.lib. W pliku InitTerm.cpp znajdują się nowe funkcje tworzące i usuwające obiekty DirectInput. W WinBase.cpp dodano jedną linijkę :
(...)
G.hInstance = hInstance;
(...)
Obsługą klawiatury zajmuje się w naszym programie nowa funkcja odczyt_znaku
(...)
/////////////////////////////////////////////////////////////////
// odczyt_znaku
//
// pobiera znak i zmienia położenie paletek dla klawiszy A,Z,UP,DOWN
void odczyt_znaku()
{
HRESULT hRet;
// pobranie znaku z klawiatury
while (hRet = G.lpDIKeyboard->GetDeviceState(256, G.KeyState)
== DIERR_INPUTLOST)
{
if (FAILED(hRet = G.lpDIKeyboard->Acquire())) break;
}
if (KEYDOWN(DIK_A))
{
if (lewa.y>0)
lewa.y-=SPEED; // lewa paletka jedzie do góry
}
if (KEYDOWN(DIK_Z))
{
if (lewa.y<SCREEN_HEIGHT-WYSOKOSC_PALETKI)
lewa.y+=SPEED; // lewa paletka jedzie na dół
}
if (KEYDOWN(DIK_UP))
{
if (prawa.y>0)
prawa.y-=SPEED; // prawa paletka jedzie do góry
}
if (KEYDOWN(DIK_DOWN))
{
if (prawa.y<SCREEN_HEIGHT-WYSOKOSC_PALETKI)
prawa.y+=SPEED; // prawa paletka jedzie do na dół
}
}
(...)
Nazwy klawiszy takich jak DIK_UP, DIK_DOWN i inne, można znaleźć w pliku dinput.h
ĆWICZENIA-6
Zmień przyciski A, Z na S i X
Dodaj przesuwanie paletek w poziomie.
11. WYŚWIETLANIE DUSZKÓW
Duszek (ang. sprite) to mały obiekt, będący fragmentem bitmapy. Do naszej bitmapy dodamy wygląd paletek i potraktujemy ją jako zbiór trzech duszków (przyk7.zip). Wpierw zwiększamy rozmiary obrazka w pliku Resource.bmp i dorysowujemy dwie paletki.
(...)
// prawa paletka
rectSource.left = 8;
rectSource.top = 16;
rectSource.right = rectSource.left+SZEROKOSC_PALETKI;
rectSource.bottom = rectSource.top+WYSOKOSC_PALETKI;
G.lpDDSBack->BltFast(prawa.x,prawa.y,G.lpDDSRes, &rectSource,
DDBLTFAST_NOCOLORKEY | DDBLTFAST_WAIT);
// rysujemy piłkę
rectSource.left = 0;
rectSource.top = 0;
rectSource.right= rectSource.left+SZEROKOSC_PILKI;
rectSource.bottom=rectSource.top+WYSOKOSC_PILKI;
G.lpDDSBack->BltFast(pilka.x,pilka.y,G.lpDDSRes, &rectSource,
DDBLTFAST_NOCOLORKEY | DDBLTFAST_WAIT);
(...)
W funkcji Game_Main tworzymy zmienną rectSource, która jest obiektem struktury RECT o czterech polach. Następnie wypełniamy te pola w zależności od położenia naszego duszka w bitmapie. Na koniec jako czwarty parametr w funkcji BltFast umieszczamy adres obiektu rectSource.
ĆWICZENIA-7
Zmień wygląd paletek (kształt, kolory itp.)
Zamień w pliku Resource.bmp położenie paletek z położeniem piłki
12. DETEKCJA KOLIZJI
Sprawimy, by piłka odbijała się tylko od paletek. Ponadto dodamy liczenie punktów i losowy wybór początkowego kierunku lotu piłki (przyk8.zip). Nowe funkcje to
pilka_na_srodek - ustawia położenie piłki w połowie wys. i szer. ekranu
losuj_kierunek - wybiera losowo jedną z czterech wartości zmiennej lot_pilki
rysuj_wynik - wypisuje na ekranie ilość punktów graczy
Nowymi zmiennymi przechowującymi stan punktowy są punkty_lewego, punkty_prawego. Funkcje rysujące paletki i piłkę umieściliśmy w funkcji rysowanie. Poniżej znajduje się przykładowa funkcja do obsługi kierunku góra-lewo.
(...)
void gora_lewo()
{
// zakres lotu
if ((pilka.y>0) && (pilka.x>0))
{
pilka.x-=SPEED;
pilka.y-=SPEED;
}
// odbicie od sciany
if (pilka.y<=0)
lot_pilki = DOL_LEWO;
// od paletki
if (pilka.x<=SZEROKOSC_PALETKI
&& pilka.y>=lewa.y-WYSOKOSC_PILKI/2
&& pilka.y<=lewa.y+WYSOKOSC_PALETKI-WYSOKOSC_PILKI/2)
lot_pilki = GORA_PRAWO;
}
(...)
Określanie strefy odbić odbywa się tak jak na rysunku poniżej
Rys.1 Zakres odbić piłki od lewej paletki
ĆWICZENIA-8
1) Zmień wygląd punktacji
2) Dodaj różne kąty odbić piłki w zależności od miejsca uderzenia w paletkę (*)
13. TŁO I KOLOR KLUCZOWY
Następny przykład pokaże, jak dodać do naszego tenisa własne tło. Zmienimy też sposób wyświetlania duszków. (przyk9.zip) Wpierw tworzymy jeszcze jedną powierzchnię i ładujemy do niej przygotowany wcześniej obrazek, przedstawiający boisko o wymiarach 648x480 pikseli. Zauważ, że nie musimy już używać funkcji EraseBackground do mazania tła. Aby nie było widać czarnych rogów w naszych duszkach, musimy je przesyłać z kolorem kluczowym. Kolor kluczowy jest to wartość, która nie ma być przesyłana przy wyświetlaniu duszka. W pliku InitTerm.cpp ustawiliśmy kolor kluczowy jako czarny. Dzięki temu czarne obszary duszków będą wyglądały jak gdyby były przeźroczyste.
(...)
DDCOLORKEY ckey;
ckey.dwColorSpaceHighValue = ckey.dwColorSpaceLowValue = 0;
G.lpDDSRes->SetColorKey(DDCKEY_SRCBLT, &ckey);
(...)
Samo wyświetlanie różni się tylko jednym słowem. W funkcji BltFast podajemy zamiast DDBLTFAST_NOCOLORKEY parametr DDBLTFAST_SRCCOLORKEY
(...)
G.lpDDSBack->BltFast(pilka.x,pilka.y,G.lpDDSRes, &rectSource,
DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT);
(...)
Ostatnią poprawką była zmiana koloru tła dla wyświetlania stanu punkowego w funkcji rysuj_wynik.
ĆWICZENIA-9
Zmień wygląd boiska
Zmień kolor kluczowy z czarnego na biały
14. KĄTY ODBIĆ I PAUZA
Aby nieco urozmaicić grę, dodamy różne kąty odbić w zależności od części paletki, którą odbijamy piłkę. Drugą dodaną rzeczą będzie pauza wywoływana klawiszem [P] (przyk10.zip). Pierwszą czynnością jest zastąpienie czterech funkcji gora_lewo, gora_prawo, dol_lewo, dol_prawo jedną o nazwie kierunek. Pola x i y obiektu lot przechowują kierunek lotu. Piłka może dzięki temu lecieć w 12 różnych kierunkach jak na rysunku poniżej.
Rys.2 Wartości zmiennych lot.x i lot.y w zależności od kierunku lotu piłki
A oto jak przedstawiają się strefy odbić na paletce.
Rys.3 Strefy odbić na lewej paletce.
Zatrzymanie gry zrobiliśmy dodając obsługę klawisza [P].
(...)
if (KEYDOWN(DIK_P))
{
if (bPauseToggle == false)
{
bPauseToggle = true;
if (G.bPauza)
G.bPauza = false;
else
G.bPauza = true; // włączono pauzę
pauza(); // napis (PAUZA)
}
}
else bPauseToggle = false;
(...)
Zmienna bPauseToggle przechowuje stan klawisza [P] (wciśnięty = true, nie = false).
Funkcja pauza wypisuje na środku ekranu żółty napis (--PAUZA--).
ĆWICZENIA-10
Zmień wyświetlanie punktów na wyświetlanie kierunków lotu.
Zmień prędkość poruszania się paletek.
15. STRONA TYTUŁOWA
Jak łatwo zgadnąć gra jest klonem Ponga (1972 r.), więc nazwiemy ją po prostu Ping-Pong. Podzielmy naszą grę na cztery części:
MENU - tutaj będą informacje o tytule, autorze i klawiszach
STARTUJEMY - ta część wykona się raz i zawierać będzie początkowe ustawienia
BIEGNIEMY - pobieranie klawiszy i wyświetlanie wszystkiego na ekranie
KONIEC_GRY - gdy któryś graczy zdobędzie 15 punktów
Gotowy kod znajduje się w pliku przyk11.zip. Tworzymy zmienną SG (Stan Gry) i wypełniamy ją jedną z czterech możliwych wartości o nazwach takich jak nasze części.
(...)
if (G.SG == MENU)
strona_tytulowa();
if (G.SG == KONIEC_GRY)
strona_koncowa();
if (G.SG == STARTUJEMY) // początkowe wartości
{
pilka.x = SCREEN_WIDTH/2-SZEROKOSC_PILKI/2;
pilka.y = SCREEN_HEIGHT/2-WYSOKOSC_PILKI/2;
lewa.x = 0;
lewa.y = SCREEN_HEIGHT/2-WYSOKOSC_PALETKI/2;
prawa.x = SCREEN_WIDTH-SZEROKOSC_PALETKI;
prawa.y = SCREEN_HEIGHT/2-WYSOKOSC_PALETKI/2;
punkty_lewego = 0;
punkty_prawego = 0;
losuj_kierunek();
G.bPauza = false;
G.SG = BIEGNIEMY; // ruszyliśmy
} // STARTUJEMY
if (G.SG == BIEGNIEMY)
{
if ((punkty_lewego==15) || (punkty_prawego==15))
{
G.SG = KONIEC_GRY;
}
(...)
Nowe funkcje strona_tytulowa i strona_koncowa wypisują odpowiedni tekst i czekają na klawisz spacji.
ĆWICZENIA-11
Zmień stronę tytułową i końcową.
Znajdź i popraw zauważone błędy.
16. DŹWIĘK ODBICIA
Na koniec dodamy jeszcze dźwięk jaki wydawać powinna odbijana piłka (przyk12.zip). Do projektu jako bibliotekę należy dołączyć m. in. pliki dsound.lib i winmm.lib. Mamy tutaj trzy nowe pliki:
Wavread.h - nagłówek dla Wavread.cpp
Wavread.cpp - funkcje do obsługi plików .WAV
pc_beep.wav - plik dźwiękowy
Ponadto jednym pliku zaszły zmiany
Initterm.cpp - tworzenie i usuwanie DirectDraw, DierctInput i DirectSound
Plik dźwiękowy został wzięty z płyty Windows 98. Samo odtworzenie dźwięku następuje po wywołaniu funkcji Play.
(...)
// odbicie od górnej lub dolnej ściany
if ((pilka.y<=0) || (pilka.y>=SCREEN_HEIGHT-WYSOKOSC_PILKI))
{
lot.y = -lot.y;
if (G.bDzwiek)
G.lpDSB_Line->Play(0, 0, 0); // dźwięk
return;
}
(...)
Do gry dodano także możliwość włączania i wyłączania dźwięku za pomocą klawisza [D].
ĆWICZENIA-12
Zmień odtwarzany plik na inny.
Zrób inną grę
17. ZAKOŃCZENIE
Mój tekst już dobiegł końca. Mam nadzieję, że zawarte w nim przykłady bawiły, ale i czegoś nauczyły. Muszę się przyznać, że nie wszystko jest mojego autorstwa. Chcę podziękować w tym miejscu Timowi Bostonowi, który jest autorem części kodu. Bez niego ten tekst nigdy by nie powstał. Myślę, że najtrudniejszy jest pierwszy krok, zaś moją największą satysfakcją byłby fakt, że pomogłem komuś ten krok zrobić. Miłej zabawy oraz wspaniałych gier życzy autor.
KONIEC
1
3