Tag: rust

  • CrabbyBird #5 Obsługa kolizji

    CrabbyBird #5 Obsługa kolizji

    Dzisiejszy wpis jest szóstym odcinkiem z serii, w której w ramach nauki Rusta i Godota pisze sobie klon FlappyBirda.

    Na tym etapie kraboptak potrafi skakać i zderzać się z przeszkodami. Podąża za nim kamera, a wokół niej tworzy się świat.

    Wszystko fajnie, ale co to za gra, której nie da się przegrać? Poza tym generowanie przeszkód zaczyna się w z góry określonej pozycji, a chcemy by zachodziło dopiero po przejęciu kontroli nad kraboptakiem przez gracza. Te problemy rozwiążemy przez zmiany stanu gry i to jest dzisiejszym celem.

    Pełny kod powstały w tym wpisie można znaleźć na githubie.

    Stan gry

    Póki co kraboptak umie tylko reagować na wciśnięcie spacji skokiem. Chcielibyśmy, żeby gra zaczęła się od lotu kraboptak, by gracz mógł przejąć kontrolę bez obawy, że przegra zanim gra się w ogóle rozpocznie. W trakcie lotu nie powinny być też generowane przeszkody. Właściwa gra powinna zacząć się dopiero po przejęciu kontroli nad kraboptakiem, pozwalając graczowi odrobinę przywyknąć do jego sposobu poruszania. Gdy gra się już rozpocznie, jakakolwiek kolizja postaci powinna zakończyć się przegraną.

    Taką wizję można zrealizować zmianami stanu kraboptak. W zależności od niego będziemy uruchamiać lub zatrzymywać generowanie przeszkód.

    Wyróżnijmy trzy takie stany:

    • flying – lot bez kontroli przez gracza,
    • flapping – pełna kontrola przez gracza, dokładnie to co mamy teraz,
    • dead – przegranko.

    Zmiany między stanami związane są z bardzo jasnymi zdarzeniami:

    • flying -> flapping — pierwsze wciśnięcie spacji,
    • flapping -> dead — kolizja kraboptaka z przeszkodą lub ziemią.

    Przygotowałam kilka animacji, które będziemy zmieniać przy okazji zmiany stanu.



    Kolejne klatki możecie znaleźć w katalogu assets: flapping, flying i game over. Wskazówki jak zrobić z nich animację w godocie możecie znaleźć w jednym z poprzednich wpisów.

    Skoro stany mamy wstępnie ustalone, przygotujmy sobie enuma w pliku player.rs,

    pub enum PlayerState {
        Flying,
        Flapping,
        Dead,
    }

    A do struktury Player dodajmy pole state: PlayerState. Zainicjujmy go w funkcji new wartością: PlayerState::Flying, bo grę zawsze rozpoczynamy w takim stanie.

    Teraz możemy się zastanowić, co będzie się działo z krabem w różnych stanach i sytuacjach. Zacznijmy od wciśnięcia przez gracza spacji. Należy wtedy sprawdzić aktualny stan.

    Jeśli krab lata, gracz powinien przejąć nad nim kontrolę. Dlatego zmieniamy jego stan na Flapping i wywołujemy funkcję flap, aby podskoczył.

    Jeśli w momencie wciśnięcia spacji aktualnym stanem jest Flapping, chcemy by krab podskoczył. Wywołujemy więc funkcję flap.

    Jeśli krab jest aktualnie martwy, a gracz wcisnął spację, nie robimy nic.

    Powyższe zachowania zakodujmy w funkcji physics_process:

    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_flap") {
            match self.state {
                PlayerState::Flying => {
                    self.state = PlayerState::Flapping;
                    self.flap(owner);
                }
                PlayerState::Flapping => self.flap(owner),
                PlayerState::Dead => {}
            }
        }
        .
        .
        .
    }

    Kolejnym miejscem, w którym należy rozważyć aktualny stan kraboptaka, jest dalsza część funkcji physics_process. Kod z którego korzystaliśmy do tej pory umieśćmy w przypadku, gdy aktualny stan to Flapping. Dla pozostałych przypadków stworzymy odpowiednie funkcje: fly i dead.

    match self.state {
        PlayerState::Flapping => {
            owner.set_gravity_scale(10.);
            // Assure that player can't face up more than max facing_angle
            if owner.rotation_degrees() < 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);
            }
    
            // Set x of linear velocity.
            owner.set_linear_velocity(Vector2::new(self.x_speed, owner.linear_velocity().y))
        }
        PlayerState::Flying => {
            self.fly(owner, delta as f32);
        }
        PlayerState::Dead => {
            self.dead(owner);
        }
    }

    Jak można się domyślić, funkcja fly określa, jak kraboptak lata. Ustawimy w niej prędkość w osi x żeby dodać do ruchu kraba trochę szaleństwa, uzależnimy jego pozycję y od pozycji x używając funkcji sinus. Sprawi to, że będzie się delikatnie wahał. Poza tym uruchomimy animację lotu kraboptak.

    fn fly(&self, owner: &RigidBody2D, delta: f32) {
            owner.set_gravity_scale(0.0);
            // Set horizontal velocity to move player forward with x_speed.
            // Don't change vertical position.
            owner.set_linear_velocity(Vector2::new(self.x_speed, 0.0));
            let pos = owner.global_position();
            // Make player swing a little.
            owner.set_global_position(Vector2::new(pos.x, pos.y + (pos.x * delta).sin()));
    
            // Start flying animation.
            self.get_jump_animation(owner).play("fly", true);
        }

    Z kolei w funkcji dead zatrzymujemy ptakokraba ustawiając jego prędkość poziomą na 0 i uruchamiamy animację gameover.

    fn dead(&self, owner: &RigidBody2D) {
        owner.set_linear_velocity(Vector2::new(0.0, owner.linear_velocity().y));
        self.get_jump_animation(owner).play("gameover", false);
        }

    Jeśli chodzi o strukturę Player to w zasadzie tyle, obsłużyliśmy wszystkie stany. Jednak to nie wszystko. Potrzebujemy wysłać do świata informację, by zaczął już generować przeszkody. Poza tym player potrzebuje informacji o kolizjach. Zrobimy to używając sygnałów, podobnie jak w poprzednim wpisie. Chodźcie, będzie fajnie.

    Sygnały

    Sygnały godotowe

    Sygnały posłużą nam do komunikacji świata z postacią gracza. Zacznijmy od czegoś klasycznego: wykrywanie kolizji. Będzie to coś podobnego, co robiliśmy przy ukrywaniu animacji dymku po tym jak krab przestaje skakać. Połączyliśmy tam swoją funkcję ze zdarzeniem skończenia się klatek animacji. Podobnie postępowaliśmy też przy Notifierze.

    Tutaj chcemy zrobić swoje rzeczy, gdy kraboptak uderza w ziemię lub rurę. Przejdźmy do edytora Godota i otwórzmy scenę Player. Po prawej stronie obok zakładki Inspector mamy zakładkę Node w której jest lista sygnałów, które umie rzucać postać. Interesuje nas sygnał body_entered. Klikając go możemy połączyć go z funkcją-reakcją. Ja nazwałam ją _on_player_body_entered, a właściwie to sama się tak nazwała, bo zmieniłam tylko litery na małe. Po podłączeniu wygląda to tak:

    Playerowe sygnały

    Uwaga
    Jak najedziemy myszką na sygnał body_entered pojawi nam się taki komunikat:

    Warning

    U mnie śmiga bez ustawiania tych wartości, nie do końca wiem czemu. Ale jeśli u Was by nie działało, może spróbujcie posłuchać się tego komunikatu. Być może są to warunki, żeby wykryć kolizję z innym RigidBody2D?

    Mając wszystko ustawione w Godocie, pora przejść do Rusta. W pliku player.rs dodajmy funkcję o tej samej nazwie, która podłączyliśmy pod sygnał body_entered. U mnie _on_player_body_entered. Jej argumentami są standardowo self, owner i jeszcze Node z którym kraboptak wszedł w kolizje.

    W momencie kolizji, chcemy zmienić stan kraba na Dead.

    // Function connected with body_entered() event from Player node.
    #[export]
    fn _on_player_body_entered(&mut self, _owner: &RigidBody2D, _node: Ref<Node>) {
        self.state = PlayerState::Dead
    }

    Gdy kraboptak zderzy się z przeszkodą zatrzymuje się, uruchamia odpowiednią animację i zaczyna spadać. Nie reaguje też na wciskanie spacji. Bardzo dobrze.

    Nasze własne sygnały

    Teraz chcemy dać znać grze, że gracz przegrał. Do tego również zastosujemy sygnały, ale już własne, a nie domyślne.

    Źródłem sygnału o zakończeniu gry, będzie struktura Player. W pliku player.rs tuż przed strukturą Player dodajmy atrybut:
    #[register_with(Self::register_signals)].
    Dzięki temu będziemy mogli skorzystać z funkcji register_signal, w której dodamy swój sygnał. Nazwijmy go game_over.

    fn register_signals(builder: &ClassBuilder<Self>) {
            builder.add_signal(Signal {
                name: "game_over",
                args: &[],
            });
    }

    Po tej deklaracji możemy już emitować nasz nowy sygnał. Zróbmy to w momencie kolizji, czyli w funkcji _on_player_body_entered. Wygląda ona teraz tak:

    // Function connected with body_entered() event from Player node.
    #[export]
    fn _on_player_body_entered(&mut self, owner: &RigidBody2D, _node: Ref<Node>) {
        self.state = PlayerState::Dead;
        // Emit game over signal
        owner.emit_signal("game_over", &[]);
    }

    W ten sposób ustawiliśmy emmitera sygnału. Zajmijmy się teraz subskrajberem, czyli obiektem zainteresowanym odbieraniem tego komunikatu. Będzie nim struktura World, która chce wiedzieć kiedy gra się skończy.

    Uwaga

    Po zbudowaniu projektu i otwarcia go w Godocie, możemy znaleźć nasz sygnał pośród sygnałów, które umie emitować postać gracza.

    Nasz sygnał pośród innych

    Moglibyśmy teraz połączyć ten sygnał z funkcją w strukturze World w edytorze Godota, tak jak robiliśmy to poprzednio. Tym razem jednak zróbmy to w Ruscie.

    Jak już ustaliliśmy, sygnałem zainteresowana jest struktura World. Chodźmy zatem do pliku world.rs. Musimy połączyć sygnał struktury Player z funkcją w strukturze World. Zrobimy to w funkcji _ready (w pliku world.rs). Na jej końcu dodajmy kod:

    // Connect game over signal.
    self.get_crabby(owner)
        .connect(
            "game_over",
            owner,
            "handle_game_over",
            VariantArray::new_shared(),
            1,
        )
        .expect("Problem with connecting `game_over` signal");

    Bierzemy postać kraba i łączymy jego sygnał game_over funkcją connect ze strukturą World ukrytą w argumencie owner.
    W GDScripcie wyglądałoby to w skrócie tak.
    <wierzchołek_źródłowy>.connect(<nazwa_sygnału>, <docelowy_wierzchołek>, )

    Pozostałe argumenty są ustawione domyślnie. W Ruscie musimy je podać. Zatem u nas funkcja przyjmuje jako argumenty kolejno:

    • nazwę sygnału (w typie GodotString),
    • zainteresowaną strukturę, u nas Game, jednak funkcja chce, by było ona typiu Some(Object),
    • nazwę funkcji która będzie funkcją-reakcją (znowu typu GodotString),
    • listę argumentów dla metody związanej z sygnałem,
    • flagi. Wartość domyślna to 1.

    Teraz zostało tylko stworzyć funkcję-reakcję w strukturze World. Póki co, będzie to tylko wyświetlanie napisu "Game Over" w konsoli. Zajmiemy się nią dokładnie w dalszych wpisach.

    #[export]
    fn handle_game_over(&self, _owner: &Node2D) {
        godot_print!("Game Over!")
        // TODO game over.
    }

    Teraz w przypadku kolizji kraboptaka z czymkolwiek dostajemy w konsoli komunikat o końcu gry.

    Przejęcie kontroli

    Zajmijmy się teraz sygnałem do świata, że gracz przejął kontrolę i można już generować rury. Sposób będzie analogiczny do tego, który zastosowaliśmy przed chwilą.

    Na początek chodźmy do pliku world.rs. Proponuję dodać do świata pole obstacle_status, które będzie kontrolować czy rozpocząć generowanie rur. W funkcji _init ustawmy go na false. W funkcji physics_process w pliku world.rs przy fragmencie w którym decydujemy, czy potrzeba nowej rury uzupełnijmy warunek:

    // Emit signal to pipe_manager if pipe is needed.
    if camera_x_end - self.last_pipe_x > self.pipe_density && self.obstacle_status {
        …
    }

    Dzięki temu generowanie rur rozpocznie się dopiero, gdy wartość pola obstacle_status zmieni się na true. A kiedy się zmieni? Wtedy gdy struktura World wyłapie sygnał od struktury Player.

    I teraz zaczyna się część analogiczna do tej powyższej:

    1. Zarejestrujmy sygnał. Będzie od pochodził ze struktury Player. (Dodaliśmy do niej już wcześniej atrybut #[register_with(Self::register_signals)]. Jeżeli chcielibyśmy rejestrować sygnał w innej strukturze, trzeba o tym pamiętać.)
      Nazwijmy go control_started. W funkcji register_signal dodajmy:
    fn register_signals(builder: &ClassBuilder<Self>) {
          …
          builder.add_signal(Signal {
                name: "control_started",
                args: &[],
            });
        }
    1. Stwórzmy funkcję-reakcję. Strukturą zainteresowaną tym komunikatem będzie World. Reakcją na przejęcie kontroli przez gracza powinno być generowanie przeszkód. Osiągniemy to przez zmianę wartości pola obstacle_status. Dodajmy w pliku world.rs funkcję:

      #[export]
      fn handle_control_start(&mut self, _owner: &Node2D) {
      // Start obstacles generation.
      self.obstacle_status = true;
      }
    2. Połączmy sygnał z funkcją-reakcją w funkcji ready struktury World.

    …
    // Connect signal to start generating pipes.
    self.get_crabby(owner)
      .connect(
        "control_started",
        owner,
        "notify_control_start",
        VariantArray::new_shared(),
        1,
        )
      .expect("Problem with connecting `control_started` signal");
    1. Wyemitujmy nowy sygnał przy pierwszym wciśnięciu spacji. Rozpoznamy to po aktualnym stanie kraba. Jeśli krab lata, spacja nie została jeszcze wciśnięta. Sygnał należy wyemitować w funkcji _physics_process, przy sprawdzaniu czy gracz wcisną spację:
    let input = Input::godot_singleton();
    if Input::is_action_pressed(&input, "ui_flap") {
        match self.state {
            PlayerState::Flying => {
                owner.emit_signal("control_started", &[]);
                self.state = PlayerState::Flapping;
                self.flap(owner);
            }
            PlayerState::Flapping => self.flap(owner),
            PlayerState::Dead => {}
        }
    }

    Ostatnie szlify

    W ten sposób obsłużyliśmy zaplanowane interakcje między postacią kraba a światem gry. Zostało nam jeszcze kilka drobnych szlifów.

    Mianowicie w przypadku, gdy ptak jest martwy wciąż odbija się od przeszkód. W oryginalnym FlappyBird, kolizje z rurami stają się nieaktywne. Zajmiemy się teraz odtworzeniem tego zachowania. Zatem nowy cel to wyłączenie kolizji z rurami i zachowanie kolizji z ziemią w przypadku przegrania rozgrywki.

    Collision layers

    Godot dostarcza całkiem fajne narzędzia do obsługi kolizji – warstwy i maski.

    Każdy obiekt który umie w kolizje ma 20 warstw z którymi może współpracować. Można je wyklikać w edytorze Godota. To te niepozorne kwadraciki. Widzimy tam dwie grupy kwadracików:

    • Layer – warstwy na których obiekt się pojawia. Domyślnie wszystkie ciała pojawiają się na warstwie 1.
    • Mask – warstwy na których obiekt szuka kolizji. Obiekty które są na niezaznaczonych warstwach są ignorowane. Domyślna warstwa to ta pierwsza.

    Żeby było wygodnie ustawimy teraz wszystkie rodzaje obiektów na oddzielnych warstwach. Możemy nadać im nazwy w ustawieniach projektu.

    Zmiana nazwy warstwy

    Samo ustawienie tych warstw można wyklikać w edytorze zaznaczając odpowiednie kwadraciki. My zrobimy to jednak w Ruscie, bo i tak potem będziemy to modyfikować w zależności od wydarzeń w grze. Przedtem przyjrzyjmy się jednak tym kwadracikom w Godocie. Gdy najedziemy na nie myszką, pojawia się takie małe okienko z nazwą
    warstwy i jej wartością.

    Wartość warstw

    Zauważmy, że wartość to 2^Bit. Wykorzystamy to przy ustawianiu warstw kolizji w Ruscie.
    Są co najmniej dwie możliwości: albo funkcją set_collision_layer albo set_collision_layer_bit.

    • set_collision_layer_bit ( int bit, bool value ) – funkcja przyjmuje w argumentach bit i wartość logiczną, która chce mu przypisać. Nadaje się tylko do ustawienia jednaj warstwy na raz,

    • set_collision_layer ( int value ) – funkcja w argumencie przyjmuje wartość, o której wspomnieliśmy wcześniej. Jeszcze fajniejsze jest to, że możemy w niej ustawić kilka warstw na raz sumując ich wartości.
      Na przykład jeśli chcemy, by obiekt znajdował się jednocześnie na warstwie pierwszej (czy jak kto woli zerowej) i trzeciej (licząc od zera – drugiej) wartość wyniesie: 2^0 + 2^2.

      Zupełnie analogicznie działa ustawianie masek kolizji . Do tego służą odpowiednio funkcje set_collision_mask_bit i set_collision_mask.

      Przenieśmy to teraz do CrabbyBirda. Przy tworzeniu gracza gdzieś na początku funkcji _ready ustawmy odpowiednio collision_layer. Zgodnie z tym jak nazwaliśmy wcześniej warstwy, przeznaczyliśmy dla gracza pierwszą warstwę (licząc od zera).

      Ustawmy od razu maski. Chcemy by na początku krab wchodził w kolizję zarówno z rurami jak i ziemią, dla których przeznaczyliśmy warstwy drugą i trzecią. Przypominam, że liczymy od zera.

      fn _ready(&mut self, owner: &RigidBody2D) {
      owner.set_collision_layer(1); // 2^0
      owner.set_collision_mask(6); // We want to set collision with 1 and 2 mask layer. 2^1 + 2^2 = 6
      …
      }

      Przejdźmy teraz do struktury Pipe i Base, w której analogicznie w funkcji _ready ustawmy collision_layer:
      owner.set_collision_layer(2); // 2^1(dla rur) i owner.set_collision_layer(4)(dla ziemi).

    Póki co gra będzie zachowywała się dokładnie tak, jak wcześniej. Jednak przygotowaliśmy wszystko pod ostatnie szlify. Zostało nam tylko wyłączyć kolizje między rurami a krabem w przypadku skuchy, czyli w funkcji dead. Dodajmy w niej:

    unsafe fn dead(&self, mut owner: RigidBody2D) {
        …
        owner.set_collision_mask(4); // 2^2
        …
        }

    W ten sposób "unieważniamy" zderzenia kraba z rurami, a pozostawiamy z ziemią.

    Efekt



    Podsumowanie

    W dzisiejszym wpisie zrobiliśmy dużo rzeczy. Grę można juz przegrać. Następnym razem zajmiemy się liczeniem punktów. Cieplutko zapraszam.

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

  • CrabbyBird #4 Generowanie świata gry – cześć II

    CrabbyBird #4 Generowanie świata gry – cześć II

    Dzisiejszy wpis jest piątym odcinkiem z serii, w której w ramach nauki Rusta i Godota pisze sobie klon FlappyBirda. Dziś zajmiemy się kontynuacją generowania świata gry: tym razem dodamy do niego przeszkody.

    Pomysł jest trochę podobny jak przy generowaniu ziemi, którym zajmowaliśmy się w poprzednim wpisie. Jednak tym razem podejdziemy do problemu nieco inaczej – użyjemy godotowego mechanizmu sygnałów.

    Pełny kod powstały w tym wpisie można znaleźć na githubie.

    Dzisiejszy cel

    Dzisiejszy problem jest nieco podobny do tematu ostatniego wpisu. Rozwiązanie mogłoby wyglądać w zasadzie tak samo, ale żeby się nie zanudzić, spróbujemy rozwiązać go trochę inaczej i porównać te dwa podejścia. Ogólnie celem jest dodawanie do gry rur i usuwanie ich, gdy są już zbędne. Zacznijmy więc!

    Ustawienia sceny rury

    Jak zwykle zaczniemy od ustawienia scen w edytorze Godota. W pierwszej kolejności zajmijmy się samą rurą. Rura w naszej grze będzie składała się tak na prawdę z dwóch rur – górnej i dolnej. Scena wygląda tak:

    Scena rury

    A tutaj są ustawienia poszczególnych elementów:

    Ustawienia rury

    Po pierwsze zwróćmy uwagę, że w zakładce Offset wyłączona jest opcja centered. Dzięki temu pozycja obrazka wyznaczana jest względem jego lewego górnego rogu.
    Zarówno do rury górnej jak i dolnej używamy tej samej tekstury. Jedną z nich obróćmy o 180 stopni. Żeby obydwie były równo ułożone w pionie, trzeba jedną z nich przesunąć o szerokość obrazka (którą można podejrzeć w okienku edycji tekstury). Stąd ustawiłam rurze górnej (tej odwróconej) pozycję x na 52.
    Poza tym pozycję y ustawiłam na 90 i -90. Wystarczy jeszcze dodać dwa CollisionShape tak, żeby pokrywały dodane Sprajty.

    Czujnik widoczności

    Podobnie jak przy generowaniu ziemi, dodajmy jeszcze jeden element – VisabilityNotifier2D. Będzie on pilnował, czy rury są widoczne w kamerze i pozwoli je usuwać, gdy są niepotrzebne. Ponieważ w naszej grze kamera porusza się zawsze w prawo, element znika z jej zasięgu gdy miniemy jego prawy brzeg. Dlatego pozycję czujniczka ustawiamy na szerokość obrazka rury, by nie usuwać jej zbyt wcześnie.

    Sygnały, które umie wysyłać czujnik, możemy zobaczyć w zakładce Node.

    Sygnały

    Nas interesuje funkcja screen_exited. Powiążmy z nią funkcję, która ma obsłużyć zdarzenia wyjścia rury z widoku kamery. U mnie nazywa się: _on_pipe_screen_exited.

    Teraz gdy wszystko jest wstępnie przygotowane, dodajmy NativeScript do stworzonej sceny Pipe i chodźmy do Rusta. Jeśli nie miałeś jeszcze okazji dodawać NativeScriptu do wierzchołka to tutaj możesz znaleźć podpowiedź jak to zrobić.

    Rzeczy, za które rura może odpowiadać sama

    Stwórzmy teraz strukturę rury w Ruscie. Będzie ona bardzo podobna do struktury Base. Dodajmy plik pipe.rs a w nim strukturkę:

    
    #[derive(NativeClass)]
    #[inherit(StaticBody2D)]
    pub struct Pipe;
    
    #[methods]
    impl Pipe {
        pub fn new(mut _owner: &StaticBody2D) -> Self {
            Pipe
        }
    
        #[export]
        pub fn _on_pipe_screen_exited(&self, owner: &StaticBody2D) {
            println!("Remove pipe!");
            owner.queue_free();
        }
    }
    

    Żeby rura mogła pilnować swojego bycia albo niebycia, musi być w kontakcie z Notifierem. Zapewnia to funkcja on_pipe_screen_exited. Jeśli chcielibyście zobaczyć odrobinę dokładniejsze omówienie tego problemu, to odsyłam do takiego samego rozwiązania przy generowaniu ziemi.

    I to wszystko. Każda rura usuwa się teraz sama.

    Edit

    Otóż nie działa to do końca dobrze. Jak się przyjrzymy, to czasem nieoczekiwanie znikają dopiero co dodane rury. Dzieje się tak, gdy gracz zderzy się z przeszkodą i ruszy się minimalnie w tył. Wtedy kamera, która go śledzi też się cofnie. Jeśli zdarzy się, że w tym momencie na scenie, na skraju widoczności kamery pojawi się nowa rura, to takie minimalne cofnięcie powoduje, że rzuci ona sygnał screen_exited, co spowoduje jej usunięcie.

    Rozwiązaniem tego problemu jest zmiana pozycji czujnika widoczności w scenie Pipe.tscn na taki, by rura znikała nieco później. Ja ustawiłam jej współrzędną x na 52 px.

    PipeManager

    Pipe manager w Godocoe

    Podobnie jak w przypadku ziemi, potrzebujemy Aranżatora Rur. Tym razem ograniczymy trochę jego zadanie w porównaniu z menadżerem ziemi. Będzie odpowiedzialny tylko za dodawanie rur. Za to jak często będą sie one generować będzie odpowiadała struktura World. Ale powoli.

    W pliku pipe_manager.rs stwórzmy strukturę PipeManager.

    pub struct PipeManager {
        pipe_template: Option<Ref<PackedScene>>,
        maximal_sprite_height: f32, // Maximal pipe height.
        minimal_sprite_height: f32, // Minimal pipe height.
        pipe_offset: f32,           // Half of space between up and down pipe.
    }

    Rury muszą sensownie mieścić się na ekranie. Na przykład otwór między rurami nie powinien być poza widocznym obszarem. Lepiej opisze to obrazek:

    Poziomy rur

    Jak widzicie, kraboptak jest naprawdę szczęśliwy tylko, gdy rury są ułożone tak akurat. Właśnie do tego posłużą nam pola maximal_sprite_height, minimal_sprite_height i pipe_offset.

    Wymiary rur

    W funkcji new inicjalizujemy co możemy, a funkcja _ready jest w zasadzie taka sama jak ta w BaseManager z dokładnością do ścieżki do pliku sceny.

    #[methods]
    impl PipeManager {
        pub fn new(_owner: &Node2D) -> Self {
            PipeManager {
                pipe_template: None,
                maximal_sprite_height: 640.0,
                minimal_sprite_height: 50.0,
                pipe_offset: 90.0,
            }
        }
    
        #[export]
        fn _ready(&mut self, owner: TRef<Node2D>) {
            self.pipe_template = ResourceLoader::godot_singleton()
                .load("res://scenes/Pipe.tscn", "", false)
                .and_then(|scene| {
                    unsafe { scene.assume_safe().cast::<PackedScene>() }.map(|x| x.claim())
                });
    
            match &self.pipe_template {
                None => godot_print!("Could not load child scene. Check name."),
                _ => {}
            }
    }

    Zanim zajmiemy się logiką odpowiedzialną za dodawanie rur, przyjrzyjmy się z grubsza godotowemu mechanizmowi sygnałów i tego, jak możemy go wykorzystać.

    Sygnały

    W skrócie i uproszczeniu: jakiś obiekt rzuca komunikat, że coś się stało a na niego reagują inne zainteresowane obiekty.

    Sygnały posłużą nam do komunikacji świata z Aranżatorem Rur. Tak na prawdę to juz ich używaliśmy, na przykład przy ukrywaniu animacji dymku po tym jak krab przestaje skakać, albo usuwaniem kafelków ziemi, czy rur gdy wyjdą z zakresu widoczności kamery. Łączyliśmy tam swoje funkcje ze zdarzeniem. Były to sygnały, które wypluwają elementy godotowe.
    Tym razem zrobimy swoje własne, niestandardowe sygnały, rzucane i odbierane po stronie rusta.

    Mamy dwa elemty związane z rurami: świat, który wie kiedy rura jest potrzebna (bo ma dostęp do kamery) i aranżatora rur, który wie jak taką rurę dodać. Chcemy je skomunikować. Emiterem zdarzenia będzie świat – obliczy sobie czy powinna pojawić się już nowa rura. Natomiast odbiorcą będzie aranżator, który doda rurę w określonej pozycji x. Pozycję y rury i sposób jej dodania określi już sam.

    Ok, skoro plan jest jasny, to popiszmy sobie.

    Sama funkcja add_pipe też wygląda podobnie do jej odpowiednika z BaseManagera, czyli funkcji spawn_one, którą możecie znaleźć w poprzednim wpisie.

    #[export]
    fn add_pipe(&mut self, owner: &Node2D, x: f32, y: f32) {
        match self.pipe_template {
            Some(ref pipe_obj) => {
                let pipe = unsafe {
                    // unsafe because `assume_safe` function using.
                    pipe_obj
                        .assume_safe()
                        // Get instance of `PackedScene`.
                        .instance(0)
                        // Can be casted to `StaticBody2D` but `Node2D` is enough.
                        .and_then(|node| node.assume_safe().cast::<Node2D>())
                        .expect("Could not create base instance.")
                };
                pipe.set_position(euclid::Vector2D::new(x, y));
                // Add base to manager.
                owner.add_child(pipe, false);
           }
           None => print!("Pipe template error."),
        }
    }

    Skoro mamy już funkcję, która umie dodawać rurę w podanej pozycji, zastanówmy się jak i kiedy jej użyć. Jak już wspominałam, rura powinna dodać się, gdy struktura World tak stwierdzi. Przejdźmy do pliku wordl.rs.

    Tuż przed strukturą World dodajmy linijkę:

    .
    .
    .
    #[register_with(Self::register_signals)]
    pub struct World {
        .
        .
        .
    }

    Dzięki temu w funkcji register_signal, będziemy mogli dodać swój sygnał. Nazwijmy go pipe_needed. Wraz z sygnałem chcemy przesłać współrzędną x, w której należy dodać rurę. Weźmy się za jej implementację:

    fn register_signals(builder: &ClassBuilder<Self>) {
        builder.add_signal(Signal {
            name: "pipe_needed",
            args: &[SignalArgument {
                name: "position_x",
                default: Variant::from_i64(100),
                export_info: ExportInfo::new(VariantType::I64),
                usage: PropertyUsage::DEFAULT,
           }],
        });
    }

    Aby dodać sygnał potrzebujemy użyć kilka struktur z biblioteki gdnative: ClassBuilder, Signal i SignalArgument. Funkcji add_signal dajemy w argumencie sygnał, do którego stworzenia potrzebujemy nazwy oraz listy argumentów. Z kolei struktura SignalArgument potrzebuje nazwy argumentu domyślnej wartości opakowanej w strukturę Variant, ustawmy go na 100 (bez jakichś wyraźnych powodów). Nie wiem co robią pozostałe pola w strukturze SignalArgument.
    Po tej deklaracji możemy już emitować nasz nowy sygnał. Tylko kiedy?

    Żeby świat umiał to stwierdzić, potrzebuje trochę więcej danych. Dodajmy do struktury World następujące pola:

    • pipe_density oznaczające zagęszczenie rur,
    • last_pipe_x oznaczające pozycję ostatnio dodanej rury.
    pub struct World {
        .
        .
        .
        // Pipe spawning settings.
        pipe_density: f32,
        last_pipe_x: f32,
    }

    Na podstawie nowych pól i pozycji kamery powinniśmy być w stanie określić, czy rura jest już potrzebna. Przypomnę tylko, że wcześniej wyznaczyliśmy sobie już prawy brzeg tego, co widzi kamera:

    // Get camera view right bound.
    let camera_x_end = {
        let camera_x_start = new_position.x + camera.offset().x;
        camera_x_start + self.screen_size.0
    };

    Skorzystajmy z tej zmiennej.

    // Emit signal to pipe_manager if pipe is needed.
    if camera_x_end - self.last_pipe_x > self.pipe_density {
        self.last_pipe_x = camera_x_end;
        owner.emit_signal("pipe_needed", &[Variant::from_i64(camera_x_end as i64)]);
    }

    Mam nadzieję, że obrazek pomoże w zrozumieniu warunku. Pomarańczowa strzałka oznacza lewą stronę nierówności. Gdy tylko będzie dłuższa, niż strzałka oznaczająca zagęszczenie rur, należy zaktualizować pole last_pipe_x i dodać nową rurę.

    Własnie w tym momencie należy wyemitować wcześniej zarejestrowany sygnał – pipe_needed. Służy do tego funkcja emit signal. W argumentach bierze nazwę sygnału, oraz slajsa argumentów, które będa potrzebne funkcji reakcji. Argumenty z kolei sa opakowane w strukturkę Variant, a to po to, by godot umiał je przesłac między scenami.

    Ok, skoro sygnał został wyemitowany, nauczmy teraz strukturkę PipeManager go dobierać i odpowiednio reagować.
    Chodźmy do pliku pipe_manager.rs.

    Celem bedzie napisanie funkcji-reakcji na sygnał pipe_needed. Zanim zajmiemy się jej implementacją, powiedzmy godotowi, że własnie ona ma być powiązana z tym sygnałem. W funkcji _ready dodajmy poniższy kod.

    #[export]
        fn _ready(&mut self, owner: TRef<Node2D>) {
           .
           .
           .
            // Get emitter of `pipe_needed` signal.
            let emitter = owner
                .get_parent()
                .and_then(|node| unsafe { Some(node.assume_safe()) })
                .expect("Can't get emitter node.");
    
            emitter
                .connect(
                    "pipe_needed",
                    owner,
                    "pipe_needed",
                    VariantArray::new_shared(),
                    0,
                )
                .unwrap();
        }

    Aby połączyć sygnał, potrzebujemy mieć dostęp do nołda, który go emituje. Dostaniemy się do niego za pośrednictwem argumentu owner wywołując funkcję get_parent, a następnie prosząc o referencję do niego funkcją assume_safe.
    Mając już dostęp do emmitera wywołujemy na nim funkcję connect. Za argumenty bierze kolejno: nazwę sygnału, wierzchołek, który ma na niego reagować, nazwę funkcji-reakcji, tablicę argumentów dla tej funkcji oraz flagi (nie zagłębiałam się co oznaczają, zgodnie z dokumentacją domyślną wartością flagi jest 0).

    Zgodnie z powyższym kodem, potrzebujemy teraz funkcji reakcji o nazwie pipe_needed. Ze struktury World otrzymamy w argumentach:

    • współrzędną x w której należy dodac rurę,
    • dolny margines, czyli miejsce poniżej której rura nie powinna się generować; w praktyce bedzie to wysokość kafelka ziemi,
    • wysokość ekranu.

    Takie dane wystarczą, żeby dobrze umieścić rurę w świecie. Napisaliśmy już funkcję, która dodaje do sceny rurę w podanej pozycji. W końcu możemy zajać się już samą funkcją pipe_needed. Jej zadaniem będzie określenie gdzie nalezy dodać rurę.

    Chcemy generować przeszkody na losowych wysokościach, ale tak by żadna z rur nie wygenerowała się poza obszarem, który widzi kamera. Polami minimal_pipe_height i pipe_offset określiliśmy pewien margines generowanych rur. Pozycja y przeszkody musi być zatem oddalona od brzegów gry co najmniej o ich sumę. Należy uwzględnić jeszcze, że nie mamy do dyspozycji całej wysokości ekranu, bo dostępny obszar ogranicza nam ziemia. Stąd pojawia się dodatkowy argument funkcji – screen_bottom_margin. Pobieramy ją w argumencie, bo nie ma powodu, żeby manager rur wiedział cokolwiek o kafelkach ziemii. W argumencie pobieramy też wysokość okna gry.

    #[export]
    fn pipe_needed(
        &mut self,
        owner: &Node2D,
        position_x: Variant,
        screen_bottom_margin: Variant,
        screen_height: Variant,
    ) {
        // Parse arguments.
        let screen_height = screen_height.to_f64() as f32;
        let screen_margin = screen_bottom_margin.to_f64() as f32;
        // Calculate range of the pipe y position.
        let top_margin = self.minimal_sprite_height + self.pipe_offset;
        let bottom_margin =
            screen_height - (screen_margin + (self.minimal_sprite_height + self.pipe_offset));
    
        // Choose random y position in given range.
        let mut rng = thread_rng();
    
        // Top and bottom margins are negative, so order is reverse.
        let y = rng.gen_range(top_margin, bottom_margin);
    
        self.spawn_one(owner, position_x.to_f64() as f32, y)
    }
    

    Na pocztku zajmujemy się parsowaniem argumentów z typu Variant na f32. Następnie wyznaczamy zakres wysokości, na których może pojawić się rura. Przechowujemy go w zmiennych top_margin i bottom_margin. Następnie, losujemy pozycję y z tego zakresu i wywołujemy funkcję spawn_one. I voala, mamy rurę, która powinna być widoczna dla gracza.

    Podsumowanie I

    Po opisanych zmianach tworzenie świata wokół kraboptaka przebiega tak:

    Efekt

    Zarówno rury jak i ziemia tworzą się w razie potrzeby. Gdy są już niewidoczne, znikają ze sceny.

    W przeciwieństwie do sposobu generowania ziemi, nie musimy dodawać aranżatora rur do świata jako pole. Struktury porozumiewają się za pośrednictwem sygnałów i całe rozwiązanie zyskuje przez to na czytelności. Tak mi sie przynajmniej wydaje.

    Edit

    W orginalnym FlappyBirdzie rury są znacznie szersze. Aby to osiągnąć, proponuję zmienić skalę w scenie Pipe.
    Możemy zrobić to w edytorze Godota. Zdaje się, że zadziałałaby zmiana skali w wierzchołku Pipe (StaticBody2D), ale nie zaleca się zmieniać własności scale dla CollisionShape2D. Może to prowadzić do "unexpected collision behavior". U mnie nic się nie stało, świat działał jak powinien, ale na wszelki wypadek zmieniłam kolejno własności wierzchołków-dzieci:

    1. Skalę w wierzchołkach PipeDown i PipeUp na 1.5 (zarówno x jak i y),
    2. Odległości między dolną a górną rurą, zmieniając pozycję y każdej z nich. Ja ustawiłam je odpowiednio na 70 i -70, watrości wybrałam eksperymentalnie,
    3. Wymiary i pozycję CollisionShape2D, tak by pokryły one obrazki rur a skala pozostała bez zmian,
    4. pipe_offset w pliku pipe_manager.rs, ustawiłam na 70.

      Oto efekt:

    Efekt

    Sam kraboptak też jest trochę za duży. W tym wypadku zmienienie pola scale w wierzchołu Player nie pomoże. Wynika to stąd, że wierzchołków typu RigidBody2D nie można skalować. Jeśli spróbujemy to zrobić, to przy nazwie wierzchołka pojawi się ostrzeżenie, a skalowanie nie będzie działało. Problem możemy rozwiązać przez zmianę skali wszystkich jego dzieci. Ja ustawiłam skalę 0.6 (x i y) dla wierzchołka AnimatedSprite i skalę x na 0.5 a y na 0.7 dla PuffAnimation. Tak jak wcześniej, nie zaleca się zmieniać własności scale dla CollisionShape2D, więc dostosujmy jego wymiary ręcznie.

    Poza tym nie podoba mi się teraz ustawienie rur. Oświetlenie jest złe. Nie wiem jak wcześniej tego nie zauważyłam. Żeby to naprawić, trzeba odrobinę zmodyfikować ustawienia górnej rury. Ustawmy jej pole scale x na -1.5, a pozycję x na 0.0.

    To efekt po powyższych poprawkach:

    Lepsze rury

    Podsumowanie II

    W ten sposób skończyliśmy generowanie świata, a gra zyskała istotny element – przeszkody. Co prawda jeszcze nic nie robią, ale to pierwszy krok do dania graczowi możliwości przegrania! Kolejne kroki już wkrótce. Cieplutko zapraszam.

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

  • CrabbyBird #3 Generowanie świata gry – cześć I

    CrabbyBird #3 Generowanie świata gry – cześć I

    Dzisiejszy wpis jest czwartym odcinkiem z serii, w której w ramach nauki Rusta i Godota pisze sobie klon FlappyBirda. Poprzednim razem zajmowaliśmy się ruchem kamery. Dziś rozpoczniemy generowanie świata gry, a dokładniej – ziemi. Gotowy kod z tego wpisu można znaleźć na githubie.

    Dzisiejszy cel

    Chcę, żeby świat gry był ograniczony od dołu ziemią, a kraboptak poruszał się względem niej. Jest to o tyle skomplikowane wyzwanie, że ma on sprawiać wrażenie nieskończonego. Osiągniemy to poprzez konstruowania ziemi z "kafelków", które będą dodawane i usuwane w zależności od tego, co widzi kamera w danej chwili.

    Najpierw w edytorze Godota

    Przygotujemy scenę Base w edytorze Godota. Użyjemy w niej wierzchołka StaticBody2D, żeby stanowiła dla kraboptaka nieruchoma przeszkodę.

    Do dzieci tego wierzchołka dodajemy Sprite, w którym będzie obrazek ziemi dodany do katalogu assets. Dodajemy go w inspektorze w polu Texture. Odznaczamy pole Centered, tak aby pozycja obrazu była aktualizowana względem lewego górnego rogu okna gry.

    Dodajmy też wierzchołek CollisionShape2D, ustawiamy jego pozycję i szerokość tak, żeby pokrywał obrazek.

    Właściwie to chcemy, żeby pozycja kafelka była wyznaczana względem lewego dolnego rogu, dlatego w inspektorze wierzchołka Base (StaticBody2D), w zakładce Transform proponuję ustawić y na wysokość obrazka. U mnie jest to -112. Wymiary obrazka można sprawdzić klikając na obrazek tekstury w wierzchołku Sprite. O tutaj:

    W ten sposób otrzymaliśmy "kafelek" ziemi. Co dalej?
    Chcemy teraz przygotowane w ten sposób kafelki ustawiać jeden obok drugiego, żeby wypełniły cały widoczny obszar. Kafelki, które wyjdą poza widoczność kamery będziemy usuwać. Tak otrzymamy efekt nieskończonej mapy.

    Menadżer kafelków

    Najpierw zajmiemy się generowaniem ziemi. Ponieważ problem ten będzie wymagał informacji, co widzi kamera, decyzja o usuwaniu lub dodawaniu elementów powinna zapadać w strukturze World. Potrzebujemy tam nowej struktury przetrzymującej aktualnie widoczne elementy i umiejącej nimi zarządzać. Nazwijmy ją BaseManager.

    W edytorze Godota tworzymy nową scenę. Ja swoją nazwałam BaseManager. Będzie to wierzchołek typu Node2D. W inspektorze w zakładce transform ustawiłam y na wysokość ekranu (u mnie 720px) po to, żeby w dalszej części pozycję dodawanych kafelków ustawiać względem dolnej krawędzi ekranu. Do tak powstałego elementu tworzymy NativeScript. W polu klasy wpisujemy nazwę klasy, która ma mu odpowiadać (u mnie BaseManager). Przepis jak to zrobić krok po kroku możesz znaleźć w pierwszej części tej serii.

    To na razie tyle w Godocie, teraz pora przejść do Rusta. Ogólnie pomysł jest taki: manager wie jak stworzyć kafelek ziemi, kiedy i gdzie ma go dodać. Zajmijmy się teraz po kolei jak go tego wszystkiego nauczyć. W pliku base_manager.rs stwórzmy strukturkę BaseManager:

    #[derive(NativeClass)]
    #[inherit(Node2D)]
    pub struct BaseManager {
        base_template: Option<Ref<PackedScene>>,
        sprite_height: f32,
        sprite_width: f32,
        end_position: f32,
    }

    Dzięki polu base_template manager wie, jak stworzyć kafelek. Pola sprite_height i sprite_width przydadzą się przy umieszczaniu kafelków, a end_position pozwoli stwierdzić czy juz pora dodawać nowy. Ale może przeanalizujmy to po kolei.

    Tworzenie nowych kafelków

    Po pierwsze zajmiemy się tym, jak stworzyć kafelek. Przepis na kafelki opisaliśmy przed chwilą w Godocie tworząc scenę Base. Do stosowania przygotowanej sceny jako wzorca służy struktura PackedScene. Jest to to uproszczony interfejs do sceny spakowanej w pliku .tscn. W ten sposób wczytujemy całą jej strukturę tak, jak zdefiniowaliśmy ją w edytorze Godota, wraz z zależnościami i ustawieniami.

    W funkcji new ustawiamy początkowe wartości wszystkich pól. Zwróćmy uwagę, że base_template ustawiamy na None. W funkcji _ready zajmujemy się ustawieniem właściwej wartości pola base_template. Wymiary kafelka to wymiary obrazka, pole end_position określa, gdzie kończy się wygenerowana ziemia. Jako, że jeszcze nie dodaliśmy żadnego kafelka, ustawmy ją na 0.

    pub fn new(_owner: &Node2D) -> BaseManager {
            BaseManager {
                base_template: None,
                sprite_height: 112.,
                sprite_width: 336.,
                end_position: 0.,
            }
        }
    
    #[export]
    fn _ready(&mut self, _owner: &Node2D) {
        self.base_template = ResourceLoader::godot_singleton()
            .load("res://scenes/Base.tscn", "", false)
            .and_then(|scene| {
                unsafe { scene.assume_safe().cast::<PackedScene>() }.map(|x| x.claim())
            });
    
        match &self.base_template {
            Some(_scene) => godot_print!("Loaded child scene successfully!"),
            None => godot_print!("Could not load child scene. Check name."),
        }
    }

    Aby dostać się do zasobów stworzonych w edytorze, potrzebujemy ResourceLoadera.

    Funkcji load dajemy trzy argumenty:

    • ścieżkę do sceny,
    • type_hint, czyli typ zasobu. Jest to argument dla Godota, nie powoduje on żadnego przekształcenia w Ruscie. W GDScripcie ten argument jest domyślny i nie musimy go podawać.
    • zmienną logiczną określającą czy zapisywać tak wczytany zasób, czy może wczytywać go przed każdym użyciem. Parametr nazywa się no_cache i dając false deklarujemy, że chcemy żeby ten zasób został zapamiętany. Ale znowu, w GDScripcie ten argument jest domyślny.

    W ten sposób otrzymujemy coś typu Option<Ref<Resource, Shared>>.
    Aby móc sensownie skorzystać z otrzymanego zasobu potrzebujemy przekształcić go funkcją cast na PackedScene. Robimy to analogicznie jak w jednym z poprzednich wpisów, czyli najpierw prosimy dzieloną referencję o bezpieczny dostęp do zasobu funkcją assume_safe (pamiętając o tym, że jest ona unsafe), potem używamy funkcji cast żeby przekształcić go na PackedScene, wszystko robiąc wewnątrz Option, no i otrzymujemy Option<TRef<'a, PackedScene, Access>>.
    Niestety nie jest to koniec. Rust mówi, że ma problem:

    self.base_template = ResourceLoader::godot_singleton()
       |  ______________________________^
    27 | |             .load("res://scenes/Base.tscn", "", false)
    28 | |             .and_then(|scene| {
    29 | |                 unsafe { scene.assume_safe().cast::<PackedScene>() }
    30 | |             });
       | |______________^ expected struct `gdnative::Ref`, found struct `gdnative::TRef`

    Rozwiązuje go funkcja claim(), która zamienia TRef na Ref. Teraz powinno byc ok. Nie jest to najwdzięczniejsza metoda używania zasobów, ale na razie musi nam wystarczyć. Może kiedyś znajdę ładniejszą.

    Na końcu jeszcze sprawdzamy, czy uzyskaliśmy coś sensownego. Jeśli doszliśmy do None, coś poszło nie tak. Prawdopodobnym błędem może być literówka w argumentach load. Ten match służy tylko sprawdzeniu czy wszystko ok, można by go było pominąć.

    Skoro nauczyliśmy już managera tworzyć kafelki, zajmijmy sie ich dodawaniem.

    Dodawanie kafelków ziemi

    Napiszmy sobie funkcję control_spawning, która zajmie sie dodawaniem kafelków, gdy zajdzie taka potrzeba. W argumencie będzie przyjmowała współrzędną x do której ma wygenerować ziemię.
    Załóżmy na razie, że mamy już funkcję spawn_one, która umie dodać kafelek ziemi w podanej w argumencie pozycji. Wtedy funkcja control_spawning mogłaby wyglądać tak:

    #[export]
    pub fn control_spawning(&mut self, owner: &Node2D, x_end: f32) {
        while x_end > self.end_position {
            self.spawn_one(owner, self.end_position, -self.sprite_height);
            self.end_position += self.sprite_width;
        }
    }

    Pole end_position przechowuje współrzędną x prawego brzegu ostatnio wygenerowanego kafelka. Gdy x_end ja przekroczy, należy dodać nowy i zaktualizować end_position. Uzupełnijmy teraz szczegóły.

    Funkcja spawn_one w argumentach przyjmuje pozycję, w której kafelek ma być dodany. Najpierw popatrzmy jak wygląda funkcja:

    #[export]
    fn spawn_one(&mut self, owner: &Node2D, x: f32, y: f32) {
        match self.base_template {
            Some(ref base_obj) => {
                let base = unsafe {
                    // unsafe because `assume_safe` function using.
                    base_obj
                        .assume_safe()
                        // Get instance of `PackedScene`.
                        .instance(0)
                        // Can be casted to `StaticBody2D` but `Node2D` is enough.
                        .and_then(|node| node.assume_safe().cast::<Node2D>())
                        .expect("Could not create base instance.")
                };
                base.set_position(euclid::Vector2D::new(x, y));
                // Add base to manager.
                owner.add_child(base, false);
            }
            None => print!("Base template error."),
        }
    }

    Chcemy, żeby ta funkcja wczytała strukturkę kafelka na podstawie pola base_template. Ponieważ jest typu Option, musimy je jakoś rozpakować. Dlatego zaczynamy od match. Zwróćmy uwagę na słowo kluczowe ref w ramieniu Some(ref base_obj). Dzięki temu Rust nie krzyczy. Jeśli je pominiemy, wypluje taki błąd:

    match self.base_template {
       |               ^^^^^^^^^^^^^^^^^^ help: consider borrowing here: `&self.base_template`
    49 |             Some(base_obj) => {
       |                  --------
       |                  |
       |                  data moved here
       |                  move occurs because `base_obj` has type `gdnative::Ref<gdnative::prelude::PackedScene>`, which does not implement the `Copy` trait

    Skoro już udało nam się dostać do referencji do PackedScene, chcielibyśmy teraz jakoś stworzyć jej instancje. Służy do tego funkcja instance() ale zanim jej użyjemy musimy sie jeszcze odrobinę pogimnastykować. Mam tu na myśli prośbę o dostęp do PackedScene za pośrednictwem funkcji assume_safe. W końcu możemy zrobić sobie kafelka! Niestety, znów potrzebujemy trochę gimnastyki. Funkcja instance zwraca coś typu Option<Ref<Node, Shared>>. Wiemy już jak dostać się do wnętrza takiego czegoś, czyli potrafimy dostać się do nołda typu Node. Istotne jest tu, że chcemy ustawić pozycję tego świeżego nołda, a typ Node nie daje takiej możliwości. Dlatego od razu musimy go przekształcić na coś, co pozawala ustawić pozycję. Takim typem jest Node2D, jest nim też StaticBody2D, który nadaliśmy wcześniej (w edytorze Godota) kafelkowi ziemi. Obydwa powinny być ok, ja wybrałam sobie Node2D.

    Po tej całej gimnastyce możemy w końcu ustawić pozycję kafelka ziemi i dodać ją do sceny.

    W ten sposób doszliśmy do etapu, w którym działa dodawanie kafelków.

    Usuwanie niepotrzebnych kafelków ziemi

    Po tych dziwnych wygibasach związanych z dodawaniem kafelków, zajmijmy sie usuwaniem tych, które są już niepotrzebne (to znaczy niewidoczne dla kamery). Zamiast zastanawiać się nad warunkami, wykorzystamy do tego Godotowy element – czujnik widoczności i mechanizm sygnałów. Wróćmy zatem do edytora Godota.

    Czujnik widoczności

    Do sceny Base.tscn dodajmy nowy element – VisabilityNotifier2D. Będzie on pilnował, czy kafelki są widoczne w kamerze i pozwoli nam je usuwać, gdy będą niepotrzebne. Ponieważ w grze kamera porusza się zawsze w prawo, element znika z jej zasięgu gdy miniemy jego prawy brzeg. Dlatego pozycję czujniczka ustawiamy na szeokość obrazka, by nie usuwać jej zbyt wcześnie.

    Czujnik umie całkiem sporo rzeczy. Możemy podejrzeć je w zakładce Node, zaraz obok zakładki Inspector.

    Nas interesuje funkcja screen_exited. Powiążmy z nią funkcję, która ma obsłużyć zdarzenia wyjścia kafelka z widoku kamery. Oznacza to, że jeśli nastąpi zdarzenie wyjścia elementu z pola widzenia kamery, wywoła się ta powiązana funkcja. Ja zostawiłam nazwę zasugerowaną przez edytor: _on_notifier_screen_exited.

    Teraz, gdy wszystko jest wstępnie przygotowane, dodajmy NativeScript do sceny Base i chodźmy do Rusta.

    Cho do Rusta

    Stwórzmy teraz strukturę kafelka ziemi w Ruscie. Dodajmy plik base.rs. Będzie on raczej prosty:

    #[derive(NativeClass)]
    #[inherit(StaticBody2D)]
    pub struct Base;
    
    #[methods]
    impl Base {
        pub fn new(mut _owner: &StaticBody2D) -> Self {
            Base
        }
    
        #[export]
        pub fn _on_notifier_screen_exited(&self, owner: &StaticBody2D) {
            owner.queue_free();
        }
    }

    Zależy nam przede wszystkim na dodaniu funkcji _on_notifier_screen_exited. Gdy Godot zauważy, że element jest poza widocznością kamery, wywoła te funkcję. Na tym w przybliżeniu polegają godotowe sygnały. I w ten sposób, kafelki same zajmują się swoim usuwaniem.

    Połączenie struktur BaseMenager i World

    Określiliśmy podstawowe funkcjonalności menadżera ziemi. Teraz głównym problemem jest to, jak ich użyć. Do tej pory korzystaliśmy tylko z funkcji wynikających z dziedziczenia przez nasze struktury klasy Godotowej, jak na przykład ustawianie pozycji.

    Tym razem nie wystarczą nam metody struktury Node2D (rodzica BaseMenagera). Potrzebujemy metod z samego BaseMenagera. Do tego posłuży nam struktura Instance. Pozwala ona na dostęp do funkcji z jednego NativeScriptu w drugim NativeScripcie. A tak całkiem konkretnie: chcemy wywołać funkcję BaseMenagera w strukturze World.

    W strukturze World dodajmy pole

    base_manager: Instance<BaseManager, Unique>,

    W funkcji new dodajmy:

    pub fn new(_owner: &Node2D) -> Self {
            World {
                base_manager: Instance::new(),
            }
        }

    Właściwą wartość ustawimy dopiero w funkcji _ready przekształcając typ Godotowy w ten stworzony przez nas.

    #[export]
    fn _ready(&mut self, owner: &Node2D) {
            .
            .
            .
            // Base manager.
            let base_manager = owner
                .get_node("./BaseManager")
                .and_then(|m| unsafe { m.assume_unique().cast::<Node2D>() });
            match base_manager {
                Some(base) => {
                    // Downcast a Godot base class to a NativeScript instance.
                    self.base_manager = Instance::try_from_base(base).expect("Can't downcast to BaseManager");
                }
                None => {
                    godot_print!("Problem with loading BaseManager node.");
                }
            }
        }
    

    Na początku postępujemy podobnie jak z postacią gracza: w funkcji get_node podajemy ścieżkę do dziecka, które chcemy wczytać, a potem przekształcamy otrzymany Node na Node2D. Następnie sprawdzamy czy w wyniku tej operacji coś otrzymaliśmy. Jeśli nie, to piszemy w konsoli, że jest problem. Jeśli tak, próbujemy przekształcić Node2D na Instance<BaseManager> funkcją try_from_base, która zwraca Option. Żeby dostać się do instancji managera, używamy funkcji expect.

    W funkcji _physics_process dodajemy następujący kod:

    #[export]
    fn _physics_process(&self, owner: &Node2D, _delta: f64) {
        .
        .
        .
        // Base management.
        self.base_manager
            .map_mut(|manager, owner| manager.control_spawning(owner.as_ref(), camera_x_range.1))
            .expect("Can't call manager's function: `control_spawning`");
        }
    }

    Aby wywołać funkcję control_spawning z managera, używamy funkcji map_mut. Jej argumentem jest dwuargumentowe λ-wyrażenie. Pierwszy argument (tego wyrażenia) to menadżer, czyli instancja BaseManagera, a drugi to owner. W ciele wywołuje funkcje menadżera. Musimy tylko podać w argumencie, gdzie manager ma skończyć generowanie kafelków.
    No właśnie, gdzie?
    Do tego potrzebujemy wyznaczyć zakres widoczności kamery w osi x. Możemy go wyznaczyć na podstawie współrzędnych kamery, jej offsetu i szerokości okna gry (przyjmijmy na razie, że wynosi ono 480px), na przykład tak:

        // Get camera view right bound.
        let camera_x_end = {
            let camera_x_start = new_position.x + camera.offset().x;
            camera_x_start + 480.0
        };

    Tu jest obrazek. Ten jaśniejszy kawałek tła oznacza zakres tego co widzi kamera. Zwrócę tylko uwagę, że camera_offset jest ujemny, więc wszystkie plusy na obrazku są spoko.

    Cała funkcja _physics_process w pliku world.rs wyglada tak:

    #[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);
    
        // Get camera view right bound.
        let camera_x_end = {
            let camera_x_start = new_position.x + camera.offset().x;
            camera_x_start + 480.
        };
    
        // Base management.
        self.base_manager
            .map_mut(|manager, owner| manager.control_spawning(owner.as_ref(), camera_x_end))
            .expect("Can't call menager's function: `manage_base`");
    }

    Efekt

    Bonus

    Po uruchomieniu napisanego kodu, gra nie wygląda jak na powyższym gifie. Widzimy w niej tylko ten fragment o jaśniejszym tle. Żeby popodglądać sobie to generowanie świata, trzeba zrobić jeszcze kilka rzeczy. Jest to zupełnie opcjonalne i służy tylko popatrzeniu sobie na znikające i pojawiające kafelki. Przedstawię tu pobieżnie jak to osiągnąć.

    Najważniejszym krokiem, jest dodanie do projektu nowej kamery, która będzie śledziła tę dotychczasową, ale widziała więcej. No ale po kolei.

    1. W edytorze Godota dodajmy dodatkową kamerę jako dziecko dotychczasowej kamery (dzięki temu będzie ją śledzić). Ustawmy ją na kamerę aktywną zaznaczając pole Current i zmieniając współrzędną x pola offset na -400.0. Dzięki temu nowa kamera będzie widziała co się dzieje poza widokiem dotychczasowej kamery.

    2. Jako dziecko dotychczasowej kamery dodajmy też wierzchołek ColorRect. Ustawmy jego kolor na coś przezroczystego, a wymiary na 480x720px. Będzie udawał, że widzi to co dotychczasowa kamera. Ustawmy jego pozycję x na -120, czyli offset kamery.

    3. Żeby kafelki znikały poprawnie, musimy zmienić ustawienia czujnika w scenie Base.tscn. Ustawmy jego pozycję x na 350 - (400-120) = 70 (szerokość obrazka – (offset nowej kamery – offset starej kamery)). Wynika to ze zmiany kamery, chcemy żeby kafelki znikały jak wyjdą z zakresu widoczności starej kamery, a nie aktualnej.

    4. Ustawmy wymiary okna gry w ustawieniach projektu na 1000x720px (to przykładowa wartość; najważniejsze, żeby szerokość okna była większa niż 480). Project -> Project Settings -> Display -> Window

    Podsumowanie

    Nie jestem przekonana czy to właściwy sposób kontaktu między strukturami godotowymi. Zamiast takiego "siłowego" wywoływania funkcji z obcego NativeScriptu mogliśmy (chyba) użyć sygnałów. Dlatego w przyszłości chciałabym zająć się sygnałami i porównać te dwie metody. W kolejnym wpisie prawdopodobnie zajmę się generowaniem przeszkód. Ogólnie zapraszam.

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

  • CrabbyBird #2 Poruszanie kamerą

    CrabbyBird #2 Poruszanie kamerą

    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.

    Ustawienia kamery

    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)):

    Ustawienia kamery - anchor mode

    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.

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

    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 AnimatedSpritecast. 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.

  • CrabbyBird #0 Pierwsza przygoda z Rustem i Godotem

    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.