Devlog lamera 7: hydrościnanie
Witam witam w nowym devlogu.
Ostatni odcinek był już parę miesięcy temu i miałem już komponować pierwszy poziom więc na pewno postępy są ogromne, prawda?
Prawda?
eee
Well cóż nie zaprzeczam że ładne kilka minut poziomu jest już ułożone. Problem w tym, że - z tego co słyszę standardowo - im więcej robię i testuję tym więcej ujawnia się rzeczy, które potrzebują jeszcze czegoś które potrzebują jeszcze czegoś które potrzebują jeszcze czegoś które
Co więc udało się faktycznie zrobić? Well, po pierwsze wyrysowałem cały tileset kaflowy, który miał symulować bazę, przez którą trzeba by przelecieć, R-Type style, ale po dłuższych rozważaniach zrezygnowałem z tego pomysłu (kafle będą wykorzystane, ale dopiero dużo później w grze). Następnie wrzuciłem do gry wielki statek specjalny, który będzie pojawiał się chwilowo w trakcie planszy, spawnował "rakiety" i ostrzał, po czym odlatywał, a na koniec będzie służył jako finalny boss. Starając się dać każdej nowej rzeczy jakiś unikatowy element, który trzeba będzie zaimplementować nowym sposobem, tutaj główna niespodzianka jest w "rakietach". Są one traktowane jak statki, tj. można je zniszczyć, ponadto działają dwuetapowo: po pierwsze, są "spuszczane" z otwartej komory, następnie celują w gracza i lecą prosto dość szybko. Do tego żeby było weselej wizualnie, pocisk wiruje wokół osi wolniej przy spuszczaniu i szybciej przy locie. Do tego tego, zamiast jakiejś eksplozji, rozpryśnie się w (nieszkodliwe, bez przesady) metalowe odłamki. Wygląda na to, że już w miarę sprawnie idzie mi godotowanie, bo implementacja wszystkiego była względnie szybka, tylko trzeba było wyrysiować assety, co było mniej szybkie, ale nadal o niebo lepiej niż gdy zaczynałem w lutym.
more hert... czekaj nie to
Mając już dwóch różnych bossów (wieloelementowego midbossa o którym napomykałem wcześniej i tego tutaj) zacząłem myśleć że kurde gdzie są tradycyjne gatunkowe fanfary przed bossem? No i poza tym generalnie przydałby się mały hint interfejsowy jeśli wrogowie będą lecieć z niestandardowych stron (a będą). Zaprojektowałem więc i zaimplementowałem w toku kilku dni scenę-alert, wyposażoną w adekwatną syrenę, pulsującą czerwień i kilka linijek obligatoryjnego engriszu. Generalnie alert przekazuje nazwę nadchodzącego bossa i co należy robić (unikać ostrzału aż se poleci czy zniszczyć). Z mojego punktu widzenia najciekawsza była animacja tekstu - zamiast po prostu pojawiać się znikąd, tekst wypala się na biało, po czym "stygnie" do docelowej czerwieni. W alercie jest to robione "z ręki" poprzez AnimationPlayer płynnie modyfikujący parametr modulate
, ale na TODO obecnym jest podobna wersja, generalnie pojawiającego się tekstu - operowana automatycznie z kodu.
W międzyczasie dla odprężenia dodałem animowany tekst do powerupa, sugerujący co należy z nim robić (dotknąć żeby zabrać lub strzelić żeby zmienić drop). Tekst animowany (SHOOT/TOUCH) wyrysowałem z ręki i w sumie spodobał mi się na tyle, że na jego bazie wyrysowałem całą czcionkę 9x13. Kosmiczną, sci-fi, generyczną jak diabli, ale własną, co zawsze jest wartością dodaną.
How it's started / how it's going
9x13 co prawda to trochę mało na nowoczesne rozdzielczości, więc podwoiłem rozmiar w aseprite. Najpierw skorzystałem z opcji skalowania jakimś algorytmem, ale najwyraźniej nie nadawał się on do takich rzeczy, gdyż efekt był dość... zdeformowany...
coś ktoś dodał do magicznego napoju?
... więc nie miałem wyjścia: dałem powiększenie całego obrazu x2 bez żadnego skalowania, po czym w znoju i trudzie przyjrzałem się każdej literce z osobna i dodawałem detale, wygładzałem, zawężałem, poszerzałem tak, aby miało to wszystko jakiś sens. W końcu 18x26 było gotowe:
Niestety próba praktycznego zastosowania tej czcionki szybko ujawniła naczelną jej wadę: wersaliki (wielkie litery) po przeskalowaniu wyszły wcale nieźle, ale miniskuły (małe litery) są kompletnie do dupy: nierówne i gryzące się jedna z drugą. Jedyny sposób na ten problem to przerysowanie i unifikacja wszystkich miniskuł, a mi i tak zeszło sporo czasu na ten skok w bok, więc obejdę problem po prostu nie używając małych liter w tekście pisanym wersją 18x26.
eugh
Mając ten temat zamknięty, przynajmniej na jakiś czas, przyszła pora na alert kierunkowy, który wymagał chwili główkowania. Idea jest taka, że pojawia się tekst WARNING, a wokół niego 1-8 strzałek wskazujących skąd zagrożenie. Pozornie sprawa trywialna... Problem jest w jasnym i czytelnym przekazaniu przez reżysera planszy (szumna nazwa na skrypt patrzący na stoper i odpalający kolejne spawny) do funkcji alertu, które konkretne strzałki mają się zapalić. Można na przykład za każdym wywołaniem funkcji przekazać osiem argumentów: zapal_alert(tak, tak, tak, tak, nie, tak, nie, nie)
, ale no kurde bonk kto będzie spamiętywał który bool na który kierunek ma być? Przeczesując pamięć przypomniała mi się starożytna (choć nadal w użyciu) metoda z czasów gdy każdy bajt pamięci się liczył, przy której zastosowaniu bardzo fajnie (dla eleganckiego "pełnego" bajta, choć to w sumie nie ma znaczenia) się składa fakt, że akurat potrzebuję ośmiu kierunków, a bajt ma osiem bitów. Pierwszy etap: enum definiujący osiem kierunków głównych i pośrednich, jak na kompasie: północ otrzymuje wartość 1 (20 = 1), północny wschód 2 (21 = 2) itd:
enum COMPASS {N = 1, NE = 2, E = 4, SE = 8, S = 16, SW = 32, W = 64, NW = 128}
Drugi etap: nowa scena, alertowa. Tekst WARNING pośrodku ekranu. Wokół tekstu wrzucam osiem strzałek rozmieszczonych w miarę estetycznie na elipsie... albo przynajmniej na jakiejś symetrycznej krzywej wokół napisu. Każda strzałka mruga samoczynnie i jest nazwana zgodnie z kierunkiem który wskazuje. Po upewnieniu się że wyglądają dobrze, wszystkie strzałki chowam. W skrypcie sceny wrzucony jest powyższy enum, a następnie tworzę dictionary
, to jest w miarę nowoczesny myk programistyczny łączący parami pozycje enuma z odniesieniami do indywidualnych strzałek:
var BIND_ARROWS = {
COMPASS.N: $N,
COMPASS.NE: $NE,
COMPASS.E: $E,
COMPASS.SE: $SE,
COMPASS.S: $S,
COMPASS.SW: $SW,
COMPASS.W: $W,
COMPASS.NW: $NW
}
Na co taki myk? Otóż dzięki temu zaoszczędzam sobie trochę żmudnego, brzydkiego switchowania (to nie 1990 rok dziadku, teraz można pętlić sobie po dowolnych rzeczach np. słownikach). Mając te rzeczy mogę utworzyc clou rozwiązania: funkcja, której mówię zapal_strzałki(COMPASS.N + COMPASS.S +...)
, która, cóż, zapali strzałki zgodnie z moją wolą. Jak? Ano tak: każdy kierunek wyszczegółniony w enumie zapala unikatowy bit; nie ma pokrywania się wartości, więc każda dowolna kombinacja kierunków będzie miała unikatowy zestaw bitów, zbity w jedną liczbę zakresu 0-255. W funkcji zapalającej robię iterację po słowniku i problem z głowy:
for go_over in BIND_ARROWS:
if go_over & argument_funkcji != 0:
BIND_ARROWS[go_over].visible = true
Śmieszne trzy linijki, jak one działają?
Linijka 1: Komenda for
idzie po każdej pozycji słownika BIND_ARROWS
. Krok po kroku, z każdej pozycji brana jest pozycja lewa (czyli wartość COMPASS.cośtam
) i przypisywana do zmiennej go_over
.
Linijka 2: gdy mamy zmienną go_over
z przypisaną wartością, jest ona przyrównywana bitowo do przekazanego argumentu funkcji. To jest ten operator &
- zwraca on sumę wszystkich pokrywających się bitów między lewą a prawą liczbą. Jeśli jest pokrycie, czyli wynik jest różny od zera, wykonujemy linijkę trzecią.
Linijka 3: skoro jest pokrycie w danym kierunku, to znaczy, że strzałka musi być zapalona. Patrzymy do słownika BIND_ARROWS
, podajemy mu naszą obecną zmienną go_over
i w odpowiedzi otrzymujemy prawą pozycję, czyli odniesienie do konkretnej strzałki-obiektu ($N, $NW
itd.). Flaga visible
wbudowana we wsystkie graficzne obiekty godota zostaje ustawiona na true - strzałka pojawia się! Przynajmniej w teorii...
...Bo w praktyce wyszło parę komplikacji. Przede wszystkim już od razu cały projekt zaczął się wywalać na twarz po uruchamianiu. Godot upiera się że BIND_ARROWS
jest kompletnie puste. Tępo patrzę na kod, przeciez kurde bele ostatnia rzecz jaką można powiedzieć o BIND_ARROWS
to to, że jest puste. Gdyby był jakiś mismatch, niedopasowane typy, no ok, ale zaraz null? Metodą prób i błędów doszedłem do tego, że gdscript z jakiegoś powodu nie pozwala na inicjowanie słowników poza wnętrzem funkcji. Idiotyczne, ale co robić? Przeniosłem deklarację słownika do funkcji wywołującej (i tak całość jest odpalana tylko raz, po czym kasowana), teraz scena działa... tylko strzałki mrygają kompletnie chaotycznie. Najwyraźniej powyższa funkcja działa dostatecznie długo, że wszystko się desynchronizuje zanim dojdzie do końca pętli... Z początku miałem to zostawić, trochę chaosu ma swoje zalety, ale ostatecznie zrobiłem tak, że wszystkie strzałki wpisałem do jednej grupy/taga, nazwanego po prostu "strzałki", i po wykonaniu powyższego kodu całej grupie wymuszam powrót do pierwszej klatki animacji. Taka operacja jest praktycznie natychmiastowa, strzałki od teraz mrygają równo. Ostatni krok - sprzątanie. Napis pojawia się parę sekund i znika, co robię tak, że daję nodę timer na 3 sekundy, odpala się sama, jak dojdzie do zera, odpala mini kod, który mryga całością parę razy w odstępie 0.04 sekundy (visible = false / visible = true
) i daje queue_free()
, sprzątające calość z pola gry. Test - działa - super sprawa.
łeło łeło (ale takie ciche)
Ta pozycja z listy TODO odhaczona, wracam do powerupów. Wyrysiowałem analogicznie animowany powerup regenerujący tarczę statku, tym razem już przy użyciu utworzonej czcionki. Niestety (khe) z tej okazji zacząłem znowu myśleć o systemie powerupów ogólnie. Pierwotnie plan był taki, że gracz ma pięć różnych typów strzału do dyspozycji: szybki niebieski, rozproszony czerwony, celowany zielony, dwustronny fioletowy i laser. Jednak teraz, gdy mam ustalony system powerupów, gdzie konkretny drop wybiera się strzelając w ten przedmiot do skutku, pięć broni do wyboru to trochę dużo i może wymiernie opóźnić wybór pożądanego upgrade. Wyrzucenie naprawy tarczy do osobnego przedmiotu niewiele w tym pomaga. Z bólem serca zatem wyszarpałem zielony celowany i fioletowy dwustronny z kodu; zrobiło się nieco przejrzyściej i sprawniej, problem tylko jak pokryć niszę, którą te oba typy pocisku zapełniały, to jest wrogów lecących inaczej jak tępo z przodu? Myślałem chwilę nad rakietami kierowanymi, ale nie jestem ich wielkim fanem, no i trudno znaleźć dobry punkt pomiędzy powolnymi i niecelnymi a super o pe, o problemach z doborem celu nie wspominając (znaleźć jakiś cel łatwo, znaleźć dobry cel... trudniej). Dobry pomysł, jak zwykle zresztą, przyszedł kompletnie znienacka, ale w końcu przyszedł. Ten pomysł - to drony, zwane w żargonie gatunkowym również bit albo option za RType albo Gradiusem. Ofc dwie drony strzelające w jednym kierunku jak w pierwszym, albo hasające sznurkiem za statkiem (nie zawsze, ale nie wdajemy się w detale gradiusa)) jak w drugim nadal nic mi nie pomagają, ale tu już miałem konkretną wizję, którą chyba widziałem już w jakiejś gierce, bodaj Image Fight (Irem kochany). W każdym razie założenia są następujące:
- W grze może być od 0 do 2 dron (będą dropione arbitralnie raz na mapę), które nie blokują pocisków (na razie, choć kto wie????)
- Drony trzymają się gracza ale z pewnym opóźnieniem
- Drony zawsze celują w stronę przeciwną do ostatniego ruchu statku (na szczęście statek rusza się tylko w 8 kierunkach, przynajmniej z klawiatury), obrót nie jest natychmiastowy, i zajmują pozycję tak, aby nie strzelać przez statek gracza
Ot mniej więcej coś takiego:
swoboda artystyczna
Techniczna implementacja była w miarę prosta: wysmażyłem parę minut animacje kółeczka z lufą po to, żebym mógł wyraźnie widzieć efekty dłubania zarówno ruchem, jak i kierunkiem drony. Tworzę scenę z animowanym placeholderem; do sceny przypinam nowy skrypt. Ma on dwa główne segmenty: decydowanie gdzie drona ma się ruszać, w funkcji wywoływanej z sekcji silnika, gdzie sprawdzam klawisze kierunkowe, oraz sprawianie, żeby noda tam się ruszała, w starej dobrej funkcji _process(delta)
wywoływanej przez godot w każdej wolnej chwili procesora (w dużym uproszczeniu).
Funkcja decydująca odbiera ten sam argument, który przekazuję do statku gracza gdy ma się ruszyć, to jest wektor dwuosiowy direction
, przymujący wartości x oraz y w zakresie od -1 do +1. Funkcja patrzy czy obie są równe, mniejsze lub większe od zera, i na tej podstawie wyznacza koordynatę cel_ruchu
(witamy znowu słownik kompasowy, tym razem przypięty do punktów (x,y) względem statku gracza - są dwa zestawy wartości zależnie czy to drona 1 czy 2) oraz kierunek, w którym drona powinna strzelać (w radianach, skokowo od -π do +π co 1/4 π).
Teraz wszystko na barkach _process
: znamy zawsze pozycję drony, dzięki wysmażonej dawno temu funkcji pomocniczej NodeTracker
wiemy zawsze gdzie jest gracz, dzięki funkcji powyższej wiemy, gdzie drona powinna polecieć. Zatem okruszek interpolacji liniowej i mamy dronę grzecznie lecącą na swoje miejsce:
self.position = self.position.lerp(gracz.position + cel_ruchu, delta * 10)
Co z celowaniem natomiast? Ponownie wiemy zawsze, w którą stronę obecnie drona jest wycelowana, wiemy dzięki funkcji powyższej w którą stronę powinna celować. Ponownie korzystając z wcześniej przygotowanej (sekcja Łucznik) funkcji podającej kierunek w którym szybciej przekręcimy się z azymutu A do azymutu B mam ostatni potrzebny komponent, tyle że tym razem potrzebny jest jeszcze if
albo dwa:
if self.rotation != rotacja_docelowa:
var w_którą_stronę = MiscFunctions.GetRotationDir (self.rotation, rotacja_docelowa)
self.rotation += w_którą_stronę*delta*szybkość_obrotu
if abs(self.rotation - rotacja_docelowa) < 0.05:
self.rotation = rotacja_docelowa #jestesmy strasznie blisko, więc żeby drona nie bujała się na boki o ułamki stopnia zrównujemy wartości
Odpalam całość, tymczasowo każąc kodowi gry spawnować dwie drony już od początku planszy. Działa! Jeszcze tylko trochę tuningu cyferek co do szybkości i responsywności dron, "na brudno" dorzucone strzelanie i jest dokładnie taki efekt, jaki chciałem:
Co teraz zatem? Teraz zatem przynajmniej te rzeczy które wiem że muszę na pewno zrobić:
- implementacja powerupu tarczy w kodzie
- pełna implementacja powerupu drony
- implementacja końcowego bossa poziomu, w tym szczególnie dużo rysowania
- porozstawianie wrogów do końca planszy, z uwagą na nowe narzędzia i alerty
- narysowanie od nowa statku gracza, bo jako pierwsza rzecz narysowana coraz mocniej odstaje od reszty gry
- jakieś animowane bajery w tle typu nie wiem floty eksplozje itp
Kiedy to wszystko będzie gotowe... Prawdopodobnie puszczę demko w świat i w międzyczasie będę wykuwał poziom 2.