Devlog lamera 2: kolizje, prawidłowo

BLOG
92V
user-54005 main blog image
Rankin | 09.06, 17:55
Poniżej znajduje się treść dodana przez czytelnika PPE.pl w formie bloga.

Witam, witam w drugiej części wpisu opisującego przygody gamedevowe autora. Celem wpisu jest demitologizacja robienia giereczek - ukazanie, że nie taki diabeł straszny, wystarczy trochę zaparcia i trochę kombinowania (i sprawne googlowanie).

W poprzednim odcinku ukazałem w skrócie drogę do zorientowania się na silnik Godot oraz pierwsze kroki w tworzeniu gierki. Mam zatem statek gracza, którym się steruje, placeholder wroga który się rusza. Niestety pierwsze próby ogarnięcia kolizji utworzły dość komiczny system fizyki ruchu (aczkolwiek kolizja się niezaprzeczalnie wykrywała). Wróciłem do punktu wyjścia. Trochę drapania po głowie później i gapienia się w online wiki godota wykombinowałem że problem był po mojej stronie, gdyż rakieta wykrywała kolizję z obiektem typu Body gdy statek gracza był nadal obiektem Area. Wywaliłem slajdy, rakietę z powrotem przestawiłem na Area2D, bo nie chcę aby kolidował ze statkiem gracza jak realny obiekt - to nie ten typ gry. Statek gracza zostawilem jako Body, ponieważ będę musiał takie właśnie sztywne kolizje zrobić później, w jaskinii / na powierzchni planety. Rakieta emituje sygnał kolizji z ciałem. Tylko, że mogą w grze być i inne ciała, niż tylko gracz. Jak sprawdzić, z czym rakieta kolidowała?

rakieta uderza w statek (interpretacja artysty)

Możliwości jest kilka, ja postawiłem na następującą. W edytorze (ze skryptu zresztą też) każdej nodzie można przydzielić dowolny tag (zwany też grupą) będący po prostu plaintextem, po czym można sprawdzać czy noda ta czy tamta jest w grupie X czy Y. Statek gracza został zatem wrzucony do grupy "player". Statek wroga - "enemies". W funkcji która otrzymuje sygnał, że rakieta w coś wleciała, sprawdzam, czy to coś jest grupie "player". Jeśli tak, w tym cosiu wywołana zostaje funkcja "otrzymałem obrażenia". W skrypcie statku gracza wstukałem tę funkcję. Żeby mieć pewność że wiadomość kolizji zostaje poprawnie wysyłana i odbierana, po obu stronach prosta debugowa wiadomość na konsolę. Odpalam grę, ponownie wlatuję w rakietę wroga. Ta nie wylatuje już w kosmos, a na konsoli wyskakuje spam par "bing" "bong" tj wspomnianych debugowych wiadomości generowanych co cykl silnika, za każdym sprawdzeniem kolizji. Sukces!

func _on_body_entered(body): #obszar kolizyjny rakiety wszedł do ciała, któremu nadajemy pseudonim "body"

if body.is_in_group("player"): #czy body jest w grupie "player"?

body.hit_by_enemy() #jeśli tak to wywołujemy w skrypcie na tym body funkcję hit_by_enemy
print("bing") #wiadomość na konsolę, że zadziałało

rakieta.gd

Skoro tak, wypadałoby się czymś odgryźć - gracz musi mieć możliwość strzelania! Po pierwsze, nowa Scena. Obszar, sprajt, kolizja, wykrycie bycia poza ekranem. Layer kolizji - nowy, nazwany "player bullets". Mask - "enemies". Sprajt - mała niebieska kulka. Pocisk rusza się wyłącznie w prawo. Jak uderzy w coś, sprawdza czy grupa "enemies". Jeśli tak, to wywołuje funkcję "zadaj obrażenia" u celu, a sam się usuwa queue_free. Zapisana scena - bullet_blu.tscn.

func _physics_process(delta):

position += transform.x * speed * delta #przesuwamy w prawo, czyli (+1,0), o speed * delta

func _on_body_entered(body):

if body.is_in_group("enemies"):

body.deal_damage(1) #jeśli to wróg, otrzyma 1 punkt obrażeń

queue_free() #ten okaz pocisku znika po uderzeniu w cokolwiek

player_bullet_blue.gd

Teraz emisja; pociski wylatujące ze środka statku wyglądają dość głupio, trzeba zrobić tak, aby wylatywały gdzieś z dziobu statku. Niby mógłbym ręcznie dodać ile trzeba pikseli do formuły, ale może jeszcze statek zmieni wygląd lub rozmiar, po co to ustalać w kodzie? Dodaję nową nodę do sceny statku, Marker2D. Noda ta nie pełni kompletnie żadnej funkcji, poza tym, że posiada pozycję i w edytorze wizualizowana jest jako spory, czytelny krzyżyk. Przesuwam ją na dziób statku, tam gdzie mają się pojawiać pociski. Do tej nody daję nowy skrypt, strzelający. W tym skrypcie - pierwsza rzecz, @export var pocisk: PackedScene. To tworzy zmienną przechowującą dowolną utworzoną scenę, którą ustawiam w edytorze przeciając do slota w interfejsie zamiast podawać ścieżkę do pliku w skrypcie (ofc tak też można). Przeciągam tam, rzecz jasna, utworzoną scenę pocisku. Skrypt nasłuchuje naciśnięcia przycisku "z" na klawiaturze, któremu dałem na ten cel alias "ply_attack" i tak długo jak ten przycisk jest wciśnięty, generuje nowe pociski (instantiate) i daje im położenie markera2D. Zanim scena się pojawi "fizycznie" w grze jako pocisk trzeba ją dodać jako "dziecko" jakiejś sceny. Nie mogę jej dodać do statku gracza bo, ponownie, będzie się z nim ruszać w górę, dół, etc. Muszę dodać do sceny-planszy poziomu.

if Input.is_key_pressed("ply_attack"): #tak długo jak klawisz "ply_attack" jest wciśnięty

var nowy_pocisk = pocisk.instantiate() #tworzymy nowy pocisk na podstawie planu podanego w utworzonej scenie
nowy_pocisk.position = self.position #nadajemy mu pozycję, ponieważ skrypt jest na Markerze, self.position - to położenie markera, nie środka statku

bullet_origin.gd

Jak zawołać coś, co nie jest obecne w danej zakładce sceny w edytorze? Jest trochę sposobów. W tym przypadku przyda się metoda owner. Odnosi się ona do nody, która w momencie wywołania metody jest nadrzędna obiektowi który posiada dany skrypt. Jeśli dobrze rozumiem... Ponieważ Marker, który zawiera ów skrypt, jest podrzędny statkowi, trzeba pójść krok wyżej. W kod wchodzi linijka owner.owner.add_child(pocisk). I jeszcze jedna rzecz: nie chcę pocisku generowanego co klatkę. Wchodzi więc zmienna "odliczająca". Jeśli jest większa od zera, nie strzelamy, tylko odejmujemy aktualną deltę. W przeciwnym razie następuje wystrzał oraz nadanie odliczającej wartości 0.2 (sekundy). OK, odpalamy, wciskam Z... gdzie moje pociski? Nie, serio, gdzie one są? Na pewno nie na dziobie myśliwca. Nagle widzę - po górnej krawędzi okna lecą szybciutko dwa piskele. Ale czemu tam?

Okazuje się, że to, jakie silnik poda współrzędne skryptowi pytającego o position... zależy. Zależy od tego, kto jest rodzicem, tj. jaka noda jest bezpośrednio nadrzędna. Dla statku gracza czy rakiety jest to obszar mapy ograniczony krawędziami okna. Dla pocisku... Owszem, podpiąłem go pod owner.owner, ale gdy pytałem o współrzędne, pytałem o współrzędne z perspektywy Marker2D. Który podlega bezpośrednio obszarowi statku i jest na równi (y=0) i kawałek w prawo (x=12) od środka tegoż statku. Więc pociski dostawały miejsce zawsze (12,0), ale po przypięciu do planszy - szukały gdzie to jest na tej planszy (owner.owner.) i tam wskakiwały, innymi słowy na równi z górną krawędzią okna i kawałek na prawo od lewej krawędzi. Jak zatem sprawdzić, gdzie Marker jest na polu gry? Proste - wystarczy pytać nie o Marker2d.position, a o Marker2d.global_position. I już działa. Prawie, bo pocisk znowu przelatuje przez wroga. Tym razem odwrotny błąd z mojej strony, ma kolidować z obszarem (rakieta), a ja czekałem na sygnał kolizji z ciałem. Szybko poprawione i pocisk grzecznie leci gdzie trzeba, jeśli trafia w rakietę, zostaje to wykryte i reakcja jest właściwa.

if shot_delay > 0:

shot_delay -= delta #odliczanie

else:

if Input.is_key_pressed("ply_attack"): #tak długo jak klawisz "ply_attack" jest wciśnięty

var nowy_pocisk = pocisk.instantiate() #tworzymy nowy pocisk na podstawie planu podanego w utworzonej scenie
nowy_pocisk.position = self.global_position #nadajemy mu pozycję, prawidłowo
owner.owner.add_child(nowy_pocisk) #wklejamy pocisk na pole gry
shot_delay = 0.2 #następnego nie tworzymy zaraz w następnej klatce, tylko za 0.2 sekundy

bullet_origin.gd

Jesteśmy teraz gdzieś na początku marca. Chwilowo w kodzie, na sucho, nic więcej nie zdziałam, muszę rysować, rysować, rysować i jeszcze raz - rysować. Narysowałem zatem właściwy spritesheet pocisków gracza (blaster, spread, laser, celowane, w obie strony). Narysowałem animowany ogień z dyszy statku. Narysowałem jak statek gracza buja się na jedno lub drugie skrzydło, korzystając z dzielenia pliku na layery i wyceniania "pi razy drzwi" kątów wszystkiego. Narysowałem parę wrogich statków, a żeby nie było nudno, dwa z nich posiadają wirujące elementy, a trzeci se błyska trochę.

Rysowanie odręczne wirujących rzeczy boli.

Z przyczyn życiowych faza rysowania zajęła dość długo, gdyż w tym okresie wiele dni byłem zwyczajnie zbyt sprany żeby cokolwiek ruszyć do przodu. Mimo, że rzeczy nie było dużo, rysowanie skończyłem dopiero pod koniec kwietnia gdzieś. Zresztą nie wszystkie rzeczy mi wyszły i jakoś wkrótce będę robił drugie podejście do niektórych projektów... Pobrałem też trochę dźwięków z freesound, żeby mieć jakieś placeholdery już na miejscu. Jednocześnie, nawet zanim skończyłem rysowanie, zabrałem się za picowanie pojazdu gracza. Po pierwsze - implementacja ognia wylotowego silnika. Pod nodę Sprite2D statku podpinam nodę AnimatedSprite2D, którą nazywam "exhaust". Tworzę w niej trzy nowe animacje: słaby odrzut (mały ogień/brak naprzemiennie szybko), średni odrzut (średni/mały ogień naprzemiennie), duży odrzut (duży/średni). Przesuwam animowany obiekt tak, aby przylegał do dyszy. W skrypcie statku zależnie od tego czy lecimy w lewo, w prawo, czy stoimy w miejscu w osi x (poziomej) aktywuję jedną z trzech utworzonych animacji.

if direction.x == 0:

$Sprite2D/exhaust.animation = "medium"

elif direction.x > 0:

$Sprite2D/exhaust.animation = "high"

else:

$Sprite2D/exhaust.animation = "low"

Następny krok picowania - chciałem, aby ruch statku w pionie wiązał się z animacją statku dla wizualizacji. Obracanie statku nos góra / nos dół nie wchodzi w grę, ponieważ nie chcę, aby pociski były obracane zależnie od ruchu w tej osi, a jeśli statek się obróci, a pociski będą leciały poziomo to będzie wyglądało głupio. Dlatego wymyśliłem, że ruch w dół oznacza że statek przechyla się na skrzydło po stronie "ekranu", ruch w górę - na skrzydło po stronie "tła". Animacja jest prościutka, po parę klatek na każdy kierunek. Bardzo było ważne, żeby animacja była płynna i ciągła, to jest jeśli zmienię kierunek ruchu statku w połowie animacji, animacja nie powinna się urywać, zaczynać od początku czy końca tego co robiła, tylko gładko zacząć bujanie w przeciwnym kierunku.

Rozwiązanie tego prostego problemu zajęło mi parę tygodni.


porzućcie wszelką nadzieję wy, którzy etc. etc.

C.D.N.

Oceń bloga:
3

Komentarze (2)

SORTUJ OD: Najnowszych / Najstarszych / Popularnych

cropper