Witam, tym razem zajmniemy się czymś bardziej złożonym, a dokładniej napiszemy grę przygodową o budowie zbliżonej do gry Mario. Nasze engine będzie uniwersalne i będzie je można wykorzystać w wielu projektach.
Nie jest ono przeznaczone tylko dla PowerDraw, aby ruszyło np. na DelphiX wystarczy zmienić jedno polecenie i grafiki przenieść do plików dxg.
1.Struktura mapy »
2.Struktura gry »
3.Struktura postaci »
4.Wykorzystanie struktur »
1. .: Struktura mapy :.
Zapewne grając w Mario zauważyliście, że grafika całej gry składa się z kwadratowych grafik. Dzięki temu jedna runda może zajmować mniej niż 1 KB. W pliku umieszczamy jedynie tablicę z wartościami. Oto grafiki wykorzystane w naszej grze:
Po złożeniu:
Teraz nadszedł czas zadeklarować naszą planszę. Aby mieć porządek w grze utwórzmy nowy Unit i nazwijmy go zmienne.pas. Powinien on wyglądać mniej więcej tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
unit Zmienne; interface uses PlikTGra, PlikTGracz, PowerDraw3, PowerInputs; const // Ilosc klatek na dlugosc mapa_w = 64; // Ilosc klatek na szerokosc mapa_h = 8; type TPlik = object Plansza : array[1..mapa_w,1..mapa_h] of Byte; end; implementation end. |
Obiekt Tplik zawiera tablicę Plansza, która stanowi naszą mapę. Jak widać ma ona dwie zmienne. Mapa_w i Mapa_h określa nam ilość kwadratowych grafik przypadających na długość i szerokość mapy. W naszym przypadku każda kwadrat ma rozmiar 50×50. Co przy rozdzielczości 800×600 daje nam mapę długości 4 ekranów i szerokości 400px :
Znamy już budowę mapy. Czas przejść do następnej części.
2. .: Struktura gry :.
Tworzymy nowy unit plik TGra.pas, w którym będziemy zapisywać wszystko co dotyczy tej struktury. Przedstawia się ona następująco :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
type TGra = object private // Rysuje plansze procedure RysujPlansze; // Rysuje postacie grające procedure RysujGraczy; // Przechodzi do następnej klatki gry procedure Oddech; public //Wartość przesunięcia mapy Przesuniecie : Integer; //Kierunek przesunięcia mapy PrzesunKierun: Byte; //Biblioteka z grafiką do gry GrafikaBaza : TVTDb; //Grafiki do gry GrafikaMapy : array[1..6] of TAGFImage; //Wykonuje czynności potrzebne do uruchomienia gry procedure Tworz; //Otwiera plansze procedure OtworzPlansze(Nazwa : String); //Przerzuca wszystko na ekran procedure Rysuj; //Przechodzi do następnych klatek graczy i mapy procedure Oddychaj; { :) } //Rozpoczyna przesuwanie mapy procedure PrzesunMapeStart(Kierunek : Byte); //Przesuwa mapę procedure PrzesunMape; //Rysuje grafikę procedure Draw(Rysunek: TAGFImage; X: Integer; Y: Integer; Klatka: Integer; Efekt : Integer); end; |
Jak widzimy budowa nie jest wcale tak skomplikowana. Przejdźmy teraz do opisu każdej procedury. Zacznijmy do procedure Tworz; . Ustawiamy na początku pozycję startową mapy na 1 ( Mapa_X := 1 ). W dalszej części otwieramy i tworzymy potrzebne grafiki. Wygląda ona następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
procedure TGra.Tworz; begin Mapa_X := 1; GrafikaBaza := TVTDb.Create(PD); GrafikaBaza.FileName := Dir+'grafikagrafika.vtd'; //----------Gleba----------- GrafikaMapy[1] := TAGFImage.Create(PD); GrafikaMapy[1].LoadFromVTDb(GrafikaBaza,'pod', D3DFMT_A8R8G8B8); //-------Powierzchnia------- GrafikaMapy[2] := TAGFImage.Create(PD); GrafikaMapy[2].LoadFromVTDb(GrafikaBaza,'nad', D3DFMT_A8R8G8B8); //---------Lewy Kąt--------- GrafikaMapy[3] := TAGFImage.Create(PD); GrafikaMapy[3].LoadFromVTDb(GrafikaBaza,'lbok', D3DFMT_A8R8G8B8); //--------Prawy Kąt--------- GrafikaMapy[4] := TAGFImage.Create(PD); GrafikaMapy[4].LoadFromVTDb(GrafikaBaza,'pbok', D3DFMT_A8R8G8B8); end; |
Kolejną procedurą również bardzo prostą jest procedure OtworzPlansze(Nazwa : String);. Dodatkowego opisu chyba nie trzeba :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
procedure TGra.OtworzPlansze(Nazwa : String); begin // przypisanie pliku do zmiennej Assignfile(Plik,Dir+'plansze'+Nazwa+'.map'); // otwarcie pliku Reset(Plik); //odczyt i zapis may do zmiennej GraPlik Read(Plik,GraPlik); //zamknięcie pliku Closefile(plik); end; |
Podążamy dalej i trafiamy na procedure Rysuj;. Wykonuje ona dwie inne procedury odpowiedzialne za rysowanie. Jej istnienie pozwala na utrzymanie porządku w kodzie. Wszystko co ma coś rysować umieszczamy tutaj. A wygląda ona następująco :
1 2 3 4 5 |
procedure TGra.Rysuj; begin RysujPlansze; RysujGraczy; end; |
No i następna procedure RysujPlansze. Jak sama nazwa mówi, jest ona odpowiedzialna za narysowanie naszej planszy według tablicy. Użyliśmy tutaj pętli for, która przesuwając się o jedno pole w poziomie rysuje 8 grafik w pionie. Przesuwa się od mapa_X, która jest pierwszą klatką do mapa_X+16, która stanowi ostatnią. Dlaczego 16 ? Otóż ekran ma długość 800, a klatka 50, więc 800 : 50 = 16. Przy pozycji Y dodajemy 100 aby plansza nie była rysowana od samej góry, tylko tak jak na rysunku numer 2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure TGra.RysujPlansze; var pozx, pozy : integer; begin for pozX:=mapa_X to mapa_X+16 do for pozY:=1 to 8 do case GraPlik.Plansza[pozX,pozY] of 1: Draw(GrafikaMapy[1],(pozX-mapa_X)*50,(pozY-1)*50+100,0,EffectNone); 2: Draw(GrafikaMapy[2],(pozX-mapa_X)*50,(pozY-1)*50+100,0,EffectNone); 3: Draw(GrafikaMapy[3],(pozX-mapa_X)*50,(pozY-1)*50+100,0,EffectNone); 4: Draw(GrafikaMapy[4],(pozX-mapa_X)*50,(pozY-1)*50+100,0,EffectNone); end; end; |
Teraz zajmiemy się procedurami odpowiedzialnymi za przesuwanie mapy. Nie będę się rozpisywał. Pierwsza uruchamia przesuwanie i nadaje kierunek, który wcześniej ustawił gracz( ale to później), zaś druga wedle kierunku przesuwa mapę w lewo lub w prawo, zmniejszając lub zwiększając wartość zmiennej startowej rysowania Mapa_X. Jeśli ktoś ma ochotę może przekształcić tę procedurę, aby mapa się płynnie przesuwała, ale ja dla łatwego zrozumienia uprościłem to.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
procedure TGra.PrzesunMapeStart(Kierunek : Byte); begin Przesuniecie :=4; PrzesunKierun := Kierunek; end; procedure TGra.PrzesunMape; begin dec(przesuniecie); If PrzesunKierun = w_lewo Then Dec(Mapa_X); If PrzesunKierun = w_prawo Then Inc(Mapa_X); If Przesuniecie = 0 Then begin Przesuniecie := -1; end; end; |
Następna procedura jest odpowiedzialna za rysowanie postaci w grze, ponieważ w naszym kodzie w tej części tutorialu znajduje się tylko jedna postać, to procedura jest bardzo prosta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
procedure TGra.RysujGraczy; begin Gracz.Rysuj; end; Podobnie jak poprzednia procedura te dwie są wersjami uproszczonymi. Co robią, chyba tłumaczyć nie trzeba: procedure TGra.Oddech; begin //Przesuwanie mapy If Przesuniecie > -1 then PrzesunMape; end; procedure TGra.Oddychaj; begin Gra.Oddech; Gracz.Oddech; end; |
Została nam jeszcze tylko jedna procedura. Jaki jest sens jej tworzenia ? A to taki, że aby grę przenieść na DelphiX wystarczy w niej zamienić PD.RenderEffect na np. DXDraw.Draw…. Oczywiście, trzeba jeszcze przepakować grafikę z plików vtd na dxg i zadeklarować ją w grze, ale to chyba nie stanowi żadnego problemu.
1 2 3 4 5 6 7 8 9 |
procedure TGra.Draw(Rysunek: TAGFImage; X: Integer; Y: Integer; Klatka: Integer; Efekt : Integer); begin PD.RenderEffect(Rysunek ,X ,Y ,Klatka ,Efekt); end; |
3. .: Struktura postaci :.
Podobnie jak przy głównej strukturze, dla tej tworzymy oddzielny plik np. plik TGracz.pas. A oto ona:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
type TPoz = record X : Integer; Y : Integer; end; type TGracz = object //komputer czy człowiek Tryb : Byte; //wysokosc skoku Skok : Integer; //max wysokość skoku maxSkok : Integer; //predkosc poruszania się predkosc: Integer; //pozycja w pikselach Pozycja : TPoz; //pozycja w klatkach PozycjaNaMapie : TPoz; //kierunek poruszania się : lewo lub prawo KierunekX : Byte; //ostatnio zmieniony kierunek KierunekX_old : Byte; //zmiana pozycji Y np. Skok RuchY : Byte; //biblioteka z grafiką GrafikaBaza : TVTDb; //grafiki dla postaci Grafika : array[1..4] of TAGFImage; //inicjacja postaci procedure Tworz; //rysowanie postaci procedure Rysuj; //poruszanie postaci procedure Idz; //mechanizmy kontrolne postaci procedure Oddech; end; |
Przejdźmy do opisywania procedur. Jako pierwszą weźmiemy procedure Tworz;. Na początku otwieramy grafikę. W naszym przypadku jest to tylko rysunek postaci odwróconej w lewo i w prawo. Oczywiście można to rozbudować przypisując osobne grafiki dla skoku, strzelania, itp.
Zmienna Pozycja określa nam aktualną pozycję postaci na ekranie. Jako początkową pozycję wybrałem X= 200 a Y = 120. Oczywiście można je zmienić według uznania.
Zmienna maxSkok określa nam maksymalną wysokość o jaką może się przemieścić jednostka podczas skoku.
Skok – aktualna wartość skoku, ponieważ na początku postać się nie porusza, ustawiamy wartość na 0.
Predkosc – określa prędkość poruszania się postaci. Domyślnie ustawiłem 1. Później zrobimy, że postać zwiększy szybkość po wciśnięciu Shift, co da nam efekt biegania.
KierunekX, KierunekX_old – kierunek, w który odwrócona będzie jednostka podczas uruchomienia gry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
procedure TGracz.Tworz; begin GrafikaBaza := TVTDb.Create(PD); GrafikaBaza.FileName := Dir+'grafikagrafika.vtd'; //---------W lewo---------- Grafika[1] := TAGFImage.Create(PD); Grafika[1].LoadFromVTDb(GrafikaBaza,'gracz0_l', D3DFMT_A8R8G8B8); //---------W prawo--------- Grafika[2] := TAGFImage.Create(PD); Grafika[2].LoadFromVTDb(GrafikaBaza,'gracz0_p', D3DFMT_A8R8G8B8); Pozycja.X := 200; Pozycja.Y := 120; maxSkok := 150; Skok := 0; Predkosc := 1; KierunekX := w_prawo; KierunekX_old := w_prawo; end; |
Przechodzimy dalej, teraz zajmiemy się procedurą Rysuj. Rysuje ona nam postać odwróconą w kierunku zależnym o zmiennej KierunekX_old.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TGracz.Rysuj; begin If KierunekX_old = w_lewo then Gra.Draw(Grafika[1],Pozycja.X-(mapa_X*50),Pozycja.Y,0, EffectSrcAlpha); If KierunekX_old = w_prawo then Gra.Draw(Grafika[2],Pozycja.X-(mapa_X*50),Pozycja.Y,0, EffectSrcAlpha); end; |
Kolejna opisywana procedura jest odpowiedzialna za to, co ma się dziać z naszą postacią niezależnie od nas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
procedure TGracz.Oddech; begin //Odświeżamy przechwycanie klawiszy Klawiatura.Update; //Pozycja na mapie musi być zależna //od kierunku, ponieważ postać idąca w lewo reagowałaby na //zmiana powierzchni już po przejściu jej drobnej części na //następną klatkę, a nie po przejściu całości. Poniższe trzy //polecenia przeliczają pozycję w pikselach na pozycję //wyrażoną w klatkach If KierunekX = w_prawo then PozycjaNaMapie.X := (Pozycja.X+(Grafika[1].PatternWidth) div 2) div 50; If KierunekX = w_lewo then PozycjaNaMapie.X := (Pozycja.X+(Grafika[1].PatternWidth) div 2) div 50; PozycjaNaMapie.Y := (Pozycja.Y-75) div 50 +1; // Jeżeli postać otrzyma polecenie skoku //w górę, wtedy ma się w nią przemieszczać... //-------Skok-------- If RuchY = w_gore then begin Inc(Skok,5); Dec(Gracz.Pozycja.Y,5); //Ograniczenie skoku //jeżeli wysokość osiągnie maksymalną wartość If (Skok>=maxSkok)or //jeżeli wysokość przekroczy ekran wyświetlania (Pozycja.Y < 110)or //jeżeli klatka nad nią będzie zajęta... //w set Możliwy_ruch znajdują się wartości klatek, //po których jednostka może się poruszać, // a więc zarazem stanowi przeszkodę, //dla skaczącego od dołu (GraPlik.Plansza[PozycjaNaMapie.X,PozycjaNaMapie.Y-1] in Mozliwy_ruch ) then begin RuchY :=0; Skok :=0; end; end; //Jak podskoczy do góry, to musi potem oczywiście spaść w dół. //Jeśli klatka pod nim ma jakąś z wartości w set Zakaz_ruchu // wtedy ma spadać. //-----Spadanie------- If Skok = 0 then If GraPlik.Plansza[PozycjaNaMapie.X,PozycjaNaMapie.Y] in Zakaz_ruchu then Inc(Pozycja.Y,5); //Wykonanie procedury odpowiedzialnej za ruch ( patrz niżej). Gracz.Idz; end; |
Została nam jeszcze ostatnia procedura związana z postacią. Przechwyca ona wciśnięcie klawisza i wykonuje związane z nim polecenie. Poruszanie się w lewo lub prawo zachodzi po sprawdzeniu, czy nie ma na drodze przeszkody. Sprawdzanie następuje poprzez odwołanie się do tablicy z planszą.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
procedure TGracz.Idz; var PredkoscA : Byte; begin // Bieg z przyciskiem Shift If (Klawiatura.Keys[54]) then // Shift PredkoscA := Predkosc + 1 else PredkoscA := Predkosc; //Zerowanie kierunku KierunekX := 0; //Poruszanie się w prawo If (Klawiatura.Keys[205])and(PozycjaNaMapie.Xand ( (GraPlik.Plansza[PozycjaNaMapie.X,PozycjaNaMapie.Y-1] in Zakaz_ruchu )or (GraPlik.Plansza[PozycjaNaMapie.X+1,PozycjaNaMapie.Y-1] in Zakaz_ruchu ) ) then begin KierunekX := w_prawo; KierunekX_old := w_prawo; INC(Gracz.Pozycja.X, PredkoscA); end; //Poruszanie się w lewo If (Klawiatura.Keys[203])and(PozycjaNaMapie.X*50 > 1)and ( (GraPlik.Plansza[PozycjaNaMapie.X,PozycjaNaMapie.Y-1] in Zakaz_ruchu )or (GraPlik.Plansza[PozycjaNaMapie.X-1,PozycjaNaMapie.Y-1] in Zakaz_ruchu ) ) then begin KierunekX := w_lewo; KierunekX_old := w_lewo; DEC(Gracz.Pozycja.X, PredkoscA); end; //Skok If Klawiatura.Keys[200] then // w górę - skok If (GraPlik.Plansza[PozycjaNaMapie.X,PozycjaNaMapie.Y-1] in Zakaz_ruchu )and (GraPlik.Plansza[PozycjaNaMapie.X,PozycjaNaMapie.Y] in Mozliwy_ruch ) then RuchY := w_gore; // Przesuwanie mapy w prawo, jeżeli znajdzie się na //przed ostatniej klatce ekranu If (PozycjaNaMapie.X > Mapa_X + 12)and(Mapa_X+16Then Gra.PrzesunMapeStart(w_prawo); // Przesuwanie mapy w lewo, jeżeli znajdzie się na //drugiej klatce ekranu If (PozycjaNaMapie.X < Mapa_X + 2)and(Mapa_X>1) Then Gra.PrzesunMapeStart(w_lewo); end; |
Na zakończenie jeszcze raz plik Zmienne.pas. Oto jego cała zawartość:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
unit Zmienne; interface uses PlikTGra, PlikTGracz, PowerDraw3, PowerInputs; const //Kierunek ruchu w_lewo = 1; w_prawo = 2; w_gore = 1; w_dol = 2; //Tryb gracza tryb_czlowiek= 1; tryb_komputer= 2; //Mapa mapa_w = 64; mapa_h = 8; type TPlik = object Plansza : array[1..mapa_w,1..mapa_h] of Byte; end; var Gra : TGra; GraPlik : TPlik; Gracz : TGracz; Wybrany : byte; X_map, Y_map : integer; Plik : file of TPlik; Dir : string; PD : TPowerDraw; Klawiatura : TPowerInput; Mapa_x : Integer; //pola po których gracz nie może się poruszać Zakaz_ruchu : Set of Byte = [0,1]; //pola po których gracz może się poruszać Mozliwy_ruch : Set of Byte = [2,3,4]; implementation end. |
4. .: Wykorzystanie struktur w grze :.
Opuszczę opis przygotowania aplikacji do PD, ponieważ omawialiśmy już to wcześniej. Chciałbym się skupić na kilku poleceniach.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
procedure TFormaGry.FormCreate(Sender: TObject); var jest : integer; begin ... //Katalog gry Dir := SysUtils.ExtractFileDir(Application.ExeName); // Przypisanie okna PoweDraw do PowerDraw //obiektu TGra PD := PowerDraw1; // Obsługa klawiatury Klawiatura := TPowerInput.Create(FormaGry); Klawiatura.DoKeyboard := True; jest := Klawiatura.Initialize; if jest<>0 then close; //Uruchaminaie gry Gra.Tworz; Gra.OtworzPlansze('Runda1'); //Tworzenie postaci grających Gracz.Tworz; end; procedure TFormaGry.ZegarRender(Sender: TObject); begin with PowerDraw1 do begin PowerDraw1.Clear(clBlack); beginScene; // Wykonywanie funkcji niezależnych Gra.Oddychaj; //Rysuj elementy gry Gra.Rysuj; endScene; Present; end; end; |
To już naprawdę koniec :). Patrząc na powyższe procedury widzimy, że wykonywanie całości gry może się odbywać niemal niezależnie od jednej aplikacji czy komponentu. Ten kod bez problemu można teraz przenieść na DelphiX, DirectX czy OpenGl. w najbliższym czasie pojawi się druga część tego tutorialu. Rozszerzymy wtedy możliwości naszej gry.