Gra w Rust w 24 godziny: doświadczenie w zakresie rozwoju osobistego

Gra w Rust w 24 godziny: doświadczenie w zakresie rozwoju osobistego

W tym artykule opowiem o moich osobistych doświadczeniach związanych z tworzeniem małej gry w Rust. Stworzenie działającej wersji zajęło około 24 godzin (pracowałem głównie wieczorami lub w weekendy). Gra jest jeszcze daleka od ukończenia, ale myślę, że doświadczenie będzie satysfakcjonujące. Podzielę się tym, czego się nauczyłem i kilkoma obserwacjami, które poczyniłem podczas tworzenia gry od zera.

Skillbox poleca: Dwuletni kurs praktyczny „Jestem programistą internetowym PRO”.

Przypomnienie: dla wszystkich czytelników „Habr” - rabat w wysokości 10 000 rubli przy zapisywaniu się na dowolny kurs Skillbox przy użyciu kodu promocyjnego „Habr”.

Dlaczego rdza?

Wybrałem ten język, ponieważ słyszałem o nim wiele dobrego i widzę, że staje się on coraz bardziej popularny w tworzeniu gier. Przed napisaniem gry miałem niewielkie doświadczenie w tworzeniu prostych aplikacji w Rust. To wystarczyło, aby dać mi poczucie wolności podczas pisania gry.

Dlaczego gra i jaki rodzaj gry?

Tworzenie gier to świetna zabawa! Szkoda, że ​​powodów nie było więcej, ale do projektów „domowych” wybieram tematy, które nie są zbyt ściśle związane z moją pracą. Co to za gra? Chciałem stworzyć coś w rodzaju symulatora tenisa, który łączyłby Cities Skylines, Zoo Tycoon, Prison Architect i sam tenis. Ogólnie rzecz biorąc, okazała się to gra o akademii tenisowej, do której przychodzą grać ludzie.

Trening techniczny

Chciałem użyć Rusta, ale nie wiedziałem dokładnie, ile pracy przygotowawczej będzie potrzeba, aby zacząć. Nie chciałem pisać shaderów pikseli i używać metody „przeciągnij i upuść”, więc szukałem najbardziej elastycznych rozwiązań.

Znalazłem przydatne zasoby, którymi się z Tobą dzielę:

Sprawdziłem kilka silników gier Rust, ostatecznie wybierając Piston i ggez. Natknąłem się na nie podczas pracy nad poprzednim projektem. Ostatecznie wybrałem ggez, ponieważ wydawał się bardziej odpowiedni do wdrożenia małej gry 2D. Modułowa struktura Pistona jest zbyt złożona dla początkującego programisty (lub kogoś, kto pracuje z Rustem po raz pierwszy).

Struktura gry

Spędziłem trochę czasu zastanawiając się nad architekturą projektu. Pierwszym krokiem jest stworzenie „ziemi”, ludzi i kortów tenisowych. Ludzie muszą krążyć po sądach i czekać. Gracze muszą posiadać umiejętności, które z czasem się doskonalą. Poza tym powinien istnieć edytor, który pozwala na dodawanie nowych osób i sądów, ale ten nie jest już darmowy.

Po przemyśleniu wszystkiego zabrałem się do pracy.

Tworzenie gry

Początek: Koła i Abstrakcje

Wziąłem przykład z ggez i dostałem okrąg na ekranie. Cudowny! Teraz trochę abstrakcji. Pomyślałem, że miło byłoby odejść od idei obiektu gry. Każdy obiekt musi zostać wyrenderowany i zaktualizowany zgodnie z poniższym opisem:

// the game object trait
trait GameObject {
    fn update(&mut self, _ctx: &mut Context) -> GameResult<()>;
    fn draw(&mut self, ctx: &mut Context) -> GameResult<()>;
}
 
// a specific game object - Circle
struct Circle {
    position: Point2,
}
 
 impl Circle {
    fn new(position: Point2) -> Circle {
        Circle { position }
    }
}
impl GameObject for Circle {
    fn update(&mut self, _ctx: &mut Context) -> GameResult<()> {
        Ok(())
    }
    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        let circle =
            graphics::Mesh::new_circle(ctx, graphics::DrawMode::Fill, self.position, 100.0, 2.0)?;
 
         graphics::draw(ctx, &circle, na::Point2::new(0.0, 0.0), 0.0)?;
        Ok(())
    }
}

Ten fragment kodu dał mi ładną listę obiektów, które mogłem aktualizować i renderować w równie ładnej pętli.

mpl event::EventHandler for MainState {
    fn update(&mut self, context: &mut Context) -> GameResult<()> {
        // Update all objects
        for object in self.objects.iter_mut() {
            object.update(context)?;
        }
 
        Ok(())
    }
 
    fn draw(&mut self, context: &mut Context) -> GameResult<()> {
        graphics::clear(context);
 
        // Draw all objects
        for object in self.objects.iter_mut() {
            object.draw(context)?;
        }
 
        graphics::present(context);
 
        Ok(())
    }
}

main.rs jest konieczny, ponieważ zawiera wszystkie linie kodu. Spędziłem trochę czasu na oddzielaniu plików i optymalizacji struktury katalogów. Tak to wyglądało później:
zasoby -> tutaj znajdują się wszystkie zasoby (obrazy)
src
- podmioty
— game_object.rs
— koło.rs
— main.rs -> główna pętla

Ludzie, podłogi i obrazy

Następnym krokiem jest utworzenie obiektu gry Osoba i załadowanie obrazów. Wszystko powinno być zbudowane na bazie płytek 32*32.

Gra w Rust w 24 godziny: doświadczenie w zakresie rozwoju osobistego

Korty tenisowe

Po przestudiowaniu, jak wyglądają korty tenisowe, zdecydowałem się zrobić je z płytek 4*2. Początkowo możliwe było wykonanie obrazu tej wielkości lub złożenie 8 oddzielnych płytek. Ale potem zdałem sobie sprawę, że potrzebne są tylko dwie unikalne płytki i oto dlaczego.

W sumie mamy dwie takie płytki: 1 i 2.

Każda część boiska składa się z płytki 1 lub płytki 2. Można je ułożyć normalnie lub odwrócić o 180 stopni.

Gra w Rust w 24 godziny: doświadczenie w zakresie rozwoju osobistego

Podstawowy tryb budowy (montażu).

Kiedy udało mi się osiągnąć renderowanie miejsc, ludzi i map, zdałem sobie sprawę, że potrzebny jest również podstawowy tryb montażu. Zaimplementowałem to w ten sposób: po naciśnięciu przycisku obiekt jest zaznaczany, a kliknięcie umieszcza go w żądanym miejscu. Zatem przycisk 1 pozwala wybrać kort, a przycisk 2 pozwala wybrać zawodnika.

Ale nadal musimy pamiętać, co oznaczają 1 i 2, więc dodałem model szkieletowy, aby było jasne, który obiekt został wybrany. Tak to wygląda.

Gra w Rust w 24 godziny: doświadczenie w zakresie rozwoju osobistego

Pytania dotyczące architektury i refaktoryzacji

Teraz mam kilka obiektów do gry: ludzi, korty i podłogi. Aby jednak modele szkieletowe zadziałały, każdy obiekt musi zostać poinformowany, czy same obiekty znajdują się w trybie demonstracyjnym, czy też po prostu narysowano ramkę. Nie jest to zbyt wygodne.

Wydawało mi się, że należy przemyśleć architekturę, aby ujawnić pewne ograniczenia:

  • Posiadanie jednostki, która sama się renderuje i aktualizuje, jest problemem, ponieważ ta jednostka nie będzie w stanie „wiedzieć”, co ma renderować – obraz i model szkieletowy;
  • brak narzędzia do wymiany właściwości i zachowań pomiędzy poszczególnymi bytami (np. właściwość is_build_mode czy renderowanie zachowań). Możliwe byłoby zastosowanie dziedziczenia, chociaż nie ma odpowiedniego sposobu na jego implementację w Rust. To, czego naprawdę potrzebowałem, to układ;
  • potrzebne było narzędzie interakcji między podmiotami w celu przydzielania osób do sądów;
  • same byty były mieszaniną danych i logiki, która szybko wymknęła się spod kontroli.

Zrobiłem więcej badań i odkryłem architekturę ECS – System komponentów jednostki, który jest powszechnie używany w grach. Oto zalety ECS:

  • dane są oddzielone od logiki;
  • kompozycja zamiast dziedziczenia;
  • architektura zorientowana na dane.

ECS charakteryzuje się trzema podstawowymi koncepcjami:

  • byty - rodzaj obiektu, do którego odnosi się identyfikator (może to być gracz, piłka lub coś innego);
  • komponenty - byty składają się z nich. Przykład - komponent renderujący, lokalizacje i inne. Są to hurtownie danych;
  • systemy — wykorzystują zarówno obiekty, jak i komponenty, a także zawierają zachowania i logikę oparte na tych danych. Przykładem jest system renderowania, który iteruje po wszystkich elementach zawierających komponenty renderujące i wykonuje renderowanie.

Po przestudiowaniu tego stało się jasne, że ECS rozwiązuje następujące problemy:

  • używanie układu zamiast dziedziczenia do systemowego organizowania jednostek;
  • pozbycie się bałaganu kodowego w systemach sterowania;
  • używając metod takich jak is_build_mode, aby zachować logikę szkieletową w tym samym miejscu - w systemie renderującym.

Tak się stało po wdrożeniu ECS.

zasoby -> tutaj znajdują się wszystkie zasoby (obrazy)
src
- składniki
—pozycja.rs
— osoba.rs
— tenis_kort.rs
— podłoga.rs
- wireframe.rs
— Mouse_tracked.rs
- zasoby
—mysz.rs
- systemy
— renderowanie.rs
— stałe.rs
— utils.rs
— world_factory.rs -> funkcje fabryki świata
— main.rs -> główna pętla

Przydzielamy ludzi do sądów

ECS ułatwiło życie. Teraz miałem systematyczny sposób dodawania danych do jednostek i dodawania logiki opartej na tych danych. A to z kolei umożliwiło zorganizowanie podziału ludzi pomiędzy dwory.

Co ja zrobiłem:

  • dodano dane o przydzielonych sądach do Osoby;
  • dodano dane o rozproszonych osobach do TennisCourt;
  • dodano CourtChoosingSystem, który pozwala analizować ludzi i sądy, wykrywać dostępne korty i przydzielać do nich zawodników;
  • dodano system PersonMovementSystem, który wyszukuje osoby przypisane do sądów, a jeśli ich tam nie ma, wysyła je tam, gdzie powinny.

Gra w Rust w 24 godziny: doświadczenie w zakresie rozwoju osobistego

Podsumowując

Bardzo podobała mi się praca nad tą prostą grą. Co więcej, cieszę się, że do jej napisania użyłem Rusta, ponieważ:

  • Rdza daje ci to, czego potrzebujesz;
  • ma doskonałą dokumentację, Rust jest dość elegancki;
  • konsystencja jest fajna;
  • nie musisz uciekać się do klonowania, kopiowania i innych podobnych działań, co często robiłem w C++;
  • Opcje są bardzo łatwe w użyciu i bardzo dobrze radzą sobie z błędami;
  • jeśli projekt udało się skompilować, to w 99% przypadków działa i dokładnie tak, jak powinien. Myślę, że komunikaty o błędach kompilatora są najlepsze, jakie widziałem.

Tworzenie gier w Rust dopiero się rozpoczyna. Ale istnieje już stabilna i dość duża społeczność pracująca nad udostępnieniem Rusta wszystkim. Dlatego z optymizmem patrzę w przyszłość języka, nie mogąc się doczekać efektów naszej wspólnej pracy.

Skillbox poleca:

Źródło: www.habr.com

Dodaj komentarz