Kategorie
CrabbyBird

CrabbyBird #1 O tym, jak animować postać gracza

Jest to drugi odcinek z serii, w której w ramach nauki Godota i Rusta piszę klon gry Flappy Bird. W poprzednim wpisie stworzyliśmy prototyp ruchu kraboptaka. Dziś zajmiemy uatrakcyjnieniem postaci i animacją jej skoków.
Kod źródłowy z tego odcinka można znaleźć na githubie.

Dzisiejszy cel

Stworzymy dziś postać kraboptaka. Kluczowy będzie przy tym sposób w jaki będziemy zarządzać animacjami. Chcemy, żeby kraboptak:

  • reagował na sygnał skoku ruchem szczypców,
  • podczas spadku nie ruszał szczypcami,
  • przy każdym skoku wzniecał dymek.

Animacja kraboptaka

Za podstawę do stworzenia postaci gracza użyjemy popularnej grafiki kraba Ferrisa z rustcean.net. Po drobnych przeróbkach zrobiłam z niej następującą animację skoku.



Pliki poszczególnych klatek umieśćmy w katalogu assets/crabby.

Zacznijmy od ustawienia animacji w Godocie. Będziemy postępować podobnie jak w tym tutorialu.

Zajmijmy się edycją wierzchołka AnimatedSprite w scenie Player. Zgodnie z zapowiedzią chcemy zrobić prawdziwą animację. W menu Sprite Frames, tam gdzie uprzednio dodaliśmy grafikę z logo Godota, wstawmy poszczególne klatki naszej nowej animacji, które umieściliśmy przed chwilą w katalogu assets/crabby. Dla lepszej orientacji nazwę animacji zmieńmy na jump. U mnie wygląda to tak:

Klatki skoku

Cho do Rusta

Gdy animacja jest już ustawiona w Godocie, chcielibyśmy móc się jakoś do niej dostać z poziomu Rusta, by móc ją uruchamiać i wyłączać. Poniższy obrazek przedstawia plan naszego działania. Podążajmy za strzałkami na poniższym obrazku, rozpoczynając od okręgu narysowanego wokół ikonki skryptu.

Realcja Godot-Rust

Dodawaniem NativeScriptu zajmowaliśmy się w poprzednim wpisie. Implementowaliśmy w nim też niektóre funkcje używane przez Godota korzystając z argumentu owner. Skupmy się więc na dalszej części problemu: jak mając dostęp do Godotowego wierzchołka Player dostać się do animacji.

Jak dostać się do dzieci ownera?

Posłużymy się funkcją get_node by dostać się do tego wierzchołka. Jej nagłówek z dokumentacji wygląda tak:

pub fn get_node(&self, path: impl Into<NodePath>) -> Option<Ref<Node, Shared>>

Jak dla mnie nagłówek jest dość przerażający. No ale bez paniki, przyjrzyjmy się mu bliżej.

Po pierwsze Option. W skrócie: może być tam coś – Some(Node), albo nic – None. Tutaj jest przykład.

Zainteresujemy się od razu funkcją and_then (trochę więcej można obaczyć tutaj). Pozwoli ona wygodnie działać na typie Option: w razie jakichś problemów, (na przykład wartości None) zwróci po prostu None. Jej argumentem będzie clousure (czy jak kto woli domknięcie, λ-wyrażenie albo funkcja anonimowa).

OK, potrafimy się dokopać do wnętrza Option. Zajmijmy się tajemniczym Ref<Node, Shared>. Jest to dzielona referencja, trzeba ją poprosić o bezpieczny dostęp do wnętrza. Posłuży do tego funkcja assume_safe. Oto jej nagłówek:

pub unsafe fn assume_safe<'a, 'r>(&'r self) -> TRef<'a, T, Shared>

Zauważmy, że funkcja jest unsafe. Musimy zaznaczyć to przy jej wywołaniu.
Więcej szczegółów o smart pointerze Ref tutaj.

Kolejna zagadką jest TRef. To tymczasowy bezpieczny wskaźnik do obiektów Godotowych. Ma on funkcję dzięki której przekształcimy obiektu Node na AnimatedSprite - cast. I tak, w końcu uzyskujemy dostęp do animacji.

Zapiszmy teraz te dość złożone rozważania w kodzie.

 owner
    .get_node("./AnimatedSprite")
    .and_then(|node| unsafe { node.assume_safe().cast::<AnimatedSprite>() });

Trochę nieszczęśliwie nazwałam wierzchołek przechowujący animację. Ścieżka w funkcji get_node oznacza jego nazwę ustawioną w edytorze Godota, a AnimatedSprite poniżej – typ.

Teraz, gdy umiemy się dokopać do animacji, pomyślmy jak ładnie umieścić to w kodzie w pliku player.rs.

No to mamy animację

Co dalej? Gdzie ten kod wsadzić? Moja pierwsza myśl: no przecież w funkcji flap. Po chwili zastanowienia wydaje mi się, że ładniej będzie przeznaczyć na wierzchołek przechowujący animację, pole w strukturze Player.
Prowadzi to do następujących zmian:

pub struct Player {
    .
    .
    .
    jump_animation_node: Option<Ref<Node>>,
}

Trzeba teraz uaktualnić funkcję new. Jako że nie mamy pewności czy podczas wywołania tej funkcji animacja jest już na scenie, ustawmy nowe pole na None.

pub fn new(_owner: &RigidBody2D) -> Self {
    Player {
        .
        .
        .
        jump_animation_node: None,
    }
}

W momencie, gdy scena już powstała, możemy ustawić polu właściwą wartość. Użyjemy do tego omawianej wyżej funkcji get_node:

#[export]
fn _ready(&mut self, owner: &RigidBody2D) {
    .
    .
    .
    self.jump_animation_node = owner.get_node("./AnimatedSprite");
}

Zostaje już tylko uruchamiać animację w momencie naciśnięcia spacji. W funkcji flap dodajmy dalszą część powyższych rozważań:

fn flap(&self, owner: &RigidBody2D) {
    .
    .
    .
    // Start flying animation.
    self.jump_animation_node
        .and_then(|node| unsafe { node.assume_safe().cast::<AnimatedSprite>() })
        .map(|anim| anim.play("jump", true));
}

String "jump" w argumencie funkcji play przekazuje do tej funkcji nazwę animacji, którą wcześniej utworzyliśmy w edytorze Godota. W przyszłości stworzymy jeszcze kilka różnych animacji dla gracza. Dzięki różnym nazwom będziemy mogli wybierać spośród nich odpowiednie.

Dygresja I Na początku chciałam, żeby pole jump_animation_node przechowywało referencję bezpośrednio do wierzchołka AnimatedSprite. Wtedy nie musielibyśmy przekształcać typu Node na AnimatedSprite przy każdym włączaniu animacji, bo zrobilibyśmy to raz w funkcji _ready. Powinno być wtedy typu mniej więcej takiego:

jump_animation_node: Option<TRef<'_, AnimatedSprite>>,

Niestety za mało wiem jeszcze o lifetimach i jedynym sposobem w jaki udało mi się to zmusić do działania, było zastąpienie '_ przez 'static . Mam jednak niejasne przeczucie, że to może jakaś zła praktyka, czy coś. Jakby ktoś z Was miał jakieś sugestie, chętnie przyjmę. Na razie jednak zrezygnowałam z tego pomysłu.

Dygresja II Być może wydzielenie pola dla wierzchołka animacji w przypadku, gdy działamy na nim tylko raz jest nadmiarowe. Coś mi jednak podpowiadało, że warto tak zrobić. W razie, jak zechcę użyć go jeszcze gdzieś, uniknę wpisywania w kółko ścieżki do podwierzchołka. Przyda się.

W tym momencie po wciśnięciu spacji kraboptak zaczyna machać szczypcami! To już prawie tak jak chcieliśmy. Problem w tym, że nie umie przestać. Naprawić to można w edytorze Godota w menu SpriteFrames odznaczając parametr loop.

Gdzie jest to loop

Teraz krab posłusznie macha szczypcami tylko raz.

Mocne skakanie

Żeby lepiej podkreślić jak żywiołowo skakać potrafi kraboptak, dodajmy za nim dymek. Będzie to proces analogiczny do tego, co zrobiliśmy do tej pory. Opiszmy go tutaj w skrócie. W razie czego moje rozwiązanie można znaleźć na githubie.

W tym celu w edytorze Godota w scenie Player dodajmy dodatkowy wierzchołek AnimatedSprite i nazwijmy go PuffAnimation. Dodajmy klatki kluczowe. U mnie znajdują się one w assets/puff. Potem postępujmy analogicznie jak powyżej:

  1. Dodajmy pole puff_animation_node do struktury Player,
  2. Ustawmy wartości tego pola w funkcjach new i _ready,
  3. Uruchommy animację w funkcji flap (nazwa animacji to default),
  4. W edytorze Godota wyłączmy zapętlanie animacji.

Po takim przygotowaniu za naszym kraboprakiem powinien ciągnąć się dymek, a animacja włączać się po przyciśnięciu spacji. Nie jest to do końca to, co chcieliśmy osiągnąć. Dymek powinien pojawiać się tylko podczas skoku. Dlatego zajmijmy się teraz jego widocznością.

W edytorze Godota, ustawmy animację na początku na niewidoczną (PuffAnimation → Inspector → Visability). W funkcji flap, przed włączeniem animacji włączmy widoczność dymku, używając funkcji show:

// Play and show jump smoke.
self.puff_animation_node
    .and_then(|node| unsafe { node.assume_safe().cast::<AnimatedSprite>() })
    .map(|anim| {
        anim.show();
        anim.play("default", true);
    });

Funkcja map jest tu dlatego, że animacja jest typu Option. Stosując ten trik, możemy działać na tym, co ma w środku, bez rozpakowywania jej. Z powodzeniem można by to zastąpić czymś takim:

// Play and show jump smoke.
match self
    .puff_animation_node
    .and_then(|node| unsafe { node.assume_safe().cast::<AnimatedSprite>() })
{
    Some(anim) => {
        anim.show();
        anim.play("default", true);
    }
    None => (),
}

Tylko to sporo więcej pisania.

Teraz dymek pojawia się po wciśnięciu spacji, odgrywa animację jeden raz i nie znika. Żeby zniknął, potrzebujemy Godotowego mechanizmu wysyłania sygnałów. Polega on na tym, że jak wierzchołek zauważy jakieś zdarzenie, rzuca komunikat, który mogą odebrać inne zainteresowane wierzchołki i jakoś na niego zareagować. Interesuje nas sygnał animation_finished, który umie emitować AnimatedSprite. Odbiorcą w tym przypadku będzie strukturka Player, która ma ukryć dymek.

W edytorze Godotowym po zaznaczeniu wierzchołka PuffAniamtion w menu po prawej stronie na samej górze możemy wybrać zakładkę "Node" (zaraz koło "Inspector"). Można tam znaleźć sygnał animation_finished. Po kliknięciu pojawia się okienko, w którym na dole wpisuje się nazwę funkcji, która będzie reagowała na ten sygnał.

Gdzie jest sygnał

Funkcję o nadanej tam nazwie dodaję do struktury Player w player.rs.

// Function connected with animation_finished() event from PuffAnimation node.
#[export]
fn _on_puff_animation_finished(&self, _owner: &RigidBody2D) {
    // Hide jump smoke
    self.puff_animation_node
        .and_then(|node| unsafe { node.assume_safe().cast::<AnimatedSprite>() })
        .map(|anim| anim.hide());
}

Jak widać, wywołujemy tam funkcję hide dokładnie tak, jak wcześniej show.

Na sam koniec możemy zmienić kolor dymku w zakładce Inspector → Visability → Modulate.

Efekt

Teraz wszystko powinno działać już tak jak zaplanowaliśmy. Prędkość animacji można regulować w edytorze w menu SpriteFrames. Ja ustawiłam ją na 30 FPS.

Gra wygląda tak:



Podsumowanie

Po dzisiejszych zmianach gra nabiera trochę swojego charakteru. No dobra, wciąż brakuje nam celu gry, ale łatwiej już wyobrazić sobie dalsze części projektu. Na mnie ta prosta animacja działa dość motywująco i przez to z nadzieją patrzę w przyszłość CrabbyBirda.

Nie do końca podoba mi się ten dymek, ale ciesze się że działa. W nieodległej przyszłości postaram się go zrobić inaczej, przy pomocy efektów cząsteczkowych. Na razie jednak ten efekt wystarczy.

W następnym wpisie zajmiemy się ruchem kamery, także zapraszam już dziś.

Jeśli macie ochotę podzielić się wrażeniami, pomysłami, nadziejami albo innymi uwagami, serdecznie zachęcam.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.