Jakie są sposoby oddzielenia logiki gry od animacji i pętli losowania?

9

Wcześniej tworzyłem gry flash, używając MovieClips i tym podobnych, aby oddzielić moje animacje od logiki gry. Teraz staram się stworzyć grę dla Androida, ale teoria programowania gier polegająca na rozdzielaniu tych rzeczy wciąż mnie dezorientuje. Pochodzę ze środowiska programowania aplikacji nie będących grami, więc jestem zaznajomiony z bardziej podobnymi do MVC wzorami i utknąłem w tym sposobie myślenia, gdy podchodzę do programowania gier.

Chcę robić rzeczy takie jak abstrakcja mojej gry, mając na przykład klasę planszy, która zawiera dane dla siatki płytek z instancjami klasy płytki, z których każda zawiera właściwości. Mogę dać temu dostęp do mojej pętli losowania i narysować planszę na podstawie właściwości każdego kafelka na planszy, ale nie rozumiem, gdzie dokładnie powinna się znaleźć animacja. O ile mogę stwierdzić, rodzaj animacji znajduje się pomiędzy abstrakcyjną logiką gry (model) a pętlą losowania (widok). Przy moim sposobie myślenia MVC frustrujące jest podejmowanie decyzji o tym, gdzie powinna się znaleźć animacja. Miałoby z nim całkiem sporo danych, jak model, ale najwyraźniej musi być bardzo ściśle sprzężone z pętlą rysującą, aby uzyskać animację niezależną od ramek.

Jak wyjść z tego sposobu myślenia i zacząć myśleć o wzorach, które mają większy sens w grach?

TMV
źródło

Odpowiedzi:

6

Animację nadal można idealnie podzielić na logikę i rendering. Stan abstrakcyjnych danych animacji byłby informacją niezbędną do graficznego interfejsu API do renderowania animacji.

Na przykład w grach 2D może to być obszar prostokąta, który oznacza obszar, który wyświetla bieżącą część arkusza duszka, który należy narysować (gdy masz arkusz składający się z powiedzmy 30 rysunków 80x80 zawierających różne kroki twojej postaci skakanie, siadanie, poruszanie się itp.). Mogą to być również wszelkiego rodzaju dane, których nie potrzebujesz do renderowania, ale być może do zarządzania samymi stanami animacji, na przykład czas pozostały do ​​wygaśnięcia bieżącego kroku animacji lub nazwa animacji („chodzenie”, „stój” itp.) Wszystko to można przedstawić w dowolny sposób. To część logiki.

W części dotyczącej renderowania po prostu wykonaj to tak, jak zwykle, pobierz prostokąt z modelu i użyj renderera, aby faktycznie wywoływać interfejs API grafiki.

W kodzie (używając tutaj składni C ++):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

To są dane. Twój renderer weźmie te dane i narysuje je. Ponieważ zarówno normalne, jak i animowane duszki są rysowane w ten sam sposób, możesz tutaj użyć polimorfii!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

TMV:

Wymyśliłem inny przykład. Powiedz, że masz RPG. Na przykład model reprezentujący mapę świata prawdopodobnie musiałby przechowywać pozycję postaci w świecie jako współrzędne kafelków na mapie. Jednak gdy poruszasz postacią, idą one o kilka pikseli na raz do następnego kwadratu. Czy przechowujesz tę pozycję „między płytkami” w obiekcie animacji? Jak zaktualizować model, kiedy postać wreszcie „dotrze” do następnej współrzędnej kafelka na mapie?

Mapa świata nie wie bezpośrednio o pozycji graczy (nie ma Vector2f lub czegoś takiego, który bezpośrednio przechowuje pozycję gracza =, zamiast tego ma bezpośrednie odniesienie do samego obiektu gracza, co z kolei pochodzi od AnimatedSprite dzięki czemu możesz łatwo przekazać go do renderera i uzyskać z niego wszystkie niezbędne dane.

Ogólnie rzecz biorąc, twoja mapa tilemap nie powinna być w stanie zrobić wszystkiego - miałbym klasę „TileMap”, która zajmuje się zarządzaniem wszystkimi kafelkami, a może także wykrywa kolizję między obiektami, które przekazuję jej i kafelki na mapie. Następnie miałbym inną klasę „RPGMap” lub jakkolwiek chcesz ją nazwać, która zawiera zarówno odniesienie do twojej mapy til, jak i odniesienie do odtwarzacza oraz wykonuje rzeczywiste wywołania Update () do twojego odtwarzacza i do twojego tilemap.

To, jak chcesz zaktualizować model, gdy gracz się porusza, zależy od tego, co chcesz zrobić.

Czy twój gracz może poruszać się pomiędzy płytkami niezależnie (styl Zelda)? Wystarczy poradzić sobie z wejściem i odpowiednio przesunąć odtwarzacz w każdej klatce. A może chcesz, aby gracz nacisnął „w prawo”, a twoja postać automatycznie przesuwa jeden kafelek w prawo? Pozwól swojej klasie RPGMap interpolować pozycję graczy, dopóki nie dotrze do miejsca docelowego, a tymczasem zablokuj całą obsługę klawiszy ruchu.

Tak czy inaczej, jeśli chcesz ułatwić sobie życie, wszystkie twoje modele będą miały metody Update (), jeśli faktycznie potrzebują trochę logiki, aby się zaktualizować (zamiast po prostu zmieniać wartości zmiennych) - Nie oddajesz kontrolera w ten sposób we wzorcu MVC wystarczy przenieść kod z „jednego kroku powyżej” (kontrolera) do modelu, a wszystko, co robi kontroler, to wywołać metodę Update () modelu (w naszym przypadku kontroler byłby RPGMap). Nadal możesz łatwo zamienić kod logiczny - możesz po prostu bezpośrednio zmienić kod klasy lub jeśli potrzebujesz zupełnie innego zachowania, możesz po prostu wyprowadzić się z klasy modelu i zastąpić tylko metodę Update ().

Takie podejście znacznie redukuje wywołania metod i takie rzeczy - co kiedyś było jedną z głównych wad czystego wzorca MVC (w końcu bardzo często wywoływano funkcję GetThis () GetThat ()) - powoduje to, że kod jest zarówno dłuższy, jak i trochę trudniejsze do odczytania, a także wolniejsze - nawet jeśli może to zająć się twój kompilator, który optymalizuje wiele takich rzeczy.

TravisG
źródło
Czy zachowałbyś dane animacji w klasie zawierającej logikę gry, klasie zawierającej pętlę gry, czy oddzielnie od obu? Poza tym zależy wyłącznie od pętli lub klasy zawierającej pętlę, aby zrozumieć, jak przełożyć dane animacji na rysowanie ekranu, prawda? Często nie byłoby tak proste, jak uzyskanie prostokąta reprezentującego sekcję arkusza duszków i użycie go do wycięcia rysunku bitmapowego z arkusza duszków.
TMV
Wymyśliłem inny przykład. Powiedz, że masz RPG. Na przykład model reprezentujący mapę świata prawdopodobnie musiałby przechowywać pozycję postaci w świecie jako współrzędne kafelków na mapie. Jednak gdy poruszasz postacią, idą one o kilka pikseli na raz do następnego kwadratu. Czy przechowujesz tę pozycję „między płytkami” w obiekcie animacji? Jak zaktualizować model, kiedy postać wreszcie „dotrze” do następnej współrzędnej kafelka na mapie?
TMV
Zredagowałem w odpowiedzi na twoje pytanie, ponieważ komentarze nie pozwalają na to wystarczająco dużo znaków.
TravisG,
Jeśli wszystko rozumiem poprawnie:
TMV
Możesz mieć instancję klasy „Animator” w swoim widoku, i miałaby ona publiczną metodę „aktualizacji”, którą widok nazywa każdą klatką. Metoda aktualizacji wywołuje metody „aktualizacji” instancji różnego rodzaju poszczególnych obiektów animacji w jej wnętrzu. Animator i znajdujące się w nim animacje mają odniesienie do modelu (przekazywane przez ich konstruktorów), dzięki czemu mogą aktualizować dane modelu, jeśli animacja go zmieni. Następnie w pętli rysowania uzyskujesz dane z animacji wewnątrz animatora w sposób zrozumiały dla widoku i narysowany.
TMV,
2

Mogę rozwinąć tę kwestię, jeśli chcesz, ale mam centralny moduł renderujący, któremu każe się rysować w pętli. Zamiast

handle input

for every entity:
    update entity

for every entity:
    draw entity

Mam bardziej podobny system

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

Klasa renderer zawiera po prostu listę odniesień do rysowalnych komponentów obiektów. Są one przypisane w konstruktorach dla uproszczenia.

Na przykład chciałbym mieć klasę GameBoard z wieloma płytkami. Każda płytka oczywiście zna swoją pozycję i zakładam jakąś animację. Uwzględnij to w jakiejś klasie Animacji, do której należy kafelek, i poproś, aby przekazał swoje odwołanie do klasy Renderer. Tam wszyscy rozdzieleni. Gdy aktualizujesz kafelek, wywołuje on aktualizację animacji .. lub aktualizuje go sam. Po Renderer.Draw()wywołaniu rysuje animację.

Animacja niezależna od klatki nie powinna zajmować zbyt wiele miejsca w pętli rysowania.

Kaczka komunistyczna
źródło
0

Ostatnio uczyłem się paradygmatów, więc jeśli ta odpowiedź będzie niepełna, jestem pewien, że ktoś ją doda.

Metodologia, która wydaje się najbardziej sensowna w projektowaniu gier, polega na oddzieleniu logiki od wyniku wyświetlanego na ekranie.

W większości przypadków chciałbyś zastosować podejście wielowątkowe, jeśli nie znasz tego tematu, jest to pytanie samo w sobie, oto podstawowa wiki . Zasadniczo chcesz, aby logika gry była wykonywana w jednym wątku, blokując zmienne, do których musi mieć dostęp, aby zapewnić integralność danych. Jeśli twoja pętla logiczna jest niewiarygodnie szybka (super mega animowany pong 3d?), Możesz spróbować naprawić częstotliwość wykonywaną przez tę pętlę, śpiąc na wątku przez krótki czas (na forum sugerowano 120 Hz dla pętli fizyki gry). Jednocześnie drugi wątek przerysowuje ekran (w innych tematach sugerowano 60 Hz) z zaktualizowanymi zmiennymi, ponownie prosząc o zablokowanie zmiennych przed uzyskaniem do nich dostępu.

W takim przypadku animacje lub przejścia itp. Przechodzą do wątku rysunkowego, ale musisz sygnalizować przez flagę (być może zmienną stanu globalnego), że wątek logiki gry nie może nic robić (lub robić coś innego ... być może nowe parametry mapy).

Po obejrzeniu współbieżności reszta jest dość zrozumiała. Jeśli nie masz doświadczenia w zakresie współbieżności, zdecydowanie zalecamy napisanie kilku prostych programów testowych, aby zrozumieć, jak przebiega przepływ.

Mam nadzieję że to pomoże :)

[edytuj] W systemach, które nie obsługują wielowątkowości, animacja może nadal przechodzić w pętlę losowania, ale chcesz ustawić stan w taki sposób, aby sygnalizował logice, że dzieje się coś innego i nie przetwarza aktualny poziom / mapa / etc ...

Stephen
źródło
1
Nie zgadzam się tutaj. W większości przypadków nie chcesz korzystać z wielu wątków, zwłaszcza jeśli jest to niewielka gra.
Kaczka komunistyczna
@ TheCommunistDuck Fair, narzut i złożoność wielowątkowości mogą zdecydowanie spowodować, że będzie ona nadmierna, a jeśli gra jest niewielka, powinna być w stanie szybko zaktualizować.
Stephen