Devlog lamera 8: ścieżki ponownie przemierzane
Ayo! Witam w nowym odcinku devlogowy. Poprzednio mówiłem o klepaniu różnych obiektów interfejsowych oraz systemu dron wspierających gracza.
Mając zatem proof of concept tego, że drony mogą się zachowywać tak, jak to sobie wymyśliłem, siadłem do aseprite znowu i w przeciągu nwm jakoś godziny max wysmażyłem wcale zgrabny animowany obiekt:
moar herta (powiększenie x3)
Docelowo niebieski element będzie zmieniał kolor w zależności od aktywnej broni. Wykrystalizowane założenia systemu dron były zatem takie:
- naraz aktywne będzie 0-2 dron
- drony strzelają bronią tego samego "typu" co statek gracza, ale o nieco innych właściwościach (idea jest taka, że mają też nieco ratować w przypadku utraty mocy)
- drony i ich upgrade będą dropione niezależnie od powerupów i tarcz w określonych punktach gry
- drony blokują pociski
Stworzyłem zatem nową scenę bazowaną na Area2D (hitbox), przekleiłem odpowiednie elementy skryptu proof of concept drony, wpiąłem gdzie trzeba i zacząłem testować blokowanie. Jak na złość kompletnie nie działało poza tym jedym wrogiem który dla odmiany wywalał całość do pulpitu. Okazało się że wrogie kule patrzą na obiekty typu Body, a drona jest typu Area, poza tym jednym co crashował bo to był laser i używał raycastu do detekcji. Podopinałem dodatkowe sygnały, blokowanie działa... tylko przy dłuższym teraz testowaniu wyszedł konkretny problem. otóż drony obracają się zgodnie (no, w drugą stronę, ale zgodnie) z ruchem gracza. Tylko że przy specyficznych, ale częstych, obrotach nie obracały się dokładnie tam, gdzie powinny, lądując 1-2 stopnie odchylone od docelowego kąta, a to nie dość, że było bardzo widoczne na większym sprajcie, to jeszcze zauważalnie odchylało trajektorię emitowanych pocisków.
(addendum: w sumie skoro będę miał klawisz blokowania ruchu dron to może lepiej żeby drony celowały zgodnie z ruchem gracza, a nie odwrotnie? hmmmmmmm)
Ofc praktycznie od razu wiedziałem, w czym problem: uderzył mój stary nemesis: floating point. Kto chce, może sobie poczytać głębiej, ale zasadza się wszystko na tym, że o ile liczby całkowite w systemie binarnym są reprezentowane dosłownie i dokładnie, to ułamki są ogromnym problemem. Najpopularniejszym kompromisem w rozwiązywaniu tego problemu jest format floating point. Niestety ma on konkretną wadę: jest tylko przybliżeniem. Dla wielu liczb wystarczającym, ale w realiach bardziej dokładnych i częstszych obliczeń, na przykład takich jakie są potrzebne do płynnego obrotu obiektu o π radianów w 60 krokach, bardzo szybko wychodzi bardzo złośliwa rzecz zmiennoprzecinkowości: gdy każesz komputerowi dodać 15 dziesięć razy, zawsze ci wyjdzie 150. Ale gdy każesz dodać 0.015 dziesięć razy, cholera wie co wyjdzie: może 0.1501, może 0.149999999999. I ten brak precyzji propaguje się wszędzie dalej w obliczeniach.
Co więcej, identyfikacja problemu nie oznacza jego rozwiązania. Parę dni motałem się między różnymi próbami, mówiąc wprost, zasłonięcia sprawy. Różne dziwne checki, ustalenia typu 'jeśli jest bardzo blisko to już ustaw na koniec', inne metody ustawiania i żognlowania cyferkami. Nic nie pomagało. W desperackim grzebaniu po (średnio pożytecznych tbh) źródłach godotowych wpadłem na dziwny (dla mnie) tip: interpolacja linowa - lerp - z której korzystałem przy niektórych ruchach, nie musi być wyoływana z poziomu pętli silnikowej _process(delta)
. Mogę ją odpalić z dowolnej funkcji, wywołać raz i sama wykonach ruch czy obrót dokładnie na wskazane koordynaty. Co więcej ma swój odpowiednik 1:1 dla rotacji obiektów. Wyciąłem więc obrotową logikę z _process i wkleiłem do funkcji dawaj_obrót, gdzie jedyne co się działo to self.rotation = lerp_angle (skąd, dokąd, szybkość)
.
Działa.
Uf.
Chwilowo miałem dość kodowania więc zostawiłem wykończenie implementacji dron na później. Zamiast tego zwróciłem się w innym kierunku: z dawna odkładany rework sprajta gracza. Sprajt ten, dość siermiężny, był jedną z pierwszych rzeczy jakie wysmażyłem do gry. Na tyle dawno, że był jeszcze wstępnie robiony nie w aseprite, a w kricie śmiesznymi pędzlami pikselowymi. Umiałem wtedy tyle co nic, nawet nie miałem konkretnych ustaleń kolorystyczno-paletowych. Żeby zrobić nowy sprajt, ale trzymający się trochę kształtu starego, zrobiłem zatem coś takiego: otworzyłem plik ze starym sprajtem w ase, dołożyłem na wierzch nową warstwę i zacząłem na niej niebieskim kolorem protoypować nową bryłę statku tak, żeby choć mniej-więcej pokrywała się z obecną przynajmniej co do ogólnych proporcji. Dłubałem i dłubałem aż w końcu było doś obiecującego, wtedy dodałem jeszcze jedną warstwę, na skrzydło, które rysowałem w kontrastowej pomarańczy. Jakiś czas retuszowałem to jedną, to drugą warstwę, aż w końcu było coś co mi się zaczynało podobać. Wywaliłem wtedy warstwę z oryginalnym rysunkiem i miałem gotową bazę na nowy statek:
Przejście z tej bryły do pełnoprawnego statku zajęło mi względnie sporo czasu - dobre cztery-pięć godzin coś, dłubania, gapienia i retuszowania i dłubania i gapienia i tak dalej. Typowo pewnie dla animowanych obiektów sprite jest złożony z czterech warstw; "tylne"/lewe skrzydło, kadłub, stabilizator i "przednie"/prawe skrzydło. Dzięki temu sporo mogłem sobie uprościć animację poprzez przesuwanie całej warstwy względem reszty rysunku. Niemniej dorobienie dalszych sześciu klatek obrotu (po trzy na skrzydło) zajęło mi chyba coś z sześć godzin trudu i znoju i klęcia pikseli i perspektywy i bicia się z impulsem dawania nowego koloru na każdy jeden poziom detali. Mimo to po +/- 10 godzinach roboty efekt końcowy bardzo mi się podobał:
just wiggle a bit
Nie jest idealny (widać pewną deformację skrzyła na ekstremum), ale na razie lepiej nie umiem. Możliwe że będzie jeszcze retusz. Na razie starczy.
wtedy / teraz (10 miesięcy różnicy)
Potem przerysowałem animację wydechu silnika żeby była nieco normalniejsza i mniej kreskówkowa niż mryganie 30 FPS, tym razem na odważnie bo nawet jest trochę pikseli przezroczystych, hehe. Nieco później wreszcie kompletnie odświeżyłem sprajt wroga dropiącego upgrade broni. zachowałem pozycję "działa", "luku" i silników, ale jako całość został zrobiony od nowa w stylu i palecie która się niejako ustaliła na większość przeciwników:
Myślałem też nad retuszem paru innych sprajtów w tym srebrno-niebieskim stylu, ale efekty były średnio obiecujące, zresztą sobie taką dwoistość internalnie uzasadniłem i styknie. Mając to opracowane wykończyłem obiekty odnawiania tarczy, dropienia dron, mrygający napis <ASSIST wskazujący że zaraz jedno czy drugie nadleci z lewej strony ekranu. Następnie zrobiłem funkcję, która będzie na ekran wrzucała tekst jaki jej podam (plus pozycja, kolor itp.); tekst pojawi się z "rozbłyskiem" i po określonym czasie sam zniknie. Być może jeszcze będę dodawał dodatkowe bajery ale może lepiej nie przesadzać żeby nie musieć wrzucać 10 parametrów za każdym razem. Dla testów użyłem go pokazania nazwy planszy na początku, o tak:
(kolory i tekst czysto poglądowe)
Teraz pora na kolejny etap: wyważenie. Dotąd większość cyferek była wrzucona na zasadzie 'eee styknie". HP wrogów, dmg broni, częstotliwość strzałów, punkty, wszystko było pi razy drzwi tylko po to, żeby sprawdzić, czy rzecz działa w ogóle. Teraz jednak musiałem całość przemyśleć i choć pobieżnie dopasować, żeby nie było za łatwo, ani za trudno i wszystko miało jakieś ręce i nogi, szczególnie jeśli chodzi o system trzech broni. Z jednej strony fajnie jest jeśli upgrade jest zauważalnie mocniejszy, z drugiej strony wrogowie później powinni być nieco twardsi żeby to zrekompensować, z trzeciej strony lepiej z tym nie przesadzać bo moc broni może spaść... Łamałem sobie nad tym głowę kilka dni i ostatecznie wykształcił się taki balans:
- niebieskie pociski są szybkostrzelne i dają w miarę dobry DPS w linii prostej
- czerwone pociski mają większy dmg/pocisk i większy DPS, ale nadal są rozproszone i mniej szybkostrzelne
- laser ma najmniejszy DPS - ale będzie przebijał wrogów na wylot
Największa zmiana dotyczy lasera, miałem tam fancy trik łączący raycast z długością linii, emisjami cząstek itd., który niestety idzie do kosza chwilowo (większość wykorzystam ponownie do dron). Zamiast tego - pasek Area2d szerokości ekranu i grubości sprajta lasera. Myślałem nad kombinowaniem nad regulacją długości obszaru żeby nie wybiegał poza ekran zbytnio ale na razie to zbędna robota i zbędne obliczenia bo poza ekranem i tak nic nie ma (wrogowie spawnowani są na krawędzi). Jeszcze tylko mała zmiana w kodzie: ponieważ obszar z zamysłu będzie dotykał wielu rzeczy naraz, nie patrzę na jeden dotknięty cel, a na całość, którą przeczesuję klasycznym for i in trafieni:
Drony zaś będą działały z grubsza podobnie jak normalna broń, za wyjątkiem laserowej - ta będzie laser odpalała krótkimi, ale znacznie mocniejszymi impulsami, które po staremu nie przebijają na wylot, żeby ułatwić życie przy bossach.
Co dalej? Lista następnych kroków jest coraz mniejsza; jeszcze trochę konstrukcyjnego kodu co do dron zostało, boss końcowy i zdobienie planszy. Plus oczywiście łatanie dziur na bieżąco. Boss jest bodaj najgorszą rzeczą - mam konkretną wizję i pomysły, ale będą one wymagały sporo rysowania, nawet jeśli oszukam system np. dając niektóre tranzycje w bardzo małej ilości klatek. Być może przyjdzie nieco zrewidować pomysły żeby się za głęboko nie wkopać na tym etapie. Z drugiej strony boss nie blokuje mi w niczym pracy nad układaniem planszy, więc tutaj powinny być przynajmniej postępy konkretne.
o/