Devlog lamera 5: docieramy do dnia dzisiejszego

BLOG
132V
user-54005 main blog image
Rankin | 27.06, 14:03
Poniżej znajduje się treść dodana przez czytelnika PPE.pl w formie bloga.

Witam, witam w devlogu lamera. W poprzednim odcinku streściłem, w kolejności niekoniecznie chronologicznej, prace nad implementacją odmiennych wrogów. Relacja zakończona była na wrogu dropiącym upgrade dla gracza - aby się nim zająć, muszę wznowić pracę nad zakulisowymi elementami mechanik i kodu.

Na obecnym etapie prac podstawowym wymogiem do pełnej implementacji systemu ulepszania broni jest sprawny interfejs, a droga do sprawnego interfejsu w tym momencie jest trochę, hm, dłuższa. Pierwsza decyzja jest taka, gdzie w organizacji gry w ogóle ma ten interfejs wisieć? Niby mogę go wrzucić pod planszę, ale ze względu na to, jak działa silnik, i tak musiałbym w takiej sytuacji mieć coś niezależnego od poziomów, co przechowywałoby dane o kluczowej ciągłości - punktację, poziom zdrowia, aktualną broń. Tutaj znowu mam dwie opcje; opcja pierwsza - wpakować te zmienne do singletonów ("wiszące w powietrzu" skrypty dostępne z każdego miejsca w projekcie). Opcja druga, przygotować obiekt-scenę zarządzającą całą logiką gry, pod którą podpięty byłby z jednej strony HUD, z drugiej podpinane i odpinane w miarę potrzeb sceny poziomów. To mi pozwoli też czytelniej pracować nad rzeczami typu chociażby menu startowe, gdyż mogę je prototypować "live", w edytorze, bez ustawiania wszystkiego na ślepo w skrypcie i testowania po kompilacji.

Robię zatem nową scenę, Zwykły Node. Nazywam ją - "Core" (rdzeń). Na razie z ręki (tj przeciągając myszką z okienka plików do okienka sceny) podpinam do niej scenę-planszę na której testowałem różne statki. Wrzucam skrypt kontrolny (SK). Wyrywam ze skryptu statku gracza wszelkie nasłuchiwanie co do przycisków sterujących, wrzucam do SK i wiążę jedno z drugim nawołaniami do funkcji; innymi słowy zamiast mieć w _process() statku zapytanie "czy wciśnięta strzałka, jeśli tak to zmień pozycję", mam z jednej strony, w SK, strukturę "czy wciśnięta strzałka, jeśli tak to zawołaj statek gracza", w statku gracza "jeśli funkcja ruszaj została wywołana, zmień pozycję tak jak ci mówię". Analogicznie zmieniam strzelanie. Dlaczego taka przebudowa? Cóż, może zdarzyć się sytuacja, gdy statek obecny jest w grze, ale nie ma się ruszać, np. w menu pauzy. Takie przerzucenie nasłuchiwania klawiatury do SK pozwoli mi łatwo sterować gdzie mają dochodzić informacje o naciskaniu klawiszy bez wprowadzania zmiennych-warunkowców i ifów w 10 różnych miejscach.

all cores are bastards

Następnie tworzę singletona (samodzielny, zawsze dostępny skrypt bez sceny), nazywam go NodeTracker. W miarę rosnącej komplikacji i wymienności elemetów gry śledzenie pod jakim adresem dokładnie siedzą sceny - core, planszy, gracza, zaczynało być dość uciążliwe. Szczególnie nawiązania do core zaczynały mi coś często wyrzucać np. okno gry (w sensie obiekt windowsa) gdy się pomyliłem o numer childa w drzewku, albo nawet nie pomyliłem tylko dodałem nowego singletona. NT ma być rozwiązaniem tego problemu - za każdym razem gdy wspomniane trzy obiekty wchodzą na scenę, w ramach inicjalizacji pukają do NodeTrackera i mówią "hej, to ja, jestem tutaj". Wtedy gdy w dowolnym innym skrypcie muszę się do nich odwołać (np. żeby umieścić nowy statek wroga, żeby namierzyć gracza, żeby aktualizować punktację), wołają w swojej inicjalizacji NodeTracker metodą np. var _core_reference = NodeTracker.GetCoreScene() i otrzymują dokładnie to, czego trzeba zapakowane w zmiennej _core_reference, co znaczy że nie trzeba szukać tych rzeczy za każdym razem, co też trochę wydajność robi (niewiele, ale...). Poprawiwszy odwołania w istniejących skryptach na nowy system, mogłem zająć się prototypowaniem HUDu.

_ready():

NodeTracker.SetCoreScene(self)

               V
func SetCoreScene(node):

_core_storage = node

               V
func GetCoreScene():

return _core_storage

podaj dalej

HUD w Godocie można zorganizować na różne sposoby. Generalnie zaczyna się to od nody CanvasLayer, która służy wydzieleniu rzeczy rysowanych niezależnych od reszty obiektów widocznych w oknie gry. Do niej można podpinać kolejne obiekty tworzące HUD właściwy. Zacząłem od prostego, niezbyt głęboko przemyślanego dłubania w nodach Label (po prostu tekst); deliberowałem nad czcionką i w końcu pobrałem taką dostępną na warunkach Creative Commons, użytą w LibreQuake.

Myśląc, jak urozmaicić zbieranie punktów, pomyslałem o systemie kombosów, zresztą obecnym w wielu moich ulubionych grach SHMUP. Chodzi o to, że każdy zabity wróg zwiększa mnożnik punktów i dodaje czas do kombosa. Żeby ten mnożnik utrzymać i zwiększać, trzeba kolejnych wrogów zabijać dostatecznie szybko aby pasek kombosa się nie wyczerpał. To znaczy, że do HUDa muszę dorzucić pokazywanie ile wynosi aktualny mnożnik, a już super byłoby gdybym mógł też jakoś zwizualizować ile czasu do resetu. To pierwsze było proste, za tym drugim przyjrzałem się obiektowi ProgressBar, czyli regulowanemu paskowi wizualizującemu dwie wartości jako zapełnienie właśnie tego paska. Po chwili dłubania miałem coś prostego:

Od razu wyrżnąłem głową w mur, gdyż miałem problem z kolorowaniem tekstu. Mnożnik (x6 powyżej) ma zmieniać kolor z niebieskiego na żółty na czerwony w zależności od wysokości, co muszę robić w kodzie, nadając labelce konkretny kolor zdefiniowany obiektem Color(wartości RGB/RGBA). Tylko za każdym razem jak wbijałem moje pożądane wartości RGB to parametru, wynik końcowy był kompletnie nie taki, jakiś biały, bladożółty lub w ogóle nie wiadomo co. W końcu jednak doszedłem, o co chodzi. Okazało się, że autorów godota w tym miejscu kompletnie pogięło i zamiast przyjmować normalne RGB kolory tak jak wszędzie normalni ludzie robią, albo hex (FFFFFF - biały), albo dziesiętnie (255, 255, 255 - biały), system wymaga, aby składowe koloru były podawane jako liczba zmiennoprzecinkowa w zakresie 0.0 - 1.0. To znaczy, że żółty to na przykład (0.9512412546, 0.951241322, 0.0000421), a nie (242, 242, 0). Żeby było śmieszniej, wartości poza zakresem nadal są interpretowane (wyjaśniło się co robi self.modulate), jako hiper-przesycone kolory. Idiotyzm totalny.

Tymczasem myślałem jak urozmaicić interfejs - chodziło o to, że te cyferki muszą mieć jakieś tło, żeby rzeczy dziejące się na polu gry ich nie zasłaniały. Skoro to tło trzeba zrobić, fajnie byłoby gdyby miało ono jakieś "wow". Można by to zrobić jako jakieś abstrakcyjne kształty, albo pójść w skeumorfizm. Co zrobić, jak zrobić - chwilowo nie miałem pomysłu, aż tu dłubiąc z combo barem i eksplorując możliwości obiektu ProgressTextureBar doznałem nagłego olśnienia. PTB, widzicie, jest dużo bardziej zaawansowaną formą ProgressBara, w zależności od podanych tekstur i wartości może zapełniać się w najróżniejsze kształty, kierunki, sposoby, a łącząc dwa i więcej można jeszcze wykombinować. Krótka sesja w aseprite i miałem już pewność, że sam kombo bar będzie działał jak sobie wyobraziłem, tj zapełniając się w poziomie, od środka, po czym zacząłem rysować konkretne animacje interfejsowe. Chodziło o to, że samo wejście HUD na ekran będzie odbywało się jako animacja, co liczyłem, że da całkiem niezły efekt. Jeszcze dłuższa chwila w AnimationPlayer i miałem gotowy efekt wjazdu na ekran:

Mając górną część UI gotową, mogłem zająć się dolną - stan broni, oraz stan pancerza. Systemy te są ze sobą w pewnym stopniu powiązane; jeśli chodzi o wytrzymałość statku, wybrałem coś pomiędzy systemem 1 trafienie = 1 zgon z klasyków a pulą HP z Tyriana czy Jets n Guns. Statek gracza ma 4 punkty pancerza, dowolny pocisk albo kolizja z wrogiem kosztują 1 punkt. Tych punktów nie można odzyskać - ale taki system karzący mocno za każdą pomyłkę nie jest zbyt fajny, więc jest dodatkowy margines w postaci tarczy, to jest do 2 punktów HP które można zebrać specjalnym powerupem. Również po to, żeby wrogowie dropiący upgrade broni mieli jakiś sens za pierwszym poziomem, a także żeby był jakiś dodatkowy impuls do niebrania pocisków na klatę, utrata HP pancerza (ale nie tarczy) wiąże się z utratą 1 poziomu broni. Mam zatem 6 poziomów zdrowia (1-4 + 0-2) oraz 4 stany broni (0-3). Zdrowie postanowiłem ukazać jako grupę zazębiających się, ale oddzielnych trójkątów. Rozwiązanie takie wymagało trochę kombinowania, gdyż TextureProgressBar potrafi fajne triki, ale nie aż tak fajne. Pomógł mi fakt, że TPB może przyjmować wartości większe i mniejsze od jego nominalnego zakresu; wartości większe są traktowane jako full, mniejsze jako pusty wskaźnik. Zrobiłem zatem cztery osobne TPB, nadałem i kształty trójkątów. Pierwszy reprezentuje zakres 0-1, drugi 1-2, etc. Wszystkie dostały tag/grupę "ArmorBar". Następnie w kodzie każę od razu całej grupie nadać obecną wartość pancerza. Jeśli jest to np. 3 - zapalają się pierwsze trzy trójkąty, a czwarty gaśnie. Broń zaś była bardzo prosta, ot trójkątny mask, zapełniany koliście, jedynie w kodzie podmieniam kolor żeby reprezentował wizualnie obecnie posiadaną broń. Kolejna chwila dłubania w Animation Player i wszystko ładnie wjeżdża na ekran. Interfejs będę też musiał jakoś chować żeby gracza nie zasłaniał, mam pomysł jak to robić, ale jeszcze nie sprawdzałem czy wejdzie.

Mając te mechaniki osadzone w kodzie, mogłem wrócić do wroga dropiącego upgrade. Implementacja była teraz śmiesznie prosta, ten wróg to tylko powtórzenie rzeczy, które już robiłem. Sam upgrade również był zorganizowany na tym samym frameworku, jedynie podczas kolizji z graczem nie wywołuje funkcji obrażeń, a funkcję upgrade broni, natomiast podczas kolizji z pociskami gracza zmienia aktywne ulepszenie na następne - w ten sposób gracz sam decyduje, z jakiej broni chce korzystać.

Pomału jestem więc coraz bliżej i bliżej momentu, w którym mogę po prostu zacząć planszę budować. Zostaje jedynie (aż) rosnąca góra szlifowania i dodawania elementów audiowizualnych, które dotąd ignorowałem. Przede wszystkim - efekty dźwiękowe. Wszędzie, wszędzie efekty dźwiękowe. Z początku miałem ich tylko dwa, na strzał gracza, oraz na trafienie wroga, oba wzięte z freesound i przedłubane trochę w audacity dla przycięcia/wzmocnienia/regulacji. Przekopywanie freesound jednak szybko zaczęło być dość męczące i zwróciłem się w stronę generowania własnych dźwięków. Miałem akurat na dysku wrzucony Buzz, pobrany w innym celu i który sam w sobie jest całą epopeją (obiecuję, kiedyś opowiem). Udało mi się w nim zrobić całkiem miło brzmiące brzęczenie lasera, choć sprawienie żeby ono się zapętlało było całym osobnym problemem (tl;dr muszę w godocie ręcznie wpisać dokładny punkt zapętlenia, zły wybór powoduje paskudne, donośne kliknięcie, nie ma dobrego wyboru); gorzej było z eksplozjami, innymi pociskami itp., aż się dowiedziałem o istnieniu jsfxr, to jest darmowego generatora dźwiękowego opartego na javie, działającego w przeglądarce i pozwalającego pobrać stworzony efekt. Garść gotowych presetów i dostateczna liczba opcji sprawiły, że udało mi się w miarę sprawnie wygenerować odpowiednio brzmiące efekty i wpiąć je do scen.


przybornik dyletanta

Kolejny szlif - eksplozje i generalnie zgony statków. Mniejsze statki miały zrobić po prostu jedno "bum", większe/twardsze statki fajnie byłoby gdyby wybuchały kilka razy przed większą, końcową eksplozją. Nie chciałem ręcznie animować każdej jednej eksplozji potrzebnej na te cele, więc poszedłem skrótem, przy okazji chyba ucząc się czegoś. Otóż wcześniej, tworząc najwyższej mocy laser gracza, skopiowałem z tutoriala generowanie drobniutkich błękitnych świetlików stworzonych z tekstury. Zatem do particle generatora mogę dać tekstury. Co więcej, jak twierdzili userzy online, cząsteczki te mogą być nie dość, że teksturowane, to jeszcze - animowane! Zatem utworzyłem kompletnie nową scenę, zawierającą wyłącznie jedną rzecz - generator cząsteczek. Do tego generatora wziąłem pobrany online spritesheet prostej ale estetycznej eksplozji (niestety - tego nadal nie umiem rysować). Wstępnie ustawiłem parametry na tylko 1 emisję, 1 cząsteczki, która będzie utworzona w punkcie (0, 0). Zapisuję scenę - mam eksplozję. W każdej scenie statku wroga wklejam ją, po czym zszywam z resztą statku, tworząc funkcję śmierci według prostego klucza:

  • statek stracił całe zdrowie?
  • wyrzucamy wroga z grupy "enemies" żeby nie ingerował dalej w tracking
  • wywołujemy w nodzie core funkcję punktującą (parametry: ile punktów, ile combo czasu dodać do licznika)
  • odpalamy animację w AnimationPlayer zwaną np. "zgon"
  • Animacja "zgon" robi następujące rzeczy naraz:
    • ukrycie sprajta
    • wyłączenie hitboxa
    • emisja dokładnie 1 cząsetczki utworzonego emitera
    • odtworzenie efektu dźwiękowego eksplozji
    • po zniknięciu cząsteczki eksplozji (niestety trzeba to na oko ocenić) wywołuje w skrypcie funkcję usuwającą statek z pamięci

Większe statki działają na bardzo podobnej zasadzie, tylko wstępnie sprajt nie jest chowany, a przez 1-2 sekundy emitowane jest wiele eksplozji na całym obszarze statku, po czym dopiero jedna większa (transform/scaling = 2 np.) i zniknięcie. Te wiele eksplozji jest emitowane tym samym generatorem, tylko zamiast 1 emisji w punkcie (0, 0) zmieniam parametry na np. 8 emisji w kwadracie o wymiarach 20x20.

Docieramy zatem do stanu na dzień dzisiejszy. Aktualnie dłubię sobie w konstrukcji bossa pierwszego poziomu, który jest o tyle nietypowy, że ma części które trzeba zniszczyć, zanim będzie można bić kadłub. Od strony technicznej nie jest to jakiś wybitny zwrot metodologii - po prostu pod kadłub (area) jest podpięte kilka dalszych grup area, zawierających własne skrypty, sprajty itp. Jedyne co ciekawego to że najpierw narysowałem statek nieuszkodzony, potem wyciąłem elementy które mają być celami, zapisałem je jako osobne pliki, a w wycięte miejsca wrysowałem zniszczone sekcje. Ponieważ wszystkie pod-elementy są niżej w drzewku niż sprite kadłuba, są rysowane na jego wierzchu i zasłaniają zniszczenie. Kiedy część jest zniszczona, daję queue_free na niej (ofc po efektownej eksplozji) i odkryty zostaje wrak. Kiedy wróg ten zostanie ukończony... juz wtedy na pewno (ha) będę mógł skleić poziom.

jedyne miejsce gdzie losowe bazgroły mogą dać pożądany efekt

CDN?

Oceń bloga:
4

Komentarze (3)

SORTUJ OD: Najnowszych / Najstarszych / Popularnych

cropper