Techniki zarządzania stanem gry?

24

Po pierwsze, nie mam na myśli zarządzania scenami; Stan gry definiuję luźno jako jakikolwiek stan w grze, który ma wpływ na to, czy dane wejściowe użytkownika powinny być włączone, czy też niektórzy aktorzy powinni zostać tymczasowo wyłączeni itp.

Jako konkretny przykład, powiedzmy, że jest to gra w klasyczną Battlechess. Po tym, jak wykonuję ruch, aby wziąć kawałek innego gracza, rozpoczyna się krótka sekwencja bitwy. Podczas tej sekwencji gracz nie powinien mieć możliwości przenoszenia pionków. Jak więc śledziłbyś tego rodzaju zmianę stanu? Maszyna skończona? Prosty czek boolowski? Wygląda na to, że ten drugi działałby dobrze tylko w grze z bardzo niewielką liczbą zmian tego rodzaju.

Mogę wymyślić wiele prostych sposobów radzenia sobie z tym za pomocą automatów skończonych, ale widzę też, jak szybko wymykają się spod kontroli. Jestem ciekawy, czy istnieje bardziej elegancki sposób na śledzenie stanów / przejść gry.

wargoński
źródło
Czy sprawdziłeś już gamedev.stackexchange.com/questions/1783/game-state-stack i gamedev.stackexchange.com/questions/2423/... ? To rodzaj przeskakiwania wokół tej samej koncepcji, ale nie mogę wymyślić niczego lepszego niż automat stanowy dla stanu gry.
michael.bartnett

Odpowiedzi:

18

Kiedyś natknąłem się na artykuł, który dość elegancko rozwiązuje twój problem. Jest to podstawowa implementacja FSM, która jest wywoływana w głównej pętli. W dalszej części tej odpowiedzi opisałem podstawowe podsumowanie tego artykułu.

Twój podstawowy stan gry wygląda następująco:

class CGameState
{
    public:
        // Setup and destroy the state
        void Init();
        void Cleanup();

        // Used when temporarily transitioning to another state
        void Pause();
        void Resume();

        // The three important actions within a game loop
        void HandleEvents();
        void Update();
        void Draw();
};

Każdy stan gry jest reprezentowany przez implementację tego interfejsu. W twoim przykładzie Battlechess może to oznaczać następujące stany:

  • animacja wprowadzająca
  • menu główne
  • animacja konfiguracji szachownicy
  • ruch gracza
  • animacja ruchu gracza
  • animacja ruchu przeciwnika
  • menu pauzy
  • ekran końcowy

Stany są zarządzane w silniku stanu:

class CGameEngine
{
    public:
        // Creating and destroying the state machine
        void Init();
        void Cleanup();

        // Transit between states
        void ChangeState(CGameState* state);
        void PushState(CGameState* state);
        void PopState();

        // The three important actions within a game loop
        // (these will be handled by the top state in the stack)
        void HandleEvents();
        void Update();
        void Draw();

        // ...
};

Zauważ, że każdy stan potrzebuje w pewnym momencie wskaźnika do CGameEngine, więc sam stan może zdecydować, czy należy wprowadzić nowy stan. Artykuł sugeruje przekazanie CGameEngine jako parametru dla HandleEvents, Update i Draw.

W końcu twoja główna pętla zajmuje się tylko silnikiem stanu:

int main ( int argc, char *argv[] )
{
    CGameEngine game;

    // initialize the engine
    game.Init( "Engine Test v1.0" );

    // load the intro
    game.ChangeState( CIntroState::Instance() );

    // main loop
    while ( game.Running() )
    {
        game.HandleEvents();
        game.Update();
        game.Draw();
    }

    // cleanup the engine
    game.Cleanup();
    return 0;
}
duch
źródło
17
C jak na klasę? Ew. To jednak dobry artykuł - +1.
Kaczka komunistyczna
Z tego, co mogę zebrać, jest to rodzaj rzeczy, o której pytanie jest wyraźnie - a nie pytające. Nie oznacza to, że nie poradzisz sobie z tym w ten sposób, jak na pewno możesz, ale jeśli wszystko, co chciałeś zrobić, to tymczasowo wyłączyć dane wejściowe, myślę, że zarówno nadmierne, jak i złe w zakresie konserwacji jest uzyskanie nowej podklasy CGameState, która ma zamiar być w 99% identyczny z inną podklasą.
Kylotan
Myślę, że to zależy w dużym stopniu od tego, jak kod się ze sobą sprzęga. Mogę sobie wyobrazić czysty podział między wybieraniem elementu a miejscem docelowym (głównie wskaźniki interfejsu użytkownika i obsługą danych wejściowych) oraz animacją kawałka szachowego w kierunku tego miejsca docelowego (animacja całej planszy, w której inne elementy poruszają się z dala od nich, wchodzą w interakcję z ruchem kawałek itp.), dzięki czemu stany są dalekie od identyczności. To rozdziela odpowiedzialność, umożliwiając łatwą konserwację, a nawet możliwość ponownego użycia (demo wstępu, tryb odtwarzania). Myślę, że to również odpowiada na pytanie, pokazując, że korzystanie z FSM nie musi być kłopotliwe.
duch
To jest naprawdę świetne, dziękuję. Kluczową kwestią, o której wspomniałeś, był twój ostatni komentarz: „korzystanie z FSM nie musi być kłopotliwe”. Błędnie wyobrażałem sobie, że użycie FSM wymagałoby użycia instrukcji switch, co niekoniecznie jest prawdą. Kolejnym kluczowym potwierdzeniem jest to, że każdy stan potrzebuje odniesienia do silnika gry; Zastanawiałem się, jak inaczej by to działało.
vargonian
2

Zaczynam od obsługi tego rodzaju rzeczy w najprostszy możliwy sposób.

bool isPieceMoving;

Następnie dodam kontrole względem tej flagi logicznej w odpowiednich miejscach.

Jeśli później stwierdzę, że potrzebuję więcej specjalnych przypadków niż to - i tylko to - ponownie uwzględniam coś lepszego. Zazwyczaj podejmuję 3 podejścia:

  • Refaktoryzuj wszelkie wyłączne flagi reprezentujące substytuty w wyliczenia. na przykład. enum { PRE_MOVE, MOVE, POST_MOVE }i dodaj przejścia tam, gdzie to potrzebne. Następnie mogę sprawdzić na podstawie tego wyliczenia, w którym sprawdzałem na podstawie flagi logicznej. Jest to prosta zmiana, ale redukująca liczbę rzeczy, które musisz sprawdzić, pozwala na użycie instrukcji switch do skutecznego zarządzania zachowaniem itp.
  • W razie potrzeby wyłącz poszczególne podsystemy. Jeśli jedyną różnicą podczas sekwencji bitwy jest to, że nie możesz przenosić pionków, możesz sprawdzić pieceSelectionManager->disable()lub podobnie na początku sekwencji, i pieceSelectionManager->enable(). Nadal zasadniczo masz flagi, ale teraz są one przechowywane bliżej kontrolowanego obiektu i nie musisz utrzymywać żadnego dodatkowego stanu w kodzie gry.
  • Poprzednia część sugeruje istnienie PieceSelectionManager: bardziej ogólnie, możesz podzielić części swojego stanu gry i zachowania na mniejsze obiekty, które w spójny sposób obsługują podzbiór ogólnego stanu. Każdy z tych obiektów będzie miał swój własny stan, który determinuje jego zachowanie, ale łatwo nim zarządzać, ponieważ jest odizolowany od innych obiektów. Oprzyj się pragnieniu, aby obiekt gamestate lub główna pętla stały się wysypiskiem dla pseudog globali i uwzględnij to!

Ogólnie rzecz biorąc, nigdy nie muszę iść dalej, jeśli chodzi o podstacje specjalne, więc nie sądzę, że istnieje ryzyko, że „szybko wymknie się spod kontroli”.

Kylotan
źródło
1
Tak, wyobrażam sobie, że istnieje granica między wyjściem ze stanów a użyciem po prostu bool / enums, gdy jest to właściwe. Ale znając moje pedantyczne tendencje, prawdopodobnie skończę, tworząc prawie każde państwo własną klasą.
vargonian
Sprawiasz, że brzmi to tak, jakby klasa była bardziej poprawna niż alternatywy, ale pamiętaj, że jest to subiektywne. Jeśli zaczniesz tworzyć zbyt wiele małych klas dla rzeczy, które mogą być łatwiej reprezentowane przez inne konstrukcje językowe, może to zaciemnić przeznaczenie kodu.
Kylotan,
1

Staram się nie używać do tego celu automatu stanów i booleanów, ponieważ oba nie są skalowalne. Oba zamieniają się w bałagan, gdy rośnie liczba stanów.

Zazwyczaj rozgrywkę projektuję jako sekwencję działań i konsekwencji, każdy stan gry przychodzi naturalnie, bez potrzeby definiowania go osobno.

Na przykład w przypadku wyłączania danych wejściowych odtwarzacza: masz pewien moduł obsługi danych wejściowych użytkownika i pewne wizualne wskazanie, że dane wejściowe są wyłączone, powinieneś uczynić z nich jeden obiekt lub komponent, więc aby wyłączyć dane wejściowe, po prostu wyłącz cały obiekt, nie musisz zsynchronizuj je w jakiejś maszynie stanów lub zareaguj na jakiś wskaźnik boolowski.

Filipp Keks
źródło