BOHATER SPOTYKA DUŻO DUŻYCH BOROSTWORÓW…
i jest to duży problem, nie ze względu na charakter spotkania (choć bohater świata 2D ma inne zdanie) ale na sposób oprogramowania natury BOROSTAWORA. Taki stwór musi się jakoś zachowywać a jego duszek powinien być oprogramowany według tych samych zasad co duszek bohatera.
Ten artykuł porusza problem duszka stwora. Jego AI zostawiam na poziomie losowego doboru kierunku ruchu. Jeżeli czytelniku chcesz się wgłębić w rozbudowę AI to polecam projekt Tostera (link do projektu podaję z unit1.pl: https://unit1.pl/forum/viewtopic.php?t=50 )
Klasa borostwora znajduje się w pliku UnitStwor000.pas;
ZAŁOŻENIA
- W zależności od kierunku ruchu odpowiednio dobierana jest animacja klatek
- Duszek borostwora zmienia kolor w cieniu przeszkody na tych samych zasadach co duszek bohatera
- Wykrywa kolizje z pniem drzewa
- Losowo dobiera kierunek ruchu
- Wchodzi do karczmy
- Nie ucieka poza obszar dostępnego świta
Jak widać warunki b, c, e są wspólne z klasą duszka bohatera. Ten fakt prowadzi do wniosku, że należy utworzyć wspólna klasą – rodzica dla bohatera i borostwora i z tej klasy tworzyć „dzieci” tych duszków. Pisząc część pierwszą „Wędrówek w 2D” nie przewidywałem występowania borostworów, stąd klasę duszka borostwora utworzyłem bezpośrednio na TSprite. Choć można było wybrać i TSimpleAnimSprite, która to posiada mechanizm animacji klatek. W TSprite też da się to zrobić i to w trzech linijkach:
1 2 3 4 5 |
IDAnimacji:=IDAnimacji+dVA*Value;//dVA to predkosc animacji, Value to OmegaTimer1.DeltaSecs if IDAnimacji>2 then IDAnimacji:=0; ImageIndex:=Round(IDAnimacji+OfsetKlatki); |
Dodatkowo osoby zainteresowane maja odpowiedź jak oprogramować duszka bohatera, aby się zmieniały jego klatki ruchu.
Tyle słowa wstępnego. Przystępujemy do pracy…
ALGORYTM DUSZKA BOROSTWORA…
Zegar gry wywołuje w takiej kolejności procedury związane z obsługa klasy bazowej duszka borostwora- TSprite
1 2 3 4 5 6 7 8 9 10 |
OmegaSprite1.Collision; OmegaSprite1.Dead;// ta nas nie interesuje bo nic nie usmiercamy OmegaSprite1.Draw; OmegaSprite1.Move(OmegaTimer1.DeltaSecs); //czesc piąta- zamiana miejscami z OmegaSprite1.Draw; |
Co prowadzi do takiego postępowania:
- Sprawdź kolizję z innymi borostworami i duszkiem gracza
- Rysuj duszka borostwora według tych samych procedur co bohatera ( omówiłem w poprzednich częściach)
- Podejmij akcję związaną ze zmianą położenia
Krok a nie wykrywa kolizji z pniami tylko z duszkami obiektów żywych a to tylko dla tego, że jak przyjdzie do szukania drogi do wskazanego celu to na takim teście kolizji nic nie znajdziemy. Drogi się szuka z siatek mapy np algorytm Dijkstry
Krok b jest przeniesiony z klasy Tgracz
Krok c to przebudowana procedura Move(const MoveCount:single) oraz MoznaIsc(MoveCount). Ich zadanie polega na symulowaniu decyzji związanych z ruchem borostwora. W przypadku gracza było to banalne wystarczyło odczytać jaki klawisz wciśnięto na klawiaturze. Tu na podstawie mapy wykonujemy ruch
BOROSTWÓR ZMIENIA POZYCJĘ
Chodzi o ten fragment kodu programu
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 |
procedure TStwor.MoznaIsc(const Value:single); var stKierunek:tkierunek; begin stareX:=x; stareY:=y; case kierunek of kierN : kierunekN (2*Value); kierNE : kierunekNE(2*Value); kierE : kierunekE (2*Value); kierSE : kierunekSE(2*Value); kierS : kierunekS (2*Value); kierSW : kierunekSW(2*Value); kierW : kierunekW (2*Value); kierNW : kierunekNW(2*Value); end; x:=x+Skok_X; y:=y+Skok_Y; JakaKostka; x:=stareX; y:=stareY; if(Drogi[wd,kd]>-1)then begin dVA:=0; Odbicie; Skok_X:=0; Skok_Y:=0; exit;//wyskocz z calej procedury end; IDAnimacji:=IDAnimacji+dVA*Value;//dVA to predkosc animacji if IDAnimacji>2 then IDAnimacji:=0; ImageIndex:=Round(IDAnimacji+OfsetKlatki); dVA:=7;//ponownie przypisz predkość animacji bo istnieją funkcje które ją zerują //podczas kolizji- zmniejsza to migotanie x:=x+Skok_X; y:=y+Skok_Y; end; |
Jest to przebudowana procedur z klasy TGracz. Idea ta sama co w analogicznej funkcji ruchu Gracz (wyjaśnienie w poprzednich częściach). Zmiana położenia odbywa się w funkcjach kierunku (osiem kierunków zgodnie z Różą Wiatrów) i tak dalej. Zastępuje to wciskanie klawiszy ruchu. Tu podkreślam, że te funkcje wyznaczają zmianę współrzędnych według tych linijek:
1 2 3 4 5 6 7 |
kierunekN (2*Value); kierunekNE (2*Value); x:=x+Skok_X; y:=y+Skok_Y; |
Linijki te nie reagują na przesuwanie wykonane przez Gracza. Duszek Gracza zawsze jest we współrzędnych środka ekranu. To świat przesuwa się względem Gracza. I my ten fakt musimy uwzględnić. A wykonujemy to w tym miejscu kodu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
procedure TStwor.Move(const MoveCount:single); begin //przesun bo przesunieto swiat x:=x+SkokX; y:=y+SkokY; BufordV:=MoveCount; MoznaIsc(MoveCount); ........ |
Dodatkowo należy omówić procedurę
1 |
Odbicie; |
Procedura ta wyznacza wolne pola z tablicy Drogi przy kolizji z pniem. Z tych pól tworzona jest tablica dynamiczna i z niej losowo je wybierane takie pole poczym zostaje ona zwolniona. Idee działania przedstawię na jednej z wewnętrznych funkcji tej procedury.
Powiedzmy, że uderzenie w pień drzewa (matematycznie w pole siatek dróg) następuje z południa. Działanie funkcji nie będę opisywać. To można prześledzić w poniższych linijkach :
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 |
function zkierunkuS:tKierunek; const {układ: W X E SW S SE} ukalKierunk:array[0..4]of tKierunek=(kierW,kierE,kierSW,kierS,kierSE); t:array[0..4,0..1]of integer=(( 0,-1), (0, 1), ( 1,-1),( 1, 0),(1, 1)); var //tablica mozliwych kierunków tabKier:array of tKierunek; _k,_w:integer; i,r:byte; begin r:=1; setLength(tabKier,r); for i:=0 to 4 do begin _w:=stareWD+t[i,0];if _w<0 then _w:=0;if _w>19 then _w:=19; _k:=stareKD+t[i,1];if _w<0 then _k:=0;if _k>19 then _k:=19; if Drogi[_w,_k]=-1 then begin tabKier[high(tabKier)]:=ukalKierunk; inc(r); setLength(tabKier,r); end; end; //wylosuj dostepny kierunek result:=tabKier[Random(high(tabKier)+1)-1];//[Random(high(tabKier))]; tabKier:=nil; end; |
Pomysł jaki tu wykorzystałem zobrazuje tym rysunkiem:
Polu z iksem jest zajęte przez pień drzewa, BOROSTWÓR nadchodzi z kierunku południowego (strzałka czerwona). Możliwe pola po odbiciu wskazują strzałki niebieski. Teraz wystarczy wyznaczyć wiersze i kolumny takich pól, sprawdzić czy są dostępne do wykonania ruchu , utworzyć z nich tablicę i wylosować takie pole. Proste podejście bo szybciej jest dostać się do indeksów niż wyznaczyć to z funkcji trygonometrycznych. Szybkie wyliczenie indeksów odbywa się w pętli poprzez dodani lub odjęcie liczby 1 (jeden) względem pola przeszkody. Czy dodawać lub odejmować to najłatwiej jest to określić stałymi wartościami przechowywanym w tablicy. Podam fragment z kodu
1 |
t:array[0..4,0..1]of integer=(( 0,-1), (0, 1), ( 1,-1),( 1, 0),(1, 1)); |
Dla ułatwienia ułożyłem ją tak jak na powyższym rysunku. Wyznaczenie możliwych pól po odbici dla pozostałych kierunków są analogiczne.
KOLIZJE Z INNYMI DUSZKAMI
Jest to najciekawsza procedura. Jej powstanie zawdzięczam Iskarowi. Krytyka robi swoje. Tak więc Panowie nie psioczyć na forum…Pomysł na jaki wpadłem jest tak prosty( a to co proste jest genialne, młotek trudno zepsuć i łatwo jest naprawić, bo to prosta , genialna konstrukcja).
Ale do rzeczy…
Przytoczę nagłówek procedury kolizji obiektu TSprite komponentu Omegi
1 |
onCollision(const Sprite:Tsprite;const colX,colY:integer); |
Procedura ta podaje bardzo ważną informację. A mianowicie zwraca współrzędne zderzenia duszka z innym duszkiem. Wynik to colX, colY. Jak ktoś uważa na lekcjach fizyki lub świeci lustrem ludziom po oczach, to wie że kąt padania i odbicia promienia światła jest taki sam i leży na jednej płaszczyźnie.
Świat 2D z natury jest płaski- jeden warunek mamy już spełniony. Kąty obliczymy w prosty sposób (proszę dosłownie nie utożsamiać naszego kąta kierunku odbicia z prawidłowym rachunkiem wynikającym z tego prawa) gdy tylko coś zauważymy.
ALGORYTM
podziel klatkę duszka na 4 części (dwie kolumny i dwa wiersze)
przypisz kierunki z Róży Wiatrów tym częściom według tej kolejności: NE, NW, SE, SW (proszę zauważyć ze w cale nie podałem w prawidłowej kolejności to znaczy NW,NE, SW, SE, zbieg ten należy rozumieć tak: jeżeli uderzenie nastąpiło z kierunku NW to odbicie będzie w kierunku NE)
sprawdź w której części- polu leżą współrzędne kolizji
na podstawie indeksu pola określ kierunek odbicia
Poglądowy rysunek:
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 |
function TStwor.KierunekPoKolizji(const colX,colY:integer):tKierunek; var s,h:integer; kwadrat,K,W:byte;//kolumna i wierzs kwadratu zderzenia begin s:=Image.TileWidth div 2;//wyznacz polowy z serokości i wysokosci klatki h:=Image.TileHeight div 2; k:=colX div s;//podziel na kolumny iwiersze w:=colY div h; kwadrat:=k+w*2;//oblicz kawdrat zderzenia case kwadrat of 0:result:=kierNE;//na podstwaie kawdratu zderzenia wyznacz kierunek odbicia 1:result:=kierNW; 2:result:=kierSE; 3:result:=kierSW; end; end; |
Oczywiście nic nie stoi na przeszkodzi aby zwiększyć podział na 8 kierunków (czyli 9 pól, środkowe jakoś w tedy należy potraktować inaczej lub przypisać mu już raz przypisany kierunek)
ZABEZPIECZEINIE PRZED UCIECZKĄ BOROSTWORA Z MAPY ŚWIATA
Podam najprostszy pomysł dla tego modelu organizacji świata 2D. Proszę zauważyć, że klatki mapy to też duszki. Znamy więc współrzędne ich rogów Wystarczy więc sprawdzać czy współrzędne X i Y borostwora nie wystają poza skrajne rogi mapy świata…Jeżeli wystają w boku zachodnim to zmień kierunek na zachodni itd. A są to te linie kodu:
1 2 3 4 5 |
//najprostszy z mozliwych warunek trzymania stworów w obrebie mapy if x<warstwa0[0, 0].x="" then="" kierunek:="kierE;" <br="">if x>Warstwa0[0,18].x then kierunek:=kierW; if y<warstwa0[0, 0].y="" then="" kierunek:="kierS;" <br="">if y>Warstwa0[19,0].y then kierunek:=kierN;</warstwa0[0,></warstwa0[0,> |
ANIMACJA KLATEK BOROSTWORA
Pod uwagę wezmę jedną z ośmiu funkcji wyznaczających kierunek ruchu. Kierunek ruchu decyduje jakie klatki maja być brane pod uwagę. Jeżeli popatrzymy na plik zasobu grafiki
To zauważymy, że aby iść na wschód to trzeba dostać się do pierwszej klatki z ostatniego wiersza Czyli 9 licząc od zera.
Jak to zrobić?
Nie jest to trudne, trzeba wprowadzić pewien skok w takiej tablicy klatek tu jest to liczba 9 (aby iść na południe to była by to liczba 3) A ze potrzebujemy 3 klatek ruchu w danym kierunku (klatka 0,1,2) to odpowiednio zwiększając taki indeks plus skok wynikający z obranego kierunku mamy gotową animację po klatkach.Wynik wstawiamy do ImageIndex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function TStwor.kierunekE (const V:Single):tKierunek; begin Skok_X:=25*V; ofsetKlatki:=9; result:=kierE; end; IDAnimacji:=IDAnimacji+dVA*Value;//dVA to predkosc animacji if IDAnimacji>2 then IDAnimacji:=0; ImageIndex:=Round(IDAnimacji+OfsetKlatki); |
Stosując odpowiednie ofsety klatek możemy uzyskać różne animacje np animacja strzału, skoku itp. Ważne aby takie klatki były zapisane w jednym obrazie. O prędkości animacji decyduje ten warunek zwiększania licznika:
1 |
IDAnimacji:=IDAnimacji+dVA*Value; |
Wartość dVA ustalona jest w momencie tworzenia duszka, Value jest pobierana z zegara aplikacji gry.Podczas wykonywania kodu gry jest ona zerowana gdy występuje zderzenie z innym duszkiem. Unika się efektu migotania wynikającego z nakładania zmieniających się klatek tych obiektów. Może również posłużyć do przyspieszani a lub opóźniania animacji w zależności od sytuacji na przykład bieg.
Nasze dzikie BOROSTWORY tworzymy tak
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 |
//część piata- tworzenie borostworów randomize; for k:=0 to 49 do begin //losuj wspołrzedne dla borostwoara- typ 1 x:=Random(1100)+50; y:=Random(600)+20; //sprwdz czy w tych wspolrzednych nie ma pnia drzewa if Drogi[y div 32,x div 32]=-1 then TworzBorostwora(x,y,OmegaSprite1,OmegaImageList1.ImageList.Items[9],1); //losuj wspołrzedne dla borostwoara- typ 2 x:=Random(1100)+50; y:=Random(600)+20; if Drogi[y div 32,x div 32]=-1 then TworzBorostwora(x,y,OmegaSprite1,OmegaImageList1.ImageList.Items[10],1); end; |
I będzie ich w najlepszym przypadku 100 sztuk.
Nasz BOROSTWÓR umie wchodzić do karczmy. Proszę sprawdzić. Wystarczy wejść do karczmy i troszkę w niej posiedzieć a na pewno ktoś do nas przyjdzie. I cieszmy się, że nie będzie chciał na piwo.
Pozdrawiam oksal
Zbylitowska Góra 15.05.2006
Autor: oksal