Kategorie
CrabbyBird

CrabbyBird #0 Pierwsza przygoda z Rustem i Godotem

Ten artykuł to pierwszy odcinek z serii, w której w ramach nauki Godota i języka Rust piszę sobie klon gry Flappy Bird. Godot to fajny silnik do tworzenia gier, a tak się składa, że od jakiegoś czasu próbuję swoich sił i programuję w Ruscie. Okazuje się, że całkiem łatwo i z pożytkiem można obydwa te ustrojstwa ze sobą ożenić.

Disclaimer

Tak naprawdę to gra już powstała. Zanim skończyłam redagować serię wpisów okazało się, że wyszła nowa wersja biblioteki gdnative, która nie jest wstecznie kompatybilna. Kod działający z wersją 0.8 możecie znaleźć na githubie. Planuję stopniowo dostosowywać go do nowej wersji 0.9 i równolegle dzielić się moimi przygodami z tworzeniem CrabbyBirda i świata, który zamieszkuje.

Moja nowa gra

Crabby Bird, czyli kraboptak przekładając na język polski, to bohater naszej nowej gry. Dzisiejszym celem będzie zaimplementowanie sterowania wymienionym właśnie żyjątkiem. Co przez to rozumiem?

  • Kraboptak ma reagować na naciśnięcie spacji podskokiem,
  • Kraboptak ma poddawać się sile grawitacji,
  • Kraboptak ma się obracać zależnie od tego, czy podskakuje, czy spada. W przypadku podskoku kąt wychylenia ma być ograniczony.

Kod można znaleźć na githubie. Link prowadzi do commita, w którym jest opisywany etap. Najnowsza wersja jest tutaj.

Moje preprzygotowanie

W ramach wstępnego zapoznania się z silnikiem graficznym przerobiłam minitutorial ze strony Godota. Jako że właściwy dla Godota język skryptowy GDScript mi nie podchodzi, chcę używać Rusta do wszystkiego, czego nie da się wyklikać w edytorze Godota. Posłuży mi do tego zestaw bindów godot-rust do GDNative.
W repozyteorium godot-rust w katalogu examples można znaleźć wersję Rustową gierki “Dodge the Creeps!” z minitutoriala ze strony Godota.

Nie chciałabym skupiać się za mocno na tym co i jak klikać w edytorze Godota. Wszystko co wiem (póki co) pochodzi właśnie z tutoriala o którym wspomniałam wcześniej.

Połączenie Rusta z Godotem

Jeśli chodzi o samo połączenie Rusta i Godota, wspomagałam się poniższymi źródłami:

  1. Tutaj pan uruchamia Hello World,
  2. Tutaj pan zaczyna pisać grę. Szczególnie przydatna była ta pierwsza część wpisu, o tym jak zrobić bibliotekę Rustową i połączyć ją z Godotem. Tutorial jest dokładny, z obrazkami. Polecam, bo nie chcę go tutaj powtarzać.

Odnoszą się one do starszej wersji biblioteki gdnative (0.8). Wydaje mi się, że na ich podstawie można z grubsza wyklikać wszystko to, czego potrzebujemy w Crabby Bird (przynajmniej na razie). Musimy tylko chwilę popatrzeć na różnice wywołane różnymi wersjami.

Różnice między wersjami

W nowej wersji biblioteki gdnative, zmieniła się nieco jej struktura i nazwy niektórych funkcji (i pewnie wiele innych rzeczy o których jeszcze nie wiem). Działający w nowej wersji przykład hello worlda można znaleźć tutaj.

Tu natomiast jest wersja poprzednia (używana w pierwszym tutorialu).

Rzucające się w oczy różnice to:

  1. Klasa InitHandle nie jest już w module gdnative::init::InitHandle, lecz w gdnative::prelude::InitHandle.

  2. Poniższe trzy makra:

    // Macros that create the entry-points of the dynamic library.
    godot_gdnative_init!();
    godot_nativescript_init!(init);
    godot_gdnative_terminate!();

    można zastąpić jednym:

    godot_init!(init);
  3. Zmiana nazwy funkcji _init na new i jej argumentu z wierzchołka typu Node na wskaźnik &Node. Podobnie z argumentem funkcji _ready.

Te obserwacje pozwoliły mi bez większego bólu wzorując się na podlinkowanych tutorialiach uruchomić przykład w wersji 0.9. W razie jakbyście mieli problemy na tym etapie, dajcie znać. Postaram się opisać, jak zrobiłam to ja – może pomoże.

Zacznijmy

Strukturka mojego projektu jest następująca:

  • katalog assets - tu są wszystkie potrzebne obrazki,
  • katalog src - tu są wszystkie pliki Rustowe,
  • katalog scenes - tu są pliki Godotowych scen,
  • bibilioteka którą wypluwa Rust jest w targer/debug/,
  • plik Godotowego projektu project.godot jest w katalogu głównym.

Ustawienie scen w Godocie

Na sam początek dobrze będzie ustawić potrzebne nam sceny w edytorze Godota. Potrzebujemy sceny dla Playera (RigidBody2D) i sceny głównej (Node2D), w której ten Player będzie żył, poruszał się i w ogóle robił to, co robią wszystkie porządne postaci z gier. W edytorze Godota docelowo wygląda to tak:

Sceny Player i Main

Przyjrzyjmy się nieco dokładniej scenie Player. Dodajemy do niej dwa wierzchołki-dzieci:

  1. AnimatedSprite2D – w inspektorze tego wierzchołka w polu Frames wybieramy New SpriteFrames i w menu, które pojawi się na dole dodajemy ikonkę Godota jako klatkę. W przyszłości zrobimy prawdziwą, działająca animację, a tymczasem musi wystarczyć nam ta jedna klatka.

Scena Player - AnimationSprite

  1. CollisionShape2D – w inspektorze tego wierzchołka w polu Shape wybieramy New RectangleShape2D i ustawiamy jego wymiary (tymi pomarańczowymi kropkami) tak, żeby pokrył obrazek.

Scena Player – CollisionShape

Dodawanie Nativescriptu

Zwróćmy jeszcze uwagę, na ikonkę zwoju przy wierzchołku Player. Oznacza ona, że z wierzchołkiem związany jest skrypt, który określa zachowania tego elementu. My chcemy to określać w Ruscie.
Stwórzmy teraz taką przejściówkę, między Godotem a Rustem, a dokładniej między Godotową sceną Player, a strukturą Player którą będziemy tworzyć w pliku player.rs w dalszej części wpisu. To w niej będziemy określać zachowanie wierzchołka Player. W tym celu, w następujący sposób powiążemy NativeScript z wierzchołkiem , krok po kroku:

  1. Dodajmy nowy skrypt w katalogu scenes (albo gdzie indziej, jeśli wolicie),

CrabbyBird0 - dodawanie NativeScriptu 1

  1. Ustawmy typ skryptu. Żebyśmy mogli użyć naszej rustowej biblioteki, ustawmy go na NativeScript. Uzupełnijmy też nazwę skryptu i nazwę odpowiadającej wierzchołkowi strukturki,

    CrabbyBird0 - dodawanie NativeScriptu 2

  2. Po kliknięciu w nowo dodany skrypt, w zakładce Inspector ustawmy pole Library na plik rustowej biblioteki: rust_library.tres. Koniecznie zapiszmy zmiany, klikając dyskietkę zaznaczoną strzałką,

    CrabbyBird0 - dodawanie NativeScriptu 3

  3. Na koniec dodajmy nowo stworzony skrypt do sceny Player. Robimy to w zakładce Inspector w polu Script.

CrabbyBird0 - dodawanie NativeScriptu 4

Teraz Player Rustowy i Player Godotowy powinny umieć sie komunikować.

Chodźmy do Rusta

Ku pamięci, zacznijmy od pliku lib.rs. Żeby wszystko śmigało, trzeba dodać tam wszystkie strukturki, których potrzebuje Godot. Zaraz zajmiemy sie tworzeniem struktury Player w pliku player.rs. Dodajmy ją od razu tutaj:

use gdnative::prelude::{godot_init, InitHandle};
mod player;

// Function that registers all exposed classes to Godot
fn init(handle: InitHandle) {
    handle.add_class::<player::Player>();
}

// Macros that create entry-points of the dynamic library.
godot_init!(init);

Łatwo o tym zapomnieć i przeżywać nieprzyjemne chwile nicniedziałania. Ale jeśli coś nie działa, to zawsze warto sprawdzić czy ta niedziałająca strukturka jest tutaj już dodana.

A tu już samo poruszanie ptakokrabem

Kojarzycie co umie Flappy Bird?
Gracz zmusza go do podfruwania poprzez naciskanie spacji albo pacnięcie w ekran dotykowy. Bez tego, ptakokrab spada na ziemię i gra się kończy.

Skoro spada, to działa na niego siła grawitacji. Dlatego aż się prosi, by wykorzystać do tego silnik fizyczny Godota. Ptakokrab będzie obiektem RigidBody2D – działać na niego będzie fizyka, a gracz będzie mógł wpływać na jego ruch. W przypadku KinematicBody2D, który rozpatrywałam na początku, fizyka nie oddziałuje na ptakokraba. Cóż, wtedy samemu należy zaimplementować siłę grawitacji.

Pora przejść do tworzenia pliczku player.rs.

#[derive(NativeClass)]
#[inherit(RigidBody2D)]
pub struct Player {
    jump_speed: f32,
    max_facing_angle: f32, // Maximal angle bird can face up, in degrees.
}

Uznałam, że istotne dla ptakokraba jest szybkość jego skoku (jump_speed) i maksymalny kąt, pod jakim może „patrzeć w górę” (max_facing_angle).

Jeszcze trochę przygotowań

No i dalej, w pierwszej kolejności zajmiemy się funkcją new. Nie dzieje się w niej nic szalonego, tylko tworzymy strukturkę Playera. Ogólnie to bez tej funkcji ani rusz – Rust będzie krzyczał, że funkcja new być musi i już.

Funkcja _ready wywołuje się, gdy scena i wszystkie jej dzieci są już zainicjalizowane. W niej nie dzieje się też nic wielkiego. Ustawiam gracza na środku ekranu. Funkcja nie jest konieczna. Gdy jej nie ma, Rust nie krzyczy.

#[methods]
impl Player {
    pub fn new(mut _owner: &RigidBody2D) -> Self {
        Player {
            jump_speed: 500.0,
            max_facing_angle: -30.0,
        }
    }

    #[export]
    fn _ready(&mut self, owner: &RigidBody2D) {
        // Set player position to the center of the screen.
        let size = owner.get_viewport_rect().size;
        owner.set_position(Vector2::new(size.width / 2., size.height / 2.));
    }
}

Skakanie

Za skakanie odpowiada funkcja flap wywoływana po wciśnięciu spacji. Wygląda tak:

fn flap(&self, owner: &RigidBody2D) {
    // Change player velocity y-component to make him jump.
    owner.set_linear_velocity(Vector2::new(
        owner.linear_velocity().x,
        -self.jump_speed,
    ));
}

Ustawiamy w niej prędkość ptakokraba, zmieniając jej współrzędną y. Jest ona ujemna, bo oś y skierowana jest w dół. Współrzędna x pozostaje bez zmian.

Dalej z rzeczy istotnych: obczajanie czy gracz wcisnął spację. Potrzebujemy funkcji, która cały czas pilnuje czy nie wciśnięto klawisza. Wykorzystamy do tego funkcję godotową _physics_process, wywołującą się co klatkę. Trochę więcej o niej znajdziecie w dalszej części wpisu.

#[export]
fn _physics_process(&mut self, owner: &RigidBody2D, _delta: f64) {
    // Flap, if space is pressed.
    let input = Input::godot_singleton();
    if Input::is_action_pressed(&input, "ui_select") {
        self.flap(owner);
    }
}

Tak naprawdę to nie do końca sprawdzamy tutaj czy inputem jest dokładnie spacja: sprawdzamy czy to co zostało wciśnięte należy do pewnej grupy zdarzeń (Input Map) o nazwie "ui_select". To jest akurat jedna z domyślnych map w której jest spacja, ale można sobie zrobić swoją grupę i nazwać ją dowolnie. (W edytorze Godota, Project → Project Settings → Input Map).

Na tym etapie ptakokrab umie już skakać po wciśnięciu spacji.

Obroty ptakokraba

Zajmijmy się teraz obrotami ptakokraba. Chcemy, żeby przy skakaniu obracał się przeciwnie do wskazówek zegara (w lewo?), ale w ograniczonym zakresie. Dlatego w funkcji flap ustawmy prędkość kątową na wartość ujemną.

fn flap(&self, owner: &RigidBody2D) {
    …
    // Rotate player anti-clockwise when jumping.
    owner.set_angular_velocity(-PI);
}

Żeby rozwiązać problem nadmiernego wychylania się kraboptaka, potrzebujemy funkcji, która wywoływałaby się co klatkę. Pozwoli nam to aktualizować obroty stworka w zależności od jego aktualnego wychylenia. Takie możliwości dają nam dwie funkcje Godotowe: _physics_process i _process. Czym się różnią? _physics_process wywołuje się w stałych odstępach czasu, niezależnie od jakichś niepowodzeń z wczytaniem nowej klatki. Natomiast _process wywołuje się co klatkę, więc jeśli klatka się opóźni z jakichś powodów, wywołanie funkcji też się opóźni.
Wybierzmy _physics_process. Nazwa sugeruje, że służy ona do obsługiwania procesów fizycznych.

Spadający ptakokrab powinien obracać się zgodnie z ruchem wskazówek zegara wtedy, gdy gracz nie wciska spacji. To też zakodujemy w funkcji _physics_process. Wygląda ona tak:

#[export]
fn _physics_process(&mut self, owner: &RigidBody2D, _delta: f64) {
    …
    // Assure that player can't face up more than `max_facing_angle`.
    let actual_rotation = owner.rotation_degrees();

    if actual_rotation < self.max_facing_angle {
        owner.set_rotation_degrees(self.max_facing_angle);
        owner.set_angular_velocity(0.0);
    }
    // Set angular velocity when falling.
    if owner.linear_velocity().y > 0.0 {
        owner.set_angular_velocity(PI / 2.0);
    }
}

Dziwić może warunek if actual_rotation < self.max_facing_angle. Oznacza on, że kraboptak osiągnął maksymalne wychylenie. Jest to poprawne, bo wartość kąta maksymalnego wychylenia jest ujemna. Pewnie ładniej by było załatwić to wartościami bezwględnymi.

Kolejny warunek oznacza, że kraboptak spada. Obracamy go wtedy zgodnie z kierunkiem wskazówek zegara.

Efekt

W ten sposób dochodzimy do końca tego wpisu. Cel osiągnięty, oto efekt naszej pracy:

Skoki kraboptaka

Uwagi

  • Linijki #[export] przy funkcjach Godotowych są istotne. Nic bez nich nic nie działa. To rzecz którą warto sprawdzić, jeśli coś nie działa.

  • Wcześniej próbowałam zrobić to samo z KinematicBody2D zamiast RigidBody2D. Oprócz tego, że trzeba było zadbać o siłę grawitacji, nie różniło się to znacząco.

  • Jeśli ptakokrab skacze źle, spada zbyt szybko lub zbyt wolno, można to poprawić zmieniając ustawienia grawitacji w edytorze Godota (pole Gravity Scale w scenie Player). Ja ustawiłam ją eksperymentalnie na 10 i póki co wygląda dobrze.

  • Podobnie z wartością prędkości obrotowej. Wybrałam ją eksperymentalnie i wydaje mi się, że wygląda OK.

  • Dokumentacja Godota pisze, żeby poruszać RigidBody2D tylko pośrednio, działając na nie siłą. Trochę nie zastosowałam się do zaleceń, sterując ptakokrabem przez zmienianie wektora prędkości i prędkości kątowej. Takie rozwiązanie zadziałało i zapobiegło podwajaniu się wysokości skoku przy podwójnym kliknięciu. Mój doraźny cel jest taki, żeby z każdym kliknięciem ptak skakał tak samo wysoko, a na potrzeby pierwszej malutkiej gierki mogę sobie chyba pozwolić na nieprzestrzeganie zaleceń…

  • W poprzedniej wersji gdnative (0.8) funkcje _ready i _physics_process wymagały dodatkowo słowa kluczowego unsafe przed definicją funkcji. Od wersji 0.9 nie jest to już konieczne.

Podsumowanie

Bardzo się cieszę, że udało nam się skomunikować ze stworkiem, którego stworzyliśmy. Musimy tylko sobie wyobrazić, że ta poruszająca się na ekranie ikonka Godota to prawdziwy prakokrab. Jeśli zastanawiacie się jak naprawdę wygląda takie zwierzę, zapraszam na kolejny wpis z tej serii. Zajmiemy się w nim animacją ptakokraba i ustawieniem jej w Godocie.

Zachęcam do dalszej zabawy i eksperymentowania. Jeśli ktoś miałby ochotę na podzielenia się swoimi pomysłami, uwagami, problemami, to też zachęcam – w dowolnie dostępnej formie.

W odpowiedzi na “CrabbyBird #0 Pierwsza przygoda z Rustem i Godotem”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *