Devlog lamera 4: wrogość przejmująca

BLOG
169V
user-54005 main blog image
Rankin | 21.06, 16:01
Poniżej znajduje się treść dodana przez czytelnika PPE.pl w formie bloga.

Witam witam, w kolejnym devlogu lamera, to jest serii poświęconej demitologizacji gamedevu na praktycznych przykładach, tj. na gierce, którą właśnie tłukę w Godocie. Zrobienie własnej gierki nigdy nie było łatwiejsze, nawet przez ludzi którzy w życiu nie programowali! Wystarczy godzina dziennie pracy nad projektem i można całkiem szybko do czegoś dojść. W poprzednim odcinku pokazałem drogę do w sumie prostego rozwiązania animacji statku, oraz utworzenie hitscanowego lasera i automatycznie celowanego pocisku.

Ponieważ w tym odcinku serii będzie nieco więcej opowiadane o koordynatach itd. wypadałoby nieco przyjrzeć się, jak te rzeczy działają w Godocie. W trybie 2-wymiarowym, bez dłubania z okienkami, kamerami itd., pole gry jest dyktowane rozmiarem okna i piksele okna bezpośrednio przekładają się na koordynaty w logice gry. To znaczy, że skoro w projekcie mam okno 1280x720, to widoczne pole gry ma koordynaty w zakresie od (0, 0) (lewy górny róg) do (1280, 720) (prawy dolny róg). Jeśli coś posiada koordynaty poza tym zakresem, obiekt nadal może funkcjonować, rzecz jasna, po prostu będzie niewidoczny. Dodatkowym ważnym quirkiem jest to, że koordynaty obiektów, jeśli tego na systemie nie wymuszę, nie są liczbami całkowitymi, a ułamkami zmiennoprzecinkowymi w precyzji single. Innymi słowy, obiekt może mieć koordynaty (1.0000001, 1.0000001) i gra będzie go traktowała jako nie będący na (1, 1), mimo że może być rysowany w tym punkcie. Ponieważ dojdzie też trochę obracania, rotacja obieków w godocie natywnie jest w radianach. W tych jednostkach kompletny obrót wokół jednej osi oznacza obrót o . Zakres jest przedstawiony od -π do +π, a krańce te stykają się na płaszczyźnie poziomej,  po lewej stronie okręgu. Jest co prawda możliwość podawania wartości w stopniach, ale wymaga to osobnych metod wywołania, które jeszcze nie wszędzie są obecne i wtedy trzeba dodawać pośredni krok na konwersję.


świat twój przedstawiony

Wracając do tematu faktycznego robienia gierki: statek gracza jest w miarę adekwatnym miejscu aktualnie. Fakt, brakuje paru rzeczy, przede wszystkim właściwej obsługi obrażeń (na razie tylko leci wiadomość debugowa i koniec), ale przyszła wreszcie pora na drugą stronę medalu: przeciwników. Jeszcze z początków pracy nad projektem mam bardzo szkieletowy zalążek skryptu wroga, który tylko leci w lewo i zgłasza kolizje. Przyszła pora go rozbudować oraz rozwidlić na konkretne logiki zachowań.

Zaczynamy tutaj dotykać nieco tematu AI, sztucznej inteligencji i zachowania wrogów. Jest to temat głęboki i rozległy, w którym mało kto osiąga naprawdę dobre wyniki. Na szczęście gra tworzona to skrolowana strzelanka, więc można wszystko zrobić kompletnie zlewając jakieś state machines, oceny sytuacji, wzorce zachowań. Tutaj wrogowie mają się zachowywać prosto i w miarę możliwości przewidywalnie. Ponieważ projekt jest dla mnie również metodą praktycznej nauki Godota, chciałbym również zaimplementować szereg wrogów o odmiennych zachowaniach bazujących na odmiennych metodach. Są to:

  • wróg, który rusza się wyłącznie w lewo, oddając strzał co jakiś czas, nazwijmy go "klatka"
  • wróg, który rusza się w linii prostej, ale w kierunku, który zdefiniuję dopiero przy wrzuceniu go na pole bitwy, może też skręcać, może strzelać w dowolnym ustalonym kierunku, nazwijmy go "szpon"
  • wróg, który rusza się w szeroko pojętą stronę gracza, zatrzymuje i oddaje strzał, nazwijmy go "goniec"
  • wróg, który porusza się wyłącznie z prawej w lewą falistą trasą, co jakiś czas oddając strzał w stronę gracza nazwijmy go "skrzydło"
  • wróg, który emituje pocisk naprowadzany, nazwijmy go "łucznik"
  • wróg, który upuści przedmiot zmieniający/ulepszający broń gracza, nazwijmy go "minivan"

Od razu też wchodzi kwestia wrzucania tych wrogów na planszę. Pierwszy poziom przynajmniej jest zorganizowany tak, że kamera stoi nieruchomo, jedynie tło symuluje ruch, w związku z tym nie mogę po prostu ustawić jednostek na mapie i budzić gdy pojawią się na ekranie. Robię zatem tak, że plansza zawiera skrypt, a w skrypcie tym jest zmienna cały czas powiększana o deltę i zmienna kontrolna. Każdej pętli sprawdzana jest zmienna czasowa; po przekroczeniu progu wywołuję odpowiednie funkcje spawnujące wrogów i zwiększam kontrolną o 1, żeby spawn nie następował co klatkę:

czas_spawnować += delta

if czas_spawnować >= 1.0 and kontrola_spawnu == 1:

spwanuj_trzy_skrzydła(Vector2(1300, 400)) #1300,400 to koordynaty kawałek za prawą krawędzią widocznej planszy, w dolnej połowie ekranu

kontrola_spawnu += 1

Proste? Jak drut kolczasy.

and they don't stop coming and they don't stop coming and they don't stop coming and

Klatka

Absolutnie najtrywialniejszy możliwy wróg do zorganizowania. Główne urozmaicenie wynikało z tego, że atakuje laserem hitscanowym. Oczywiście nie prosto w gracza, to byłoby nie fair, po prostu co x sekund strzela pionowo w górę i dół, jeszcze dając jakiś ułamek sekundy na usunięcie się z pola rażenia zanim laser wywoła uszkodzenia. Ruch to zatem proste position -= transform.x * speed * delta; Co do strzelania zaś to w odpowiednich miejscach postawilem dwa Marker2D i gdy pora strzelić, podpinam na nich na chwilę przygotowany obiekt lasera, zbudowany podobnie do tego, który zrobiłem dla statku gracza. Pamiętać należy, żeby go obrócić w odpowiednią stronę przed aktywacją - i gotowe.

Szpon

Od razu idziemy w nieco bardziej złożone założenia. Szpon musi lecieć w dowolnym kierunku, któy mu wskażę. Dlatego w komendzie ruchu mamy nie transform.x, (płaszczyzna pozioma), a nową zmienną, kierunek, która jest znormalizowanym wektorem 2-wymiarowym. Ponadto wstawiona jest funkcja ustal_kierunek(nadany_kierunek : Vector2), która otrzymuje od skryptu planszy wektor, który zostanie wprowadzony do zmiennej kierunek. Następnie szpon musi strzelać w dowolnym kierunku, niezależnym od kierunku ruchu, a który również ustalę przy wprowadzeniu jednostki na planszę. O ile możliwe byłoby leniwe generowanie pocisku w środku statku i celowanie go gdzie trzeba, zrobiłem inaczej. Tym razem noda Marker2D, ustawiona na końcówce "lufy" statku, bezpośrednio podlega sprajtowi, a nie nadrzędnej nodzie obiektu. To znaczy, że jeśli obrócę sprajt, Marker2D samoczynnie obróci się razem z nim, więc mogę brać jego pozycję oraz obrócenie, po czym przekazywać je wytworzonemu pociskowi. Obrócenie to również ustalam za pomocą funkcji wywołyowanej przez skrypt planszy.

Zostaje jeszcze kwestia zmiany toru ruchu i strzału, prosta rzecz. Po [X] sekundach podmieniam kierunek ruchu oraz obrót sprajta na nowe. Dane te również podaje skrypt planszy w momencie wkładania statku na pole gry. W pętli procesowej odliczam czas i gdy nadchodzi pora zmiany, jedno = drugie i tyle kombinowania.

Goniec

Nieco przekombinowany typ szczerze mówiąc, na którego tuningu spędziłem trochę za dużo czasu. Goniec jako pierwszy posiada bardziej skomplikowaną, autonomiczną logikę zachowania, która przedstawia się tak:

  1. Ruch w stronę gracza (możliwe odchylenie +/- kilka stopni) przez X sekund
  2. Przerwa 0.5 sekundy
  3. Ponowny ruch w obecną stronę gracza przez X sekund
  4. po 0.5 sekundy w bezruchu zostaje oddany strzał w stronę gracza
  5. wracamy do 1

Co więcej, jest 6 osobnych animacji powiązanych z oddaniem strzału, oraz 6 osobnych Marker2D, na których pojawi się pocisk. Jak się zatem zabrać za przełożenie takich założeń na kod? Po pierwsze, szkieletem zostanie połączenie match z enum. Match (aka select aka switch) to instrukcja wyboru, popularna w wielu językach. Bierze ona podany parametr i sprawdza czy jest on równy wartościom zdefiniowanym przez użytkownika; jeśli tak, wykonywany zostaje odpowiedni blok komend. Enum sam w sobie to tylko pomoc koderska, jest to zgrupowana pewna ilość wartości, którym można przydzielić czytelne (haha) nazwy i odnosić się do nich za pomocą tych nazw. Dzięki temu zamiast pytania czy aktualny stan pojazdu to zero, jeden, czy czterdzieści, mogę pytać czy stan pojazdu to STAN.RUCH1, czy STAN.STRZELANIE i tak samo mogę kazać żeby stan_pojazdu = STAN.PAUZA, bez pamiętania czy ta pauza to była wartość 5 czy 3.50, mogę też dodać więcej wartości do tej grupy bez potrzeby przeglądania calutkiego kodu i poprawiania gdzie się mogło zmienić. Funkcjonalność ta sama, ale czytelność i łatwość modyfikacji rośnie potężnie. Wewnątrz każdej kolejnej sekcji match - poza pauzą, rzecz jasna - ustalam w którą stronę jest pojazd gracza i albo przesuwam się w jego stronę gońca (modyfikując kierunek o losową ilość stopni +/-), albo wystrzeliwuję pocisk. Rzeczy które już w większości się przewijały przez kod w wielu miejscach.

direction_vector = self.position.direction_to(_player_ref.position) #gdzie jest gracz?
var check_the_angle = direction_vector.angle() #pod jakim kątem jest gracz?
if (check_the_angle > (-PI/6)) and (check_the_angle < (PI/6)): #prawa 1/6 wycinku koła

$AnimationPlayer.play("att_right") #animacja strzału w prawo
bullet_spawn_marker = $SpawnRight #strzelamy z punktu prawego

elif (check_the_angle > (-PI/2)) and (check_the_angle <= (-PI/6)): #prawa górna 1/6 wycinku koła

$AnimationPlayer.play("att_top_right") #animacja strzału w górę-prawo
bullet_spawn_marker = $SpawnTopRight #strzelamy z punktu góra-prawo

elif (check_the_angle > (-(5*PI)/6)) and (check_the_angle <= (-PI/2)): #i tak dalej w sumie 6 razy

nie matchujesz? to elifuj a jak nie to elifuj a jak nie to elifuj a

Tyle, że objawił się problem, gdy odpaliłem gościa do testów czy wszystko działa i stwierdziłem, że ruch jest cholernie niestetyczny, sztuczny i mechaniczny. Pomyślałem wtedy, że zorganizuję jakieś rozpędzenie i hamowanie typu, nie wiem, wykładniczego wielomianu w funkcji czasu. Dłubałem tutaj kilka dni, próbując to tak, to inaczej, ale wciąż efekt końcowy wyglądał, cóż, niezbyt dobrze; ruch trwał za krótko, żeby moja formuła dawała satysfakcjonujący, czytelny efekt, niezależnie jak mocno dłubałem w cyferkach. Już byłem gotowy zrezygnować z tego pomysłu i zastąpić go zwyczajnym teleportem z A do B, ale wpadłem gdzieś na temat lerpów w godocie, że mogą one symulować "tarcie" przy ruchu. Lerp - to wbudowana w wektory w Godocie liniowa interpolacja. Bez wdawania się w szczegóły, lerp() to funkcja biorąca obecny wektor, wektor docelowy, oraz "wagę"; zwraca wektor będący gdzieś pomiędzy obecnym a docelowym. Tendencja jest taka, że im większa różnica, tym szybszy ruch. Oczywiście, jak zwykle, są dwa problemy do pokonania. Po pierwsze, jeśli od razu ustawię punkt końcowy, pojazd zacznie szybko i będzie tylko spowalniał, a ja chcę żeby najpierw przyspieszał, a potem hamował. Ten problem rozwiązałem tworząc wirtualny punkt, do którego goniec dąży. Punkt zaczyna na pozycji gońca, po czym rusza się ze stałą prędkością do punktu kończącego ruch. Póki się rusza, statek przyspiesza, a gdy staje, statek zaczyna hamować. Trochę tweaku parametrów i miałem efekt dokładnie taki, o jaki mi chodziło.

zwykły liniowy ruch vs lerp na nim osadzony

Drugim problemem jest to, że czekanie aż interpolowany ruch dojdzie docelowej współrzędnej może trochę... potrwać. W całkiem nowoczesnej interpretacji paradoksu Zenona interpolowany ruch jest tym wolniejszy im bliżej celu, a ponieważ współrzędne są przechowywane w liczbach zmiennoprzecinkowych, może się bardzo bardzo długo zbliżać zanim wreszcie przeskoczy ostatnią całkowitą i ostatni piksel. Przejawia się to tym, że jeśli kod czeka, az pozycja będzie się == wartości docelowej, w zależności od "wagi" podanej w lerpie może się pozornie nic nie dziać przez nawet ładne kilka sekund. Trzeba sobie z tym radzić jakoś; ja zdecydowałem, że gdy różnica między obecną pozycją a celem będzie mniejsza od jakiejś arbitralnej wartości (np. 1.0), nastąpi przeskok na pozycję docelową i przejście do następnej fazy ruchu.

ANTRAKT

albo: Pulp Fiction i kule

Podczas długich testów i dłubania w gońcu zaczął wychodzić dość martwiący problem. Niektóre pociski przechodziły przez niego na wylot, bez wykrywania trafienia i zadawania obrażeń. Po nawet dość wstępnej analizie okazało się, że działo się tak zwyczajnie dlatego, że ruch w grze występował nie jako logiczne przesuwanie czegoś na planszy, a teleportację z A do B. Powodów jest wiele, wszystkie można określić mianem wspólnym - "wydajność". Chodzi o to, żeby nie pakować nie wiadomo ile operacji w każdym jednym cyklu gry bo się zaraz to odbije na procesorze, wydajności i zacznie się klatkowanie. Niestety - powód nie powód, niektóre pociski były dość szybkie, a goniec konkretnie na tyle wąski, że zdarzało się sporo sytuacji gdzie w jednej klatce pocisk był tuż przed wrogiem, a w następnej - już kawałek za, więc zupełnie logicznie trafienia nie było: w ani jednej, ani drugiej klatce nie nastąpiło dotknięcie hitboxa.

to musiała być boska interwencja

Co można z takim problemem zrobić? Well, można by spowolnić kule. To psułoby dynamikę i balans (nie żeby jakiś intencjonalny występował w tej chwili). Można by też powiększyć hitbox, ale to by znowu powodowało sytuacje, gdy pocisk wyraźnie nie powinien był trafić wroga, ale jednak trafił. Mógłbym też wrócić do starego systemu fizycznego move_and_collide() z pierwszych podejść do kolizji, ale nie wiedziałem czy dam radę go tak nagiąć, żeby dobrze i bez artefaktów symulował pożądany efekt.

efekty mogą być różne

Zadecydowałem zatem podejść do problemu w nowy (dla mnie, bo to też znalazłem online) sposób, to jest shapecast. Shapecast to bardzo bliski krewny raycast, który wykorzystałem przy laserze. Różnica jest taka, że zamiast kolizji punktowej sprawdza, czy w kolizję wejdzie zdefiniowany przeze mnie kształt. Nowe działanie pocisków jest zatem następujące: po pierwsze, zmieniam typ głównej nody pocisku z Area2D na Node2D i kasuję CollisionShape2D żeby nie dublować funkcjonalności. Następnie dodaję nodę ShapeCast2D. Wprowadzam do niej kształt odpowiadający widocznemu kształtowi pocisku. W kodzie, zamiast po prostu wyświechtanego position +=, każdej klatki sprawdzam za pomocą shapecasta, czy po drodze do punktu docelowego odległego o, a jakże, speed*delta,  nie występuje coś, z czym pocisk by kolidował. Jeśli tak - czy to wróg. Jeśli tak - zadajemy obrażenia. Pocisk zostaje skasowany. Jeśli po drodze nie ma nic, dopiero wtedy przenosimy kulę na nową pozycję. System działa wcale nieźle. Przy okazji stwierdziłem też, że gra wyglądałaby ładniej w większym klatkażu, a że i tak wszędzie w obliczeniach biorę deltę na poprawkę. Biorąc pod uwagę, że gra wzięła przypadkowo wygenerowaną nieskończoną ilość skrzydeł na klatę bez przesadnego dławienia się, bez specjalnego opporu wykopałem prawie wszystkie kalkulacje z _physics_process do _process, co znaczy, że aktualizacje pozycji obiektów nie będą 60 razy na sekundę, a tyle, ile system podoła. Ciekawe, czy w przyszłości nie będę zmieniał tego z powrotem.

 

var movement = transform.x * speed * delta #so where we droppin boys
var target_hit
_ray.target_position = movement #promień rzucamy do obliczonego celu
_ray.force_shapecast_update() #sprawdzamy TERAZ, nie kiedyś, TERAZ
if _ray.is_colliding(): #w coś trafiliśmy?

target_hit = _ray.get_collider(0) #patrzymy na pierwszy trafiony obiekt
if target_hit.is_in_group("enemies"): #czy to wróg?

target_hit.hit_by_player_bullet(2) #to wrog - zadajemy obrażenia
queue_free() #pocisk znika

else: #w nic nie trafiliśmy

position += movement #to pocisk leci dalej

 

Skrzydło

Naczelną cechą skrzydła jest to, że miało stabilnie wędrować po falistej linii. Technicznie mógłbym w tym celu utworzyć jakiś skomplikowany kod obliczający odchylenie od płaszczyzny na podstawie czasu albo inne sin(x). Od początku jednak planowałem zaimplementować zamiast tego metodę Path2D. P2D jest to noda zawierająca szereg punktów, z których generowana jest trasa. Potem można poprosić nodę o postęp na niej, procentowy (30% trasy) albo liniowy (40 jednostek od startu), a ona samoczynnie ustawi rzecz na odpowiednich współrzędnych (x, y). Przy tym ustawienie sztywnej ścieżki nie jest jej jedynym zastosowaniem, ot chociażby godotowy tutorialowy projekt gierki wykorzystuje P2D aby generować losowe miejsce spawnu mobów.

Wymagało to będzie nietypowego podejścia do konstrukcji statku jako gotowego obiektu. Dotąd prawie każdy obiekt był u korzeni obszarem (Area2D) lub ew. ciałem (CharacterBody2D), zgodnie z godot style guide sugerującym żeby root danej sceny reprezentował, czym ona logicznie ma być (np. obszarem kolidującym z pociskami/graczem). Tym razem nie mogę tego zrobić, gdyż normalne działanie P2D jest takie, że podlega mu drugi specjalny obiekt, PathFollow2D który dopiero zawiera potrzebną pozycję na trasie. Co więcej, nie mogę ustawić Patha jako osobnego obiektu na planszy, bo raz po co ma wisieć bezczynnie kiedy nic z niego nie korzysta, a dwa nie chcę stawiać 20 ścieżek z których każda byłaby użyta tylko raz, a trzy po co sobie dokładać jeszcze śledznie czy path użyty czy nieużyty czy kasować czy nie. Konstrukcja sceny statku jest zatem taka:

Path2D //ścieżka

└ PathFollow2D //położenie na ścieżce

└ Area2D //statek

└ sprite, hitbox, itd.

Na dodatek rdzenny skrypt sterujący będzie aż dwa poziomy poniżej korzenia. Powód jest dość prosty, zaoszczędzi mi to bardzo dużo pisania $PathFollow2D/Area2D/Sprite.cośtam $PathFollow2D/Area2D.global_position itp. itd, gdyż jedyne co P2D będzie robiło to istnienie. Oczywiście zanim będzie istniało, trzeba siąść i ustalić tę ścieżkę. To było trochę trudniejsze. Ścieżka jest przechowywana jako krzywa Beziera i jako taka zawiera trochę więcej parametrów niż można by się spodziewać, które działają trochę nieintuicyjnie, przynajmniej dla laika (mnie). Specyfika jest taka, że nie ustalam każdy jeden milimetr trasy, nie rysuję myszką żadnych krzywych, tylko określam szereg punktów, powiedzmy orientacyjnych. Co więcej, jeśli chcę, aby trasa była płynną krzywą, a nie grupą zygzaków o kształcie zębów piły, muszę podawać dodatkowe parametry, "ease in" i "ease out", które opiszą, jak zostanie wygięty odcinek przed tym punktem i po tym punkcie. Na moje cele musiałem metodą prób i błędów wypracować jakiś rytm, który by się powtarzał, aż będę miał trasę dłuższą w poziomie niż szerokość ekranu w domyślnej rozdzielczości (1280 pikseli). Punkty główne zaś nie są w koordynatach względnych, lecz absolutnych, więc musiałem po prostu ręcznie obliczać, że nowy punkt główny będzie 200p w lewo od poprzedniego, ease-in będzie (-50, +100), etc. etc. Trochę klepania było, ale efekt końcowy wyszedł nawet niezły, ładna sinusoida.

trasa vs punkty ją tworzące

 

Gdy ścieżka jest ustalona, PF2D siedzi, Area2D siedzi, jesteśmy już w domciu praktycznie. Tyle tylko co zamiast position += kierunek * delta * speed, pytamy PF2D o progres na podstawie, a jakże, delta * speed. Nie obyło się oczywiście bez tarć. Najpierw miałem ambicję delikatnie zmieniać szybkość poruszania się co jakiś czas, dla urozmaicenia. Wyglądało to jednak niezbyt dobrze gdy czasem drugi statek wyprzedzał pierwszy w formacji, bo tak się dłuższą chwilę losowało. Kombinowałem też jak w kodzie zrobić żeby statek zwalniał przy "garbach" trasy, ale okazało się, że to naturalnie wychodzi z krzywych beziera na pathu. Ostatni tweak był taki, że queue_free(), czyli komendę kasującą i sprzątającą gdy już wróg zostanie zniszczony albo wyleci za ekran, trzeba było wywołać na właściwym obiekcie. Jeśli bowiem zawołałbym q_f tam gdzie jest skrypt przypięty, czyli pod Area2D, skasowane byłoby wszystko niżej, a P2D siedziałby sobie wesoło dalej, bezczynne i tylko zajmujące pamięć, dopóki całej planszy nie wywalę po ukończeniu. Niby nie jest to jakiś kosmiczny wyciek pamięciowy, ale po co wyrabiać sobie kiepskie nawyki. Co prawda gdy to klepałem było akurat dość późno w nocy, więc z jakiegoś powodu zamiast wprost się odwołać np. owner.queue_free(), specjalnie w Path2D postawiłem osobny skrypt, kompletnie pusty poza jedną funkcją, wyrzucaj(), zawierającą jedną komendę, queue_free(), która ta komenda była wywoływana gdy trzeba z poziomu A2D. Nie wiem o co mi chodziło, chyba o pewność że jak się pogubię albo pozmieniam kto jest właścicielem czego to nie zostanie wywalona ani cała plansza, ani PathFollow, ani jeszcze co innego niepożądanego - wszystkie te obiekty nie mają komendy wyrzucaj() więc jak coś będzie nie tak to się po prostu wywróci do pulpitu zamiast cicho coś usuwać.

programowanie w jednym obrazku

 

Łucznik

Clou tego statku jest jego pocisk. Pocisk jest samonaprowadzający; żeby jednak nie było unfair, posiada szereg ograniczeń. Przede wszystkim naprowadzanie działa na zasadzie ograniczonego obracania; to jest zamiast automatycznie zawsze ustawiać pocisk w kierunku gracza, następuje stopniowe obracanie we właściwym kierunku. Ponadto naprowadzanie trwa tylko kilka sekund, po czym pocisk leci w prostej, aż wyleci poza ekran. Ponadto w ramach wizualnego picu strzałka po pojawieniu się na polu miała się obracać aż wskaże na gracza, zatrzymać na moment i dopiero wtedy wystrzelić. Niestety ten drugi pomysł okazał być się trochę nierealny; znowu rozbijałem się o problem paradoksu Zenona, to jest zdecydowania między deltą a skokiem kiedy strzałka jest dostatecznie wycelowana (o idealnym celu nie ma co myśleć bez odgórnego narzucenia kierunku) żeby strzelić, a na dobitkę przy prędkości wirowania dostatecznie niskiej by ten efekt miał jakiś sens, było dość łatwe aby statek gacza kręcił dosłownie kółka wokół wroga, znacznie opóźniając moment wystrzału, jeśli w ogóle go nie blokując. Drugie podejście zatem było prostsze, strzała zaczyna wycelowana w prawo, obraca się 180 stopni i wtedy odpala. Cała reszta to po prostu kwestia pilnowania gdzie jest gracz i decydowania w którą stronę obrócić strzałkę żeby w niego kierować. Piszę "tylko", ale tutaj był cały pies pogrzebany! Szybko wyłoniło się dziwaczne zachowanie, pocisk ładnie się kierował chwilę, po czym nagle mu odbijało i zaczynał kręcić wywijasy w kompletnie przeciwną stronę.  O co chodziło?

Ano o to chodziło, że uruchomiło się parę quirków silnika godot. Po pierwsze, jak wspomniałem na początku wpisu, wartość obrotu danego obiektu działa w radianach, w zakresie od -π do +π (jeden pełny obrót to zawsze 2π radianów) , a po lewej stronie (kierunek wektorowy (-1, 0)) znajduje się przeskok między tymi wartościami. To znaczy, że za każdym razem gdy mijiałem pocisk po jego lewej stronie, nagle obliczenia wskazywały że gracz jest łomatko odległy o prawie 2π w przeciwną stronę niż dotąd, trzeba szybko kręcić w przeciwną stronę!!! Co gorsza, o ile zakres nominalny jest od -π do +π, to silnik spokojnie przyjmuje na klatę wartości znacznie większe w obu kierunkach, co może mieć miejsce gdy np. obrót obiektu nie jest ustawiany na sztywno, ale modyfikowany +wartość/-wartość. W związku z tym musiałem napisać specjalną funkcję. Funkcja ta powie czy się obracać w lewo, czy w prawo, na podstawie podanych jej kierunków początkowego i docelowego. Ażeby mogła tego dokonać, trzeba po pierwsze odciąć wszelkie potwórzenia obrotu wynikające z problemu nr 2 (modulo z 2π), sprawdzić który kierunek odwrótu wychodzi z kalkulacji, a następnie, jeśli różnica między punktami jest większa niż półkole (π), to znaczy jeśli właśnie przekroczyliśmy lewą granicę, kierunek zostaje odwrócony. Teraz wreszcie wszystko działa jak należy.

Minivan

Minivan to wróg, owszem, ale jego naczelną funkcją jest dropienie upgrade / zmiany broni. Żeby się nim zająć, musiałem wrócić do dłubania w kodzie statku gracza, oraz wreszcie wprowadzić pełnoprawny interfejs. O tym - w następnym odcinku...

Oceń bloga:
6

Komentarze (1)

SORTUJ OD: Najnowszych / Najstarszych / Popularnych

cropper