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:
- Tutaj pan uruchamia Hello World,
- 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:
-
Klasa
InitHandle
nie jest już w modulegdnative::init::InitHandle
, lecz wgdnative::prelude::InitHandle
. -
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);
-
Zmiana nazwy funkcji
_init
nanew
i jej argumentu z wierzchołka typuNode
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:
Przyjrzyjmy się nieco dokładniej scenie Player
. Dodajemy do niej dwa wierzchołki-dzieci:
AnimatedSprite2D
– w inspektorze tego wierzchołka w poluFrames
wybieramyNew 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.
CollisionShape2D
– w inspektorze tego wierzchołka w poluShape
wybieramyNew RectangleShape2D
i ustawiamy jego wymiary (tymi pomarańczowymi kropkami) tak, żeby pokrył obrazek.
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:
- Dodajmy nowy skrypt w katalogu
scenes
(albo gdzie indziej, jeśli wolicie),
-
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, -
Po kliknięciu w nowo dodany skrypt, w zakładce
Inspector
ustawmy poleLibrary
na plik rustowej biblioteki:rust_library.tres
. Koniecznie zapiszmy zmiany, klikając dyskietkę zaznaczoną strzałką, -
Na koniec dodajmy nowo stworzony skrypt do sceny
Player
. Robimy to w zakładceInspector
w poluScript
.
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:
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
zamiastRigidBody2D
. 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 sceniePlayer
). 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 kluczowegounsafe
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”
Trafiłem na wpis przeglądając TWiR (https://this-week-in-rust.org/blog/2020/08/26/this-week-in-rust-353/)
Temat przygniótł mnie, więc wrzucam do schowka na przyszłość.
Życzę przyjemność z czynionych prac i ogólnie… sukcesu?
Pozdrawiam, Wicu