Devlog #9: cracktro
Witam, witam w nowym devlogu. Prawie wszystko, co mi przychodzi do głowy względem przejścia z robienia frameworków na komponowanie planszy jest gotowe (poza finalnym bossem, którego muszę sporo obrysować, na co pewnie poświęcę większość weekendu... któregoś). No, na ostatnią chwilę przyszło mi do głowy pewne przeprojektowanie logiki intra na to, żeby była mniej ciągiem funkcji wołających kolejne funkcje, a bardziej jakimś zawiadywaniem.
A to dlatego, że przyszła pora na coś pokroju ekranów przed-tytułowych, co by zawierało logo ....grupy.... i jakieś tam powiązane pierdółki. Z początku myślałem to zrobić a la gierki z epoki amigi, czyli fade in - prosta animacja - fade out, ewentualnie bitcrushowany głos. Podczas brainstormingu na privie wychodziły różne propozycje i wzorce:
[głos Franko] RAN SOFTŁER PREZENC
Ale teraz, gdy zasiadłem do faktycznego dłubania nad tym elementem programu przyszło mi coś jeszcze lepszego do głowy: cracktro. Stare dziadki amigowe pamiętają (mam nadzieję) że gry pirackie - czyli 99% obecnych w obiegu - po włożeniu dyskietki do napędu witało nie logiem gry, nie logiem twórców, a (z reguły) donośną muzyką cziptunową, jakąś sexy grafiką i/lub bajerami graficznymi typu latające kulki, starfield, bałnsujący tekst itd. W ten sposób witała się grupa crackerska, która złamała zabezpieczenie antypirackie i puściła spiraconą grę w świat. Wiele z tych intro crackerskich (tj. cracktro) dość konkretnie zapadało w pamięć i było małymi cudeńkami kodu wpakowanymi w kosmicznie małą ilość miejsca. Ot, chociażby:
Czyli co? Mojej gry nikt nie spiraci (bo nie będzie nic do łamania lol), ale coś stylizowane "na epokę" rozpaliło moją wyobraźnię, również jako mały test tego, czego się "naumiałem" tłukąc prawie rok już w godocie. Do tego podkład dźwiękowy zlecony znajomemu muzykowi nada klimat. Zatem plan robót był następujący:
- tęcza decrunchowa
- stonowane tło
- wirujące logo
- scroller
- starfield
- odbicie?
- coś bałnsującego
- inne efekty?
Tęcza decrunchowa
Wiele intro/cracktro/demoscenowych rzeczy (choć nie większość) po odpaleniu powodowało, że ekran zalewała fala wielokolorowych pasków przypominających tęczę. Oznaczało to, że kod był skompresowany (crunch) popularnym programem Powerpacker, tęcza sygnalizowała, że nic się nie zawiesiło, tylko powerpacker właśnie rozpakowuje (decrunch) treść do pamięci. Jest to bardzo charakterystyczny bajer wizualny:
(był jeszcze też imploder/exploder, który IIRC miał subtelniejszy efekt)
Jak zatem zasymulować tęczowe paski jadące przez ekran? Robiąc w fbasicu czy innym cpp pewnie bym ręcznie poprzestawiał piksele pętęlką jadącą przez ekran i już. W Godocie nie ma - chyba - tak bezpośredniego szturchania powierzchni ekranu... ale są triki. Jeden trik to ofc własny shader, ale nie umiem into shadery. Drugi trik - zapełnienie ekranu czymś kwadratowym w dużej ilości. Naturalnie nasuwa się ColorRect, czyli kwadracik zapełniony jednym kolorem. Wysmażyłem zatem nową scenę, w niej pętelka jadąca po wysokości ekranu, w niej pętelka jadąc po szerokości ekranu, w niej tworzenie nowego kwadracika na współrzędnych i dodanie go do tablicy przechowującej wszystkie kwadraciki.
for scr_y in (screenwidth.y/2):
for scr_x in (screendwith.x/2):
var n_kwadrat = ColorRect.new(detale: rozmiar, pozycja, kolor)
tablica_kwadratów.append(n_kwadrat)
tabela_pełna = true
Kolejność pętelek jest ważna, gdyż inaczej tęcza byłaby pionowa, nie pozioma. Następnie w _physics_process() sprawdzam czy tabela jest już pełna (tabela_pełna == true). Jeśli jest, robię następujące operacje:
- pętla iterująca po tabeli
tablica_kwadratów
czylifor kwad in tablica_kwadratów:
- losowanie koloru
kolor = Color(rng(0.0-1.0), rng(0.0-1.0), rng(0.0-1.0)
- losowanie ile następnych linijek (1-5) ma być zapełnione kolorem i przełożenie tego na ilość kwadratów (szerokość ekranu * ile_linijek)
- nadawanie koloru kwadrat po kwadracie
kwad.modulate = kolor
Wszystko fajnie, pięknie, tylko cholernie wolne. 2 klatki na sekundę. Godot niestety nie kumpluje się zbytnio z iteracajami po tablicach i mazaniu ColorRect, przynajmniej twórcy silnika nie spodziewali się, że ktoś będzie sobie operował na 640*360 (rozdziałka/2) = dwustu tysiącach ColorRect naraz. Zabrałem się do optymalizacji.
Pierwsza optymalizacja: przesiadłem się z ColorRect na Sprite2D, wysmażone na gotowo z białego kwadrata 2x2. Nieco pomogło. Zwiększyłem kwadrat do 4x4, co zredukowało ilość obiektów do zaledwie 50 tysięcy. Pomogło bardziej. Następnie przestawiłem operacje ze sztywno regulowanego _physics_process
do _process
liczonego "ile wlezie" między wywołaniami _p_p. Przyspieszyło na tyle, że musiałem dodać funkcję spowalniającą. Wielki sukces.
Jeszcze tylko nieco przytłumiłem kolory (RGB * 0.75) żeby nie biły po oczach tak mocno i scena "decrunch gotowa". Pora na nową scenę, samo cracktro.
Stonowane tło
Od razu zaczął się problem. Albo "problem" Zrobiłem sobie tło teksturą gradientową, bo jest to łatwe i szybciej idzie niż klepanie nowego obrazka w zewnętrznym programie za każdym razem, jak będę chciał stuningować rozkład kolorów. No i się okazało od razu, że tesktura gradientowa przyjmuje rozkład kolorów tylko w poziomie (chyba...). Ok luzik obracam całość o 90 stopni. Drugi problem: tekstura gradientowa może być rozciągnięta tylko w poziomie. To znaczy, że mogę mieć 1280x720, ale nie mogę mieć 720x1280, tak żeby po wspomnianym obróceniu zapełniała cały ekran. Poszedłem na kompromis, tekstura 1280x1280, górna jej część na stałe poza ekranem. Trochę dłubania w paskach gradientowych, efekt jest jaki ma być, super.
Wirujące logo
Drugi, praktycznie kluczowy bajer: wirujące logo. Na klasyczną modłę jakiś centralny element wirujący 3D. Elementem jest, ofc, wielkie "RANE". Czcionkę wybrałem przeczesując czadowe archiwum demoscenowych fontów bitmapowych. Co z obracaniem? Well, z początku, jak to rozbestwiony nową technologią, myślałem nad zrobieniem obiectu Label3D, który by sobie wirował po osiach tak, jak mu każę. Niestety testy wykazały, że jest to prawie niemożliwe, ze względu na trudności w łączeniu elementów 3D i 2D, operowania kamerą, światłem, filtrowaniem... za dużo roboty, za duże obciążenie na głupie cracktro. Sięgnąłem zatem po starą dobrą metodę: skalowanie. Godot przyjmuje negatywne wartości skali, co daje iluzję wirowania na płaszczyźnie poziomej lub pionowej:
(tak naprawdę gdy skala = 0 godot nie wyświetla nic, kreski są poglądowe)
Ustaliłem pivot między "A" a "N" żeby logo wirowało według swojego centrum, a nie lewego górnego rogu i skleiłem na szybko AnimationPlayer zmianę skali z (1,1) na (-1,1) na (1,1) w ciągu dwóch sekund, zapętliłem, spoko działa. Fejkowy efekt jest bo nie ma perspektywy, ale co mi tam.
Scroller & starfield
Najłatwiejsze elementy. Ponownie czcionka z archiwum demoscenowego, noda Label, postawiona za prawym rogiem ekranu, w skrypcie przesuwa się stabilnie w lewo, jak cały tekst zjedzie ekranu (z ręki piszę pozycję na podstawie długości w pikselach), teleportowany znowu za prawy róg i tak w kółko. Starfield dosłownie kopuj-wklej z kodu planszy generator cząsteczek, tyle co zawęziłem i podbiłem ilość cząsteczek w generacji.
Odbicie
Tutaj musiałem wyjść ze swojej strefy komfortu. Z początku myślałem nad po prostu ręcznym odbiciem powyższych elementów, czyli scale (1,-1) i spięte razem, ale już przy logo obrotowym zaczynały się schodki, a particle gen był praktycznie niemożliwy do zduplikowania w ten sposób.
Musiałem zwrócić się ku shaderom.
Jest to temat kompletnie mi obcy, więc na chama zrobiłem poszukanie internetowe odbicia godot shader 2D, znalazłem filmik i zacząłem małpować 1:1 rozwiązania z poprawką na to że filmik był na starszą edycję godota. Kompletnie nie rozumiem zasad, ale plusy i minusy, x i y rozumiem, więc od razu poprawiłem błąd w kodzie tutoriala (plus zamiast minusa). Absolutne niesamowite uczucie było patrzeć, jak klepany kod praktycznie na bieżąco miał efekty widoczne w oknie edytora planszy. Trochę klepania, odpalam, jest piękny efekt nieco przytłumionego lustra na dole ekranu. Pojawił się co prawda kolejny problem - odbicie zmieniało kształt zależnie od rozmiaru okna, ale jak popatrzyłem na kod shadera zauważyłem że jeden parametr zależał od rozmiaru okna (które się zmieniało) i tekstury (która się nie zmieniała), więc poprawiłem żeby tylko od tego pierwszego liczyło i starczy. Stan cracktra na ten moment jest zatem następujący:
Wirujące logo v2
Drugie podejście do loga: wirowanie w obu płaszczyznach. Tego już nie mogłem łatwo zrobić w AnimationPlayer. Co więcej, nadal mogę tylko jedno z dwóch modyfikować, gdyż zmienianie obu naraz psuje iluzję. Dlatego w kodzie rozbiłem myk:
- losowanie w której osi będzie "obrót" (skalowanie)
- decydowanie na podstawie obecnej wartości czy skalowanie jest od -1 do 1, czy odwrotnie
- obracanie każdego
_process
aż skala przekroczy wartość docelową - patrz punkt pierwszy
Do tego drugi bajer, obracanie w płaszczyźnie ekranu, co losową ilość sekund (1-3) kod decyduje w którą stronę (lewo czy prawo) obracać dalej. Teraz efekt ma ręce i nogi!
Coś bałnsującego
Bałnsowanie musi być! Ot chociażby tekst "rane dezign" nawiązujący do "melon dezign", wysmażony w sprajcie z mojej czcionki tylko nieco podpicowanej. Żeby nie dłubać krzywych, pozycji odbijania z ręki i nadać odpowiednią sprężystość, sięgam po dobrego kumpla każdego programisty wizualnego: trygonometryki. Konkretnie - sinusoidę. "Ale przecież sinusoida nie jest sprężysta!" co bystrzejsi mogą powiedzieć: No fakt, sinusoida płynnie faluje:
sin(x) (a statkiem kiwało i kiwało...)
Ale wystarczy odbić funkcję według osi x. Jak? No wartość bezwzględna przecież:
|sin(x)| (hop... hop... hop...)
Następnie nieco zwiększamy amplitudę żeby efekt był lepiej zarysowany:
2|sin(x)| (hop... hop... hop...)
Na koniec zwiększamy częstotliwość żeby bałns był żwawy:
2|sin(2x)| (hophophop)
Co jest x-em w kodzie? Jest to zmienna "czas", którą cały czas powiększam o deltę, czyli... no, czas. Od ostatniego wywołania.
Inne efekty?
Co jeszcze mogłem wrzucić żeby było więcej efektów? Korciły mnie strasznie wirujące kulki z cracktra paradox powyżej, ale uważna analiza wskazała, że poruszają się one po elipsoidzie. Może bym to ogarnął w kodzie, może nie (teraz mam pomysł jak...), ale robiłoby się trochę ciasno na ekranie (ale skoro teraz mam pomysł jak... hm....). Zamiast tego zrobiłem fajerwerki: kuleczka sprajtowa leci na losową wysokość ekranu i emituje "eksplozję" kolorowych cząsteczek. Ot, sprite2d, do niego podpięty CPUParticleGenerator2D, trochę tuningowania parametrów, sterowanie ruchem w kodzie i już. Drugi efekt: meteor/kometa - sprite z prostym skryptem sterującym ruchem i przejrzystością, spawnowany co x sekund w losowym miejscu na ekranie.
Gotowe. Efekt całości prac wygląda tak:
Chociaż kurde wirujące kulki... hmm...