Dzisiejszy wpis jest trzecim odcinkiem z serii, w której w ramach nauki Rusta i Godota pisze sobie klon FlappyBirda. W poprzednim odcinku zajmowaliśmy się animacją skoków postaci gracza. Dziś do projektu dodamy kamerę, która będzie podążała za kraboptakiem. Kod powstały w wyniku tego wpisu można znaleźć na tutaj.
Cel wpisu
Na początek określmy cel nieco dokładniej:
- Kamera ma śledzić ruchy gracza w poziomie (to znaczy poruszać się względem osi x),
- Kamera nie będzie śledziła ptaka w pionie,
- Chcemy, by gracz widział więcej tego, co przed nim (ustawimy postać w 1/4 szerokości tego co widzi kamera).
Zacznijmy więc
Na samym początku zastanówmy się gdzie ta kamera powinna być.
Moja pierwsza myśl: umieścić kamerę jako dziecko gracza. Wtedy naturalnie będzie za nim podążała.
Takie rozwiązanie działa i być może jest wystarczające w tak małej gierce, ale ma co najmniej kilka wad. Problemem może okazać się na przykład ustawienie kamery na inny obiekt. Nie wykluczam, że coś takiego może mi przyjść do głowy. Poza tym, to po prostu brzydkie. Kamera nie jest częścią kraboptaka.
No po prostu nie.
Podsumowując: kamera i postać gracza to dwa oddzielne byty, które na siebie oddziałują.
Dlatego sugeruję dodać kamerę jako oddzielny wierzchołek w scenie Main
w edytorze Godota.
Ustawienia kamery
Dodajmy do sceny Main
wierzchołek Camera2D
. W inspektorze po prawej stronie zaznaczmy pole Current
by włączyć kamerę. Ustawmy też pole Anchor mode
na Fixed TopLeft
żeby lewy górny róg tego, co widzi kamera, był związana ze środkiem układu współrzędnych.
W przypadku ustawienia Drag Center
, ze środkiem układu współrzędnych związane jest środek tego, co widzi kamera. Różnica wygląda tak (przy założeniu, że pozycja obrazu tła to (0,0)
):
Oprócz tego przesuńmy kamerę tak, by gracz widział więcej przed sobą, a mniej za sobą. W tym celu ustawmy offset
na 25% szerokości ekranu: u mnie -0.25 * 480
. To jest rozwiązanie tymczasowe – pod koniec tego wpisu uzależnimy offset
kamery od szerokości okna.
To tyle w Godocie. Pora na Rust.
Struktura świata gry
Do zarządzania ruchem kamery potrzebujemy informacji o pozycji kraboptaka. Tylko jak połączyć te dwa wierzchołki, skoro są osobne i żyją obok siebie? Ano nadwierzchołkiem. Jest nim scena Main
w Godocie.
Aby zarządzać relacjami między nimi, dodajmy strukturę w Ruscie, która będzie związana właśnie z tą sceną. Zacznijmy od dodania nowego NativeScriptu do wierzchołka Main
. Ja planuję nazwać odpowiadającą mu strukturę World
, dlatego wpisuje tę nazwę w polu ClassName
.
W katalogu src
dodajmy plik world.rs
. W nim stwórzmy strukturę World
, by mogła współdziałać z elementami Godota. Powinna wyglądać mniej więcej tak:
#[derive(NativeClass)]
#[inherit(Node2D)]
pub struct World;
#[methods]
impl World {
pub fn new(_owner: &Node2D) -> Self {
World
}
.
.
.
}
Tym razem przyjmę nieco inną strategię niż w poprzednim wpisie, w którym w polach struktury zapisywaliśmy referencje do jej podwierzchołków, a potem przy każdym użyciu wierzchołka przekształcaliśmy je na odpowiedni typ. Tym razem nie dodamy pól, lecz funkcje, które będą zwracać wierzchołki dzieci już odpowiednio przekształcone. W skrócie: chcemy wyabstrahować cały proces w funkcję, oszczędzić sobie myślenia i pisania w kółko tego samego. Napiszmy funkcję, która zwróci nam wierzchołek postaci gracza:
fn get_crabby(&self, owner: &Node) -> TRef<'_, Node2D> {
owner
.get_node("./Player")
.and_then(|cam| unsafe { cam.assume_safe().cast::<Node2D>() })
.expect("There is no crabby")
}
Robimy dokładnie to, co dotychczas. Najpierw prosimy ownera o podwierzchołek o nazwie Player, prosimy o bezpieczny dostęp do niego i przekształcamy go na typ Node2D
. Na sam koniec rozpakowujemy go jeszcze funkcją expect
. Dzięki temu możemy dostać się do "środka" typu Option
, czyli do wierzchołków, i nie martwić się rozpakowywaniem Option
później. Gdyby coś poszło nie tak, w konsoli pojawi się komunikat, który jest argumentem funkcji except
. I voilà!
Dlaczego przekształcamy kraba na Node2D
skoro jest wierzchołkiem RigidBody2D
? Dlatego, że każde RigidBody2D
jest Node2D
, a zależy nam tylko na pobraniu pozycji i jest to wystarczające. Ale możecie przekształcić go sobie na RigidBody2D
.
Analogicznie stwórzmy funkcję, która zwróci nam kamerę:
fn get_camera(&self, owner: &Node) -> TRef<'_, Camera2D> {
owner
.get_node("./Camera2D")
.and_then(|cam| unsafe { cam.assume_safe().cast::<Camera2D>() })
.expect("There is no camera")
}
Ruch kamery
Teraz, gdy mamy dostęp do gracza i kamery, zajmiemy się obsługą relacji między nimi. Kamera ma podążać za postacią gracza, ale tylko w osi x. Dlatego w funkcji _physics_process
zajmiemy się aktualizacją pozycji kamery. Pozycja x kamery ma "śledzić" pozycję x kraboptaka, a pozycja y ma pozostać bez zmian. Dla czytelności do ich obliczania użyjemy zmiennych camera_x
i camera_y
:
#[export]
fn _physics_process(&self, owner: &Node2D, _delta: f64) {
// Get TRef to camera Node.
let camera = self.get_camera(owner);
// Change only x position of camera to make it follow crabby.
let new_position = {
let camera_x = self.get_crabby(owner).global_position().x;
let camera_y = camera.global_position().y;
Vector2::new(camera_x, camera_y)
};
// Set camera to new position.
camera.set_global_position(new_position);
}
Dodanie świata do gry
Tak przygotowaną strukturę, należy teraz zainicjować w pliku lib.rs
. Po zmianie wygląda on tak:
use gdnative::prelude::{godot_init, InitHandle};
mod player;
mod world;
// Function that registers all exposed classes to Godot.
fn init(handle: InitHandle) {
handle.add_class::<player::Player>();
handle.add_class::<world::World>();
}
// Macros that create the entry-points of the dynamic library.
godot_init!(init);
Ruch kraboptaka
Żeby zobaczyć efekt należy jeszcze zmodyfikować ruch postaci gracza – póki co nie porusza się on w poziomie. Aby to zmienić dodajmy do struktury Player
pole x_speed
. Zainicjujmy je w funkcji _init
na np. 100. Potem w pliku player.rs
w funkcji _process_physics
dodajmy linijkę:
#[export]
fn _physics_process(&mut self, owner: &RigidBody2D, _delta: f64) {
…
// Set x of linear velocity.
owner.set_linear_velocity(Vector2::new(self.x_speed, owner.linear_velocity().y))
}
W ten sposób wprawimy go w ruch w osi x ze stałą prędkością. Za pierwszym razem zrobiłam tę modyfikację w funkcji _ready
, ale nie był to udany pomysł. Kraboptak stopniowo zwalniał, by w końcu zupełnie się zatrzymać. A to dlatego, że funkcja _ready
wywoływana jest tylko raz, gdy element dodawany jest do sceny.
A, jeszcze jedno. Żeby zobaczyć różnicę, trzeba dodać jakieś tło w edytorze Godota do sceny Main
jako Sprite
. Zwróćmy szczególną uwagę, żeby ten wierzchołek był ponad wierzchołkiem gracza w hierarchi sceny, bo inaczej zasłoni nam go. Jeśli kamera porusza się względem dodanego tła, wszystko jest dobrze. Ostatecznie postać gracza i tak wyleci poza tło. Nie martwmy się tym na razie, to rozwiązanie tymczasowe. Służy ono tu tylko do sprawdzenia czy nasz ruch kamery działa. Być może w którymś z późniejszych wpisów zajmiemy się generowaniem tła.
Uzależnienie ustawienia kamery od szerokości okna
Na koniec uzależnijmy jeszcze ustawienie kraboptaka od szerokości ekranu, żeby nie robić tego ręcznie przy każdej zmianie wymiarów okna. Zrobimy to przez ustawienie własności offset
kamery w Ruscie.
W pliku world.rs
w funkcji _ready
pobierzmy wymiary okna gry i ustawmy offset kamery na 1/4 szerokości okna:
#[export]
fn _ready(&self, owner: &Node2D) {
// Set camera offset to 1/4 of screen width.
let camera_offset = {
let screen_width = owner.get_viewport_rect().size.width;
Vector2::new(-0.25 * screen_width, 0.0)
};
self.get_camera(owner).set_offset(camera_offset);
}
Jeśli macie ochotę zmienić wymiary okna możecie, to zrobić w ustawieniach projektu, w edytorze Godota.
Project → Project Settings… → Display
(w lewym menu pionowym) → Window
.
Efekt
Podsumowanie
W ten sposób dodaliśmy do gry kamerę i zajęliśmy się zarządzaniem jej ruchem. Nie wprowadziło to znaczących zmian w odbiorze gry, mimo że zmieniliśmy sporo rzeczy. Był to istotny krok przygotowawczy do dalszej części projektu. W przyszłym wpisie zajmiemy się już generowaniem przeszkód. Cieplutko zapraszam.
Jeśli macie ochotę podzielić się wrażeniami, pomysłami, nadziejami albo innymi uwagami, serdecznie zachęcam.
2 odpowiedzi na “CrabbyBird #2 Poruszanie kamerą”
Także, zamiast przeczytać dziś kolejny wpis z serii, zostawię komentarz. Często w listingach kodu widywałem `unsafe`. Generalnie odradza się jego używanie. Pytanie więc, czy tak częste tego używanie wymusza sam godot, posiadasz zawsze pewność że wszystko będzie dobrze, czy tak po prostu tego używałaś? Na każde `unsafe` zerkałem dość sceptycznie i jestem ciekaw Twojego podejścia do tego.
Nie, piszę aż kompilator przestanie bić mnie po łapach 🙂 Co do zasady, to kieruję się tym co w dokumentacji biblioteki `gdnative`. W dotychczasowych wpisach `unsafe` wiąże się z użyciem funkcji `assume_safe`
Faktycznie, w pierwszej wersji pierwszego wpisu nadużywałam `unsafe`, ale już to poprawiłam. Była to pozostałość z przepisywania kodu działającego w wersji 0.8 na wersję 0.9. Ot, nie zauważyłam, że wraz ze zmianą wersji `unsafe` nie był już konieczny przy funkcjach `_ready` i `_physics_process`.