Tematem tego artykułu jest poruszanie się bohaterów w świecie gry 2D, konkretnie chciałbym w nim przedstawić rozwiązanie pewnych problemów jakie można w takim „świecie” napotkać. Oczywiście kod programu jest w DELPHI z użyciem OMEGI (czytaj: swego rodzaju krucjata przeciwko zamianie kompilatora na CPP, może kiedyś ktoś przedstawi na unit1.pl komponent GLScene pod DELPHI- jeszcze bardziej potężniejszy „przeciwnik” przejścia na CPP ). Temat chciałbym przedstawić w trzech częściach. Ale do rzeczy…
Jakie to problemy?:
a). jak wyświetlić duszka po drugiej stronie drzewa (za liśćmi) lub budynku ,
b). jak przejść przez las,
c). jak warunek a i b połączyć z testem kolizji,
d). jak uniknąć dzielenia obiektów większych niż rozmiar przyjętej siatki planszy nie tracąc spójności obiektu pod względem wykrywania kolizji i zajmowanego „klocka” pola siatki 2D,
e). jak przyśpieszyć działanie programu pomimo włączonej funkcji OmegaSprite1.Collision (sugestie).
Tu chciałbym nadmienić, że podane rozwiązania są propozycją ugryzienia problemu i na pewno nie są jedynym sposobem. Dają dobry wynik w teście na FPS i mogą być bazą (niewielka zmiana podejścia) do opracowania metody tworzenia gigantycznych światów. Do części 3 dołączę kilka uwag do mojego programu- narzędzia(sam exek, na razie kod zachowam w tajemnicy, może ktoś z czytających ten artykuł znajdzie swoje własne rozwiązanie) pozwalający wygenerować giganty 2D. Mapa giganta składa się z 3 warstw. Przykładowo na swoim kompie (Duron 850, RAM: 512MB, graf: 64MB) świat 2000×2000 generował się około 10 sekund. Dla kości świata o wymiarach 128×64 uzyskuję FPS:265 a dla kości 94×64 FPS: 212. FPS testowany był bez synchronizacji pionowego odświeżania monitora. Próbowałem testować plansze 2500×2500, ale kompowi zabrakło pamięci. Oczywiści w każdym z przypadków był 1 bohater testujący kolizję przejścia- drogi. W załączniku plik gigant2D.exe
UWAGA
Omawiane przykłady nie są super szybkim przewijaniem światów 2D. Podstawowym tematem artykułu jest zmiana koloru duszka znajdującego się za obiektem. Program gigant2d.exe napisałem i dołączyłem do artykułu tylko po to aby nie być gołosłownym (nie uwzględniłem w nim omawianej zmiany koloru, utworzyłem w nim precyzyjniejszą metodę wykrywania przeszkody, która bazuje na pomyśle jaki opiszę w części drugiej).
Jeśli ktoś w podanych przykładach chciałby zwiększyć FPS niż przeze mnie ustalony, to powinien w komponencie OmegaScreen1 zmienić na wartość FALSE właściwość OmegaScreen1.VSYNC:
CZEŚĆ 1- ZMIANA BARWY BOHATERA DUSZKA
Patrz rysunek A (pod głównym tematem artykułu).
Przed przystąpieniem do analizowania kodu, chciałbym zwrócić uwagę na taki problem:
obiekt umieszczony na planszy gry nie będący na przykład trawą czy też innym gruntem a mogący wytworzyć kolizję z duszkiem gracza wcale nie musi mieć rozmiaru kostki pola świata 2D. Przykładowo nasz świat oprzemy na kostkach 64×32 a użyte drzewa będą mieć wymiary: 64×80, 64×90, 48×80, 38×46… A stąd już blisko do takiego problemu : powiedzmy osadzam w świecie karczmę lub inny budynek… I jak tu do niego wejść? A no prosto Tak samo jak w świecie rzeczywistym… otworzyć drzwi. Ale drzwi są (z reguły) jedne i z jednej strony!
I jest poważny problem: Jakie przyjąć rozwiązanie aby to wszystko połączyć? Pewnie wielu z nas stosuje warstwy w świecie 2D. Tylko, że są one o tych samych wymiarach co warstwa podstawowa (ale na to też mam pomysł)… Mogą być dużo mniejsze niż taki budynek., drzewo (ale do drzew nie wchodzimy, chyba że ma dziuplę:)…Zostaje jedno uniwersalne rozwiązanie… Każda rodzina typu krzak, budynek, to osobna klasa , zaś drzwi, wejścia czy też inne przejścia, to wirtualne dziury w kolejnych warstwach…(i tak powstał temat na kolejny artykuł).
Przyjmując ten model rozwiązania należy liczyć się z tym, że nie możemy wykorzystać komponentu Omegi do tworzenia map, czyli OmegaMap.
Przystępujemy do budowy szkieletu klasy przechowującej kostki świata gry i obiektów. Zaczniemy od klasy bazowej dla świata. Tu chciałbym jeszcze raz podkreślić, że nie jest to jeszcze podejście do gigantycznych światów 2D rzędu 250×250 czy też większych do 2000×2000. Jeśli mi ktoś nie wierzy, że można to ugryźć przy pomocy DELPHI (a nawet niedocenianego Pascala, choć tu z grafiką będzie trudniej) oraz będąc programistą amatorem z lamerskim (w moim przypadku) spojrzeniem na wiele zagadnień:, to odsyłam do mojego projektu edytora światów 2D na www.delphi.ilion.pl dział narzędzia (co prawda ta wersja edytora nie zawiera jeszcze pomysłów tu omawianych ale może go kiedyś zmodyfikuję).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
TKostka=class(TSprite) protected constructor Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem;//indeks obrazu z listy OmegaImageList const IdKlatka:Integer//indeks wycinanej klatki );virtual; procedure Draw;override; procedure Move(const MoveCount:single);override; end; |
Ta procedura odpowiada z sposób rysowania: procedure Draw;
a ta: procedure Move(const MoveCount:single);override; za ruch mapy świata.
1 2 3 4 5 6 7 8 9 10 11 |
procedure TKostka.Move(const MoveCount:single); begin inherited Move(MoveCount); x:=x+SkokX; y:=y+SkokY; end; |
gdzie SkokX i SkokY to zmienne zdefiniowane w sekcji var głownego projektu.
procedure TKostka.Draw;
begin
if ImageIndex<0 then exit;
inherited Draw;
end;
Musimy ten warunek podać if ImageIndex<0 then exit; jeśli chcemy otrzymać niewypełnione kostki warstwy bez używania dodatkowych tablic. Warto też sobie zapamiętać, ze jeśli byśmy podali do ImageIndex wartość -1(minus jeden) to zostanie narysowany cały obraz bez wycinani klatek. A ja tu to wykorzystałem do warunku rysowania lub nie jakiegoś obiektu (bez jakiś tam dodatkowych pętli).
Na tej klasie będzie żyć każda pojedyncza kostka warstwy i dodatkowo będzie ona klasą bazową do klasy naszych krzaków i drzew. Dla osób pierwszy raz spotykających się z pojęciem warstwy w świecie 2D przedstawiam poglądowy rysunek traktujący warstwy, że tak powiem klasycznie
Teraz możemy zdefiniować klasę dla krzaków
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
TRoslina=class(TKostka) k, w:integer; //kolumna i wiersz zaczepienia pnia rosliny protected constructor Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem;//indeks obrazu z listy OmegaImageList const IdKlatka:Integer//indeks wycinanej klatki );virtual; procedure Draw;override; procedure Move(const MoveCount:single);override; procedure onCollision(const Sprite:Tsprite;const colX,colY:integer);override; end; |
Klasa TRoslina odziedziczy po rodzicu procedurę Move, którą w dalszej części nieznacznie zmodyfikujemy. TRoslina będzie mieć możliwość wykrywania testu kolizji z duszkiem gracza:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
procedure TRoslina.onCollision(const Sprite:Tsprite;const colX,colY:integer); begin if (Sprite is TGracz)then begin TGracz(Sprite).fKolizja:=true; TGracz(Sprite).fPokryty:=TGracz(Sprite).Pokryty(x,y,Image.TileWidth,Image.TileHeight); end; end; |
Jej zadaniem jest przekazanie informacji w postaci logiczne flagi fKolizja i fPokryty do duszka gracza.
Na część pierwszą artykułu tyle informacji o tych klasach powinno wystarczyć. Jedynie chciałbym w tym miejscu podkreślić, że tak planujemy kod aby unikać dodatkowych pętli po utworzonych obiektach.. A może być ich znaczna ilość. Te funkcje i tak są wykonywane wystarczy spojrzeć na procedurę zegara aplikacji. Jest tam wywoływana procedura OmegaSprite1.Draw, OmegaSprite1.Move, OmegaSprite1.Collision, OmegaSprite1.Dead. Dowodem tego, że to działa jest chociażby przesuwanie mapy świata. Przecież ten kod
1 2 3 4 5 6 7 8 9 10 11 |
procedure TKostka.Move(const MoveCount:single); begin inherited Move(MoveCount); x:=x+SkokX; y:=y+SkokY; end; |
Jest tylko w tym i tylko w tym miejscu zdefiniowany. A o tym czy zmienia się wartość zmiennych SkokX lub SkokY decyduje gracz przyciskając klawisze strzałek….
Teraz bierzemy pod lupę klasę duszka Gracza:
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 |
TGracz=class(TSprite) public fPokryty, fKolizja:boolean; function Pokryty(x0,y0,_width,_height:single):boolean; procedure MoznaIsc; protected constructor Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem;//indeks obrazu z listy OmegaImageList const IdKlatka:Integer//indeks wycinanej klatki ); procedure Move(const MoveCount:single);override; procedure Draw;override; end; |
A poniżej kilka uwag do je funkcjonowania
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 |
constructor TGracz.Create(const AParent: TOmegaSprite; const ImageListItem: TImageListItem; const IdKlatka:Integer ); begin inherited Create(AParent); Image := ImageListItem;// określamy indeks obrazu duszka z listy zasobów grafiki OmegaImageList ImageIndex :=idKlatka;// tu wycinamy konkretną klatkę width :=Image.TileWidth;// okreslamy szerokosc i wysokosć obrazu duszka height : =Image.TileHeight; DoCollision :=true;// żądamy testu wykrywania kolizji DoPixelCheck :=true;// test ma uwzględniać zmianę pikseli end; |
Tę procedurę rozbudujemy w następnych częściach artykułu:
1 2 3 4 5 6 7 |
procedure TGracz.Move(const MoveCount:single); begin inherited Move(MoveCount); end; |
Tę również, która odpowiada ona z ruch (choć w tej postaci nic nie testuje, będzie to zrobione w następnej części)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure TGracz.MoznaIsc; begin //Sterowanie Graczem if oisUp in Form1.OmegaInput1.keyboard.states then//idz do gory SkokY:=SkokY+1; if oisDown in Form1.OmegaInput1.keyboard.states then//idz w dół SkokY:=SkokY-1; if oisRight in Form1.OmegaInput1.keyboard.states then//idz w prawo SkokX:=SkokX-1; if oisLeft in Form1.OmegaInput1.keyboard.states then//idz w lewo SkokX:=SkokX+1; end; |
Procedura poniżej zamieszczona zmienia sposób rysowania duszka gracza. Jeśli duszek znajdzie się za drzewem to zostanie wyświetlony w innej barwie. Można popróbować rożnych ustawień ostatnich pięciu parametrów to znaczy składowych R,G,B, Alpha i tej ostatniej, która może przyjąć takie wartości:
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 |
bmIgnore = -1; bmNormal = 0; bmAdd = 1; bmSrcAdd = 2; bmSrcCol = 3; bmSrcColAdd = 4; bmMultiply = 5; bmMultiplyAlpha = 6; bmInvMultiply = 7; bmInvMultiplyAlpha = 8; bmInvert = 9; bmSrcBright = 10; bmDestBright = 11; bmInvSrcBright = 12; bmInvDestBright = 13; |
Stale te zdefiniowane są w źródłach komponentu OMEGI (tu w pliku OmegaScreen.pas)
1 2 3 4 5 6 7 8 9 10 11 |
procedure TGracz.Draw; begin if not fKolizja then inherited Draw else Image.Draw(Round(x),Round(y),0,0.5,0.5,1,1,255,0,255,155,ImageIndex,0{ czyli bmNormal }); end; |
A co robi to dziwactwo?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function TGracz.Pokryty(x0,y0,_width,_height:single):boolean; begin result:=false; if (x+2>X0)and (y+2>y0)and (x-2+Image.TileWidth <x0+_width)and <br=""> (y-2+Image.TileHeight<y0+_height) <br=""> then result:=true; end;</y0+_height)></x0+_width)and> |
Już odpowiadam. Sprawdza czy cały obrazek duszka został przykryty obszarem obrazu drzew czy też budynku
(rezultat jej działania wykorzystuje procedura testu kolizji klasy TRoslina). Jeśli nie, to ma się nam pokazać tylko głowa duszka lub inna część nieprzykryta bez zmiany koloru. Tak jak to pokazuje poniższy rysunek:
Jak się przyjrzeć, to można zobaczyć, że gracz jest pod roślinką i po mimo kolizji może pod nią iść…(część 2)
Na zakończenie poniżej przedstawiam, to co powinniśmy uzyskać z kodu 1 części. (pełny w załączniku do artykułu). W części pierwszej duszek nie reaguje na kontakt z pniem drzewa… ale o tym w 2 części.
Koniec części 1.
P.S.
W załączniku jest kod części pierwszej.
Pozdrawiam oksal
Autor: oksal