Stan gry „Stack”?

52

Myślałem o tym, jak zaimplementować stany gry w mojej grze. Głównymi rzeczami, które chcę za to są:

  • Półprzezroczyste górne stany - są w stanie zobaczyć menu pauzy w grze z tyłu

  • Coś OO-uważam, że jest to łatwiejsze w użyciu i rozumiem leżącą u podstaw teorię, a także utrzymuję porządek i dodaje więcej.



Planowałem użyć połączonej listy i potraktować ją jako stos. Oznacza to, że mogłem uzyskać dostęp do poniższego stanu dla półprzezroczystości.
Plan: Niech stos stanu będzie połączoną listą wskaźników do IGameStates. Stan najwyższy obsługuje własne komendy aktualizacji i wprowadzania, a następnie ma element transparentny, który decyduje, czy należy narysować stan poniżej.
Wtedy mógłbym zrobić:

states.push_back(new MainMenuState());
states.push_back(new OptionsMenuState());
states.pop_front();

Aby przedstawić ładowanie odtwarzacza, przejdź do opcji, a następnie menu głównego.
Czy to dobry pomysł, czy ...? Czy powinienem spojrzeć na coś innego?

Dzięki.

Kaczka komunistyczna
źródło
Czy chcesz zobaczyć MainMenuState za OptionsMenuState? A może tylko ekran gry za OptionsMenuState?
Skizz
Plan był taki, że stany miałyby mieć nieprzezroczystość / isTransparent wartość / flagę. Sprawdziłbym i sprawdził, czy stan najwyższy ma to prawda, a jeśli tak, to jaką ma wartość. Następnie renderuj go z takim nieprzezroczystością w stosunku do drugiego stanu. W tym przypadku nie, nie zrobiłbym tego.
Kaczka komunistyczna
Wiem, że jest późno, ale dla przyszłych czytelników: nie używaj neww sposób pokazany w przykładowym kodzie, po prostu prosi o wycieki pamięci lub inne, poważniejsze błędy.
Pharap

Odpowiedzi:

44

Pracowałem na tym samym silniku co koderanger. Mam inny punkt widzenia. :)

Po pierwsze, nie mieliśmy stosu FSM - mieliśmy stos stanów. Stos stanów tworzy pojedynczy FSM. Nie wiem, jak wyglądałby stos FSM. Prawdopodobnie zbyt skomplikowane, aby zrobić cokolwiek praktycznego.

Moim największym problemem z naszą Globalną Maszyną Stanową było to, że był to stos stanów, a nie zbiór stanów. Oznacza to np. ... / MainMenu / Ładowanie było inne niż ... / Ładowanie / Menu główne, w zależności od tego, czy menu główne pojawiło się przed czy po ekranie ładowania (gra jest asynchroniczna, a ładowanie odbywa się głównie na serwerze ).

Jako dwa przykłady rzeczy uczyniło to brzydkim:

  • Doprowadziło to np. Do stanu LoadingGameplay, więc miałeś Base / Loading oraz Base / Gameplay / LoadingGameplay do ładowania w stanie Gameplay, który musiał powtarzać dużą część kodu w normalnym stanie ładowania (ale nie wszystkie i dodać trochę więcej ).
  • Mieliśmy kilka funkcji, takich jak „jeśli w kreatorze postaci przejdź do gry; jeśli w grze przejdź do wyboru postaci; jeśli w postaci wybierz powrót do logowania”, ponieważ chcieliśmy pokazywać te same okna interfejsu w różnych stanach, ale wprowadzać Wstecz / Dalej przyciski nadal działają.

Mimo nazwy nie był zbyt „globalny”. Większość wewnętrznych systemów gier nie używała go do śledzenia swoich stanów wewnętrznych, ponieważ nie chciały, aby ich stany rżały się z innymi systemami. Inni, np. System interfejsu użytkownika, mogą go używać, ale tylko do kopiowania stanu do własnych lokalnych systemów stanu. (Chciałbym szczególnie ostrzec system przed stanami interfejsu użytkownika. Stan interfejsu użytkownika nie jest stosem, to naprawdę DAG, a próba wymuszenia na nim jakiejkolwiek innej struktury spowoduje, że korzystanie z interfejsów będzie frustrujące.)

To, co było dobre, to izolowanie zadań związanych z integracją kodu od programistów infrastruktury, którzy nie wiedzieli, jak właściwie przebieg gry jest zorganizowany, abyś mógł powiedzieć facetowi piszącemu „wstaw swój kod w Client_Patch_Update”, a facetowi piszącemu grafikę ładowanie „umieść kod w Client_MapTransfer_OnEnter”, a my możemy bez problemu zamienić niektóre przepływy logiczne.

W przypadku pobocznego projektu miałem więcej szczęścia z zestawem stanów niż ze stosem , nie boję się tworzyć wielu maszyn dla niepowiązanych systemów i nie pozwalam sobie wpaść w pułapkę posiadania „stanu globalnego”, który jest naprawdę to po prostu skomplikowany sposób synchronizacji za pomocą zmiennych globalnych - Jasne, skończysz na tym, że zbliża się termin, ale nie projektuj tego z myślą o swoim celu . Zasadniczo stan w grze nie jest stosem, a wszystkie stany w grze nie są ze sobą powiązane.

GSM, podobnie jak wskaźniki funkcji i zachowanie nielokalne, utrudniało debugowanie, chociaż debugowanie tego rodzaju dużych przejść między stanami nie było zbyt zabawne, zanim je mieliśmy. Zestawy stanów zamiast stosów stanów tak naprawdę nie pomagają, ale powinieneś o tym wiedzieć. Funkcje wirtualne zamiast wskaźników funkcji mogą to nieco złagodzić.


źródło
Świetna odpowiedź, dzięki! Myślę, że mogę dużo czerpać z twojego postu i twoich przeszłych doświadczeń. : D + 1 / Tick.
Kaczka komunistyczna
Zaletą hierarchii jest to, że możesz budować stany narzędzi, które są po prostu wypychane na szczyt i nie musisz się martwić o to, co jeszcze działa.
coderanger
Nie rozumiem, jak to argument przemawia za hierarchią, a nie za zestawami. Hierarchia sprawia, że ​​wszelka komunikacja międzypaństwowa jest bardziej skomplikowana, ponieważ nie masz pojęcia, gdzie zostały one przekazane.
Fakt, że interfejsy użytkownika są w rzeczywistości DAG, jest dobrze przemyślany, ale nie zgadzam się z tym, że z pewnością można je przedstawić na stosie. Każdy podłączony skierowany wykres acykliczny (i nie mogę wymyślić przypadku, w którym nie byłby to połączony DAG) może być wyświetlany jako drzewo, a stos jest zasadniczo drzewem.
Ed Ropple,
2
Stosy są podzbiorem drzew, które są podzbiorem DAG, które są podzbiorem wszystkich wykresów. Wszystkie stosy to drzewa, wszystkie drzewa to DAG, ale większość DAG nie jest drzewem, a większość drzew nie jest stosami. DAG mają uporządkowanie topologiczne, które pozwoli ci przechowywać je w stosie (do przejścia np. Rozwiązywanie zależności), ale kiedy wpychasz je do stosu, tracisz cenne informacje. W takim przypadku możliwość nawigowania między ekranem a jego rodzicem, jeśli ma on wcześniejsze rodzeństwo.
11

Oto przykładowa implementacja stosu gamestate, który okazał się bardzo przydatny: http://creators.xna.com/en-US/samples/gamestatemanagement

Jest napisany w C # i aby go skompilować potrzebujesz frameworka XNA, jednak możesz po prostu sprawdzić kod, dokumentację i wideo, aby uzyskać pomysł.

Może obsługiwać przejścia stanów, stany przezroczyste (takie jak modalne skrzynki komunikatów) i stany ładowania (które zarządzają rozładowaniem istniejących stanów i załadowaniem następnego stanu).

Używam teraz tych samych pojęć w moich projektach hobbystycznych (nie w języku C #) (oczywiście, może nie być odpowiedni dla większych projektów), a dla małych / hobbystycznych zdecydowanie mogę polecić takie podejście.

Janis Kirsteins
źródło
5

Jest to podobne do stosowanego przez nas stosu FSM. Zasadniczo po prostu daj każdemu stanowi funkcję enter, exit i tick i wywoływaj je w kolejności. Działa bardzo dobrze do obsługi rzeczy takich jak ładowanie.

koderanger
źródło
3

Jeden z tomów „Gem Programming Gems” miał implementację automatu stanów przeznaczoną dla stanów gry; http://emergent.net/Global/Documents/textbook/Chapter1_GameAppFramework.pdf zawiera przykład wykorzystania go w małej grze i nie powinien być zbyt specyficzny dla Gamebryo, aby był czytelny.

Tom Hudson
źródło
Pierwsza część „Programowania gier fabularnych z DirectX” implementuje także system stanów (i system procesów - bardzo interesujące rozróżnienie).
Ricket
To świetny dokument i wyjaśnia prawie dokładnie, jak zaimplementowałem go w przeszłości, bez zbędnej hierarchii obiektów, której używają w przykładach.
dash-tom-bang
3

Aby dodać trochę standaryzacji do dyskusji, klasycznym terminem CS dla tego rodzaju struktur danych jest automat pushdown .

hojny
źródło
Nie jestem pewien, czy jakakolwiek rzeczywista implementacja stosów państwowych jest prawie równoważna automatowi wypychania. Jak wspomniano w innych odpowiedziach, praktyczne implementacje niezmiennie kończą się poleceniami takimi jak „pop dwa stany”, „zamień te stany” lub „przekaż te dane do następnego stanu poza stosem”. A automat to automat - komputer - a nie struktura danych. Zarówno stosy stanów, jak i automaty wypychania używają stosu jako struktury danych.
1
„Nie jestem pewien, czy jakakolwiek rzeczywista implementacja stosów państwowych jest prawie równoważna z automatem wypychającym”. Co za różnica? Oba mają skończony zestaw stanów, historię stanów i prymitywne operacje na stanach push i pop. Żadna z innych operacji, o których wspominasz, nie różni się pod względem finansowym od tego. „Pop dwa stany” po prostu pojawia się dwa razy. „swap” to pop i push. Przekazywanie danych jest poza głównym założeniem, ale każda gra wykorzystująca „FSM” również wykorzystuje dodatkowe dane, nie czując, że nazwa już się nie stosuje.
wspaniałomyślny
W automacie wypychania jedynym stanem, który może wpłynąć na przejście, jest stan na górze. Zamiana dwóch stanów w środku jest niedozwolona; nawet patrzenie na stany w środku nie jest dozwolone. Wydaje mi się, że semantyczna ekspansja terminu „FSM” jest rozsądna i ma zalety (i nadal mamy terminy „DFA” i „NFA” dla najbardziej ograniczonego znaczenia), ale „automat pushdown” jest terminem ściśle informatycznym i tylko zamieszanie czeka, jeśli zastosujemy go do każdego systemu opartego na stosie.
Wolę te implementacje, w których jedynym stanem, który może wpłynąć na wszystko, jest stan znajdujący się na górze, chociaż w niektórych przypadkach przydatna jest możliwość filtrowania danych wejściowych stanu i przekazania przetwarzania do stanu „niższego”. (Np. Mapy przetwarzające dane wejściowe kontrolera do tej metody, najwyższy stan bierze bity, na których mu zależy, i prawdopodobnie je usuwa, a następnie przekazuje kontrolę do następnego stanu na stosie.)
dash-tom-bang
1
Dobry punkt, naprawiony!
wspaniały
1

Nie jestem pewien, czy stos jest całkowicie niezbędny, a także ogranicza funkcjonalność systemu państwowego. Używając stosu, nie można „wyjść” ze stanu do jednej z kilku możliwości. Załóżmy, że zaczynasz w „Menu głównym”, a następnie w „Załaduj grę”. Po pomyślnym załadowaniu zapisanej gry możesz przejść do stanu „Wstrzymaj” i powrócić do „Menu głównego”, jeśli użytkownik anuluje ładowanie.

Chciałbym tylko, aby stan określił stan, który ma być przestrzegany po wyjściu.

W przypadkach, w których chcesz powrócić do stanu poprzedzającego bieżący stan, na przykład „Menu główne-> Opcje-> Menu główne” i „Pauza-> Opcje-> Pauza”, po prostu przekaż jako parametr startowy do stanu stan, aby wrócić do.

Skizz
źródło
Może źle zrozumiałem pytanie?
Skizz
Nie, nie zrobiłeś tego. Myślę, że głosujący na nie głosował.
Kaczka komunistyczna
Korzystanie ze stosu nie wyklucza użycia jawnych przejść między stanami.
dash-tom-bang
1

Innym rozwiązaniem dla przejść i innych takich rzeczy jest zapewnienie stanu docelowego i źródłowego wraz z maszyną stanu, która może być powiązana z „silnikiem”, cokolwiek to może być. Prawda jest taka, że ​​większość maszyn stanowych prawdopodobnie będzie musiała być dostosowana do danego projektu. Jedno rozwiązanie może przynieść korzyść tej lub innej grze, inne rozwiązania mogą to utrudnić.

class StateMachine
{
public:
    StateMachine(Engine *);
    void Push(State *);
    State *Pop();
    void Update();
    Engine *GetEngine();

private:
    std::stack<State *> _states;
    Engine *_engine;
};

Stany są wypychane z bieżącym stanem i maszyną jako parametrami.

void StateMachine::Push(State *state)
{
    State *from = 0;
    if (!_states.empty()) from = _states.top();
    _states.push(state);
    state->Enter(this, from);
}

Stany pojawiają się w ten sam sposób. To, czy zadzwonisz Enter()na niższy Statenumer, jest kwestią dotyczącą implementacji.

State *StateMachine::Pop()
{
    _ASSERT(!_states.empty());
    State *state = _states.top();
    State *to = 0;
    _states.pop();
    if (!_states.empty()) to = _states.top();
    state->Exit(this, to);
    return state;
}

Wchodząc, aktualizując lub wychodząc, Statedostaje wszystkie potrzebne informacje.

void SomeGameState::Enter(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.Bind(this, &SomeGameState::KeyDown);
    LoadLevelState *state = new LoadLevelState();
    state->SetLevel(eng->GetSaveGame()->GetLevelName());
    state->Load.Bind(this, &SomeGameState::OnLevelLoaded);
    sm->Push(state);
}

void SomeGameState::Update(StateMachine *sm)
{
    Engine *eng = sm->GetEngine();
    float time = eng->GetFrameTime();
    if (shouldExit)
        sm->Pop();
}

void SomeGameState::Exit(StateMachine *sm, State *from)
{
    Engine *eng = sm->GetEngine();
    eng->GetKeyboard()->KeyDown.UnsubscribeAll(this);
}
Nick Bedford
źródło
0

Użyłem bardzo podobnego systemu w kilku grach i odkryłem, że z kilkoma wyjątkami, służy on jako doskonały model interfejsu użytkownika.

Jedynymi problemami, jakie napotkaliśmy, były przypadki, w których w niektórych przypadkach pożądane jest wycofanie wielu stanów przed wypchnięciem nowego stanu (przepuszczaliśmy interfejs użytkownika, aby usunąć wymaganie, ponieważ zwykle był to znak złego interfejsu użytkownika) i tworząc styl w stylu czarodzieja przepływy liniowe (rozwiązane łatwo poprzez przekazanie danych do następnego stanu).

Zastosowana implementacja owinęła stos i obsłużyła logikę aktualizacji i renderowania, a także operacje na stosie. Każda operacja na stosie wyzwalała zdarzenia w stanach, aby powiadomić ich o wystąpieniu operacji.

Dodano również kilka funkcji pomocniczych, aby uprościć typowe zadania, takie jak Zamień (Pop & Push, dla przepływów liniowych) i Resetuj (aby wrócić do menu głównego lub zakończyć przepływ).

Jason Kozak
źródło
Jako model interfejsu użytkownika ma to sens. Zawahałbym się nazywać je stanami, ponieważ w mojej głowie kojarzy mi się to z wnętrzami głównego silnika gry, podczas gdy „Menu główne”, „Menu opcji”, „Ekran gry” i „Ekran pauzy” są na wyższym poziomie, i często nie mają interakcji z wewnętrznym stanem gry podstawowej i po prostu wysyłają polecenia do silnika podstawowego w postaci „Wstrzymaj”, „Anuluj”, „Załaduj poziom 1”, „Poziom początkowy”, „Uruchom ponownie”, „Zapisz” i „Przywróć”, „ustaw poziom głośności 57” itp. Oczywiście może się to znacznie różnić w zależności od gry.
Kevin Cathcart
0

Takie podejście podchodzę do prawie wszystkich moich projektów, ponieważ działa niesamowicie dobrze i jest niezwykle proste.

Mój najnowszy projekt, Sharplike , dokładnie kontroluje przepływ kontroli. Wszystkie nasze stany są połączone zestawem funkcji zdarzeń, które są wywoływane, gdy zmieniają się stany, i zawiera koncepcję „nazwanego stosu”, w której można mieć wiele stosów stanów w ramach tej samej maszyny stanów i rozgałęzić się między nimi - konceptualna narzędzie i nie jest konieczne, ale przydatne.

Ostrzegałbym przed paradygmatem „powiedz kontrolerowi, który stan powinien podążać za tym, kiedy się kończy” sugerowany przez Skizz: nie jest strukturalnie zdrowy i tworzy takie rzeczy jak okna dialogowe (które w standardowym paradygmacie stanu stosu wymagają jedynie utworzenia nowego podać podklasę z nowymi członkami, a następnie odczytać ją po powrocie do stanu wywołującego) o wiele trudniej niż to musi być.

Ed Ropple
źródło
0

Zasadniczo użyłem tego dokładnego systemu w kilku układach ortogonalnie; na przykład stany interfejsu i menu gry (inaczej „pauza”) miały własne stosy stanów. Interfejs użytkownika w grze również używał czegoś takiego, chociaż miał aspekty „globalne” (takie jak pasek zdrowia i mapa / radar), które zmiana koloru może zabarwić, ale które zostały zaktualizowane we wspólny sposób w różnych stanach.

Menu w grze może być „lepiej” reprezentowane przez DAG, ale z domyślną maszyną stanu (każda opcja menu, która przechodzi do innego ekranu, wie, jak tam przejść, a naciśnięcie przycisku Wstecz zawsze wyświetlało stan najwyższy) efekt był dokładnie to samo.

Niektóre z tych innych systemów również miały funkcję „zastępowania stanu najwyższego”, ale zazwyczaj była ona wdrażana w StatePop()późniejszym terminie StatePush(x);.

Obsługa kart pamięci była podobna, ponieważ faktycznie wepchnąłem tonę „operacji” do kolejki operacji (która funkcjonalnie zrobiła to samo co stos, podobnie jak FIFO, a nie LIFO); kiedy zaczniesz korzystać z tego rodzaju struktury („teraz dzieje się jedna rzecz, a kiedy jest zrobiona, wyskakuje sama”), zaczyna infekować każdy obszar kodu. Nawet AI zaczęła używać czegoś takiego; AI było „nieświadome”, a następnie zmieniło się w „ostrożne”, gdy gracz wydawał dźwięki, ale nie było go widać, a następnie ostatecznie wzrosło do „aktywnego”, gdy zobaczyło gracza (i w przeciwieństwie do mniejszych gier tamtych czasów, nie można było się ukryć w kartonie i spraw, aby wróg zapomniał o tobie! Nie, że jestem zgorzkniały ...).

GameState.h:

enum GameState
{
   k_frontend,
   k_gameplay,
   k_inGameMenu,
   k_moviePlayback,
   k_numStates
};

void GameStatePush(GameState);
void GameStatePop();
void GameStateUpdate();

GameState.cpp:

// k_maxNumStates could be bigger, but we don't need more than
// one of each state on the stack.
static const int k_maxNumStates = k_numStates;
static GameState s_states[k_maxNumStates] = { k_frontEnd };
static int s_numStates = 1;

static void (*s_startupFunctions)()[] =
   { FrontEndStart, GameplayStart, InGameMenuStart, MovieStart };
static void (*s_shutdownFunctions)()[] =
   { FrontEndStop, GameplayStop, InGameMenuStop, MovieStop };
static void (*s_updateFunctions)()[] =
   { FrontEndUpdate, GameplayUpdate, InGameMenuUpdate, MovieUpdate };

static void GameStateStart(GameState);
static void GameStateStop(GameState);

void GameStatePush(GameState gs)
{
   Assert(s_numStates < k_maxNumStates);
   GameStateStop(s_states[s_numStates - 1])
   s_states[s_numStates] = gs;
   s_numStates++;
   GameStateStart(gs);
}

void GameStatePop()
{
   Assert(s_numStates > 1);  // can't pop last state
   s_numStates--;
   GameStateStop(s_states[s_numStates]);
   GameStateStart(s_states[s_numStates - 1]);
}

void GameStateUpdate()
{
   GameState current = s_states[s_numStates - 1];
   s_updateFunctions[current]();
}

void GameStateStart(GameState gs)
{
   s_startupFunctions[gs]();
}

void GameStateStop(GameState gs)
{
   s_shutdownFunctions[gs]();
}
dash-tom-bang
źródło