Deus ex adfectus machina – menedżer stanów

Skąd taki tytuł?

Deus ex machina (łac.) – Bóg z maszyny

adfectus (łac.) – stan

Za Eurypisesem wprowadzę do Theme Festival coś, co szybko rozwiąże problem zarządzaniem grą, „aby akcja nie trwała zbyt długo” (za Wikipedią). Ten wpis poświęcę koncepcji menedżera stanów, „Boga z maszyny stanów”.

O co tu chodzi?

Całą grę należy wyobrazić sobie jako graf przejść między stanami. Stanem gry może być wszystko, od menu, przez okno opcji, po różne etapy rozgrywki. Przejścia między stanami mogą być uruchamiane wciśnięciem klawisza, uruchomieniem odpowiedniej opcji w GUI, wystąpieniem odpowiedniego zdarzenia w grze, lub po prostu minięciem określonego odstępu czasu.

Jeden obraz starczy za tysiąc słów

Poniżej prosty przykład zbioru stanów oraz przejść między nimi:

Diagram stanów

Każde z pól symbolizuje pewien stan, w jakim znajduje się aplikacja oraz jakie możliwości ma użytkownik. Po uruchomieniu programu (lewa „kropka”) ładowane jest menu główne, z którego można przejść do opcji (i dalej, do opcji grafiki, dźwięku etc.). Z każdego z tych stanów można wrócić do menu opcji, a stamtąd do menu głównego. Nie można bezpośrednio ze stanu „Opcje dźwięku” dostać się do „Pomoc” i tak dalej, i tak dalej. Koncepcja jest na tyle przejrzysta, że bardzo łatwo można rozpisać parę kartek (tak, papier i kredki to najlepszy program do diagramów) z najdrobniejszymi detalami aplikacji.

Stan

Wyodrębnijmy rzeczy, za które każdy ze stanów (niezależnie od rodzaju) będzie musiał być odpowiedzialny.

  • Rysowanie
  • Ładowanie i zwalnianie zasobów przy wchodzeniu do danego stanu i opuszczaniu go
  • Reakcje na urządzenia wejścia/wyjścia (mysz + klawiatura)

W ten sposób powstaje interfejs IState:

public interface IState
{
    /// <summary>
    /// Initializes the state. Perform loading here.
    /// </summary>
    void Initialize();

    /// <summary>
    /// Pauses the state. The state is not updated while paused.
    /// </summary>
    void Pause();

    /// <summary>
    /// Resumes the state.
    /// </summary>
    void Resume();

    /// <summary>
    /// Perform unloading here.
    /// </summary>
    void Dispose();

    string Name { get; }

    /// <summary>
    /// MouseClick handler
    /// </summary>
    void OnMouseClick(MouseState mouseState);

    /// <summary>
    /// Keyboard handler
    /// </summary>
    void OnKeyboardClick(KeyboardState keyboardState);

    /// <summary>
    /// Draw state content.
    /// </summary>
    /// <param name="gameTime">Time since last frame</param>
    void Draw(GameTime gameTime);

    /// <summary>
    /// Update simulation.
    /// </summary>
    /// <param name="gameTime">Time since last frame</param>
    void Update(GameTime gameTime);
}

Od tej pory każdy stan będzie musiał implementować ten interfejs, a to dlatego, że zarządzaniem całością będzie zajmował się

Menedżer stanów

Menedżer stanów jest naturalną konsekwencją pomysłu stanów i ich zbioru. Jego głównym elementem jest stos stanów. Został zaimplementowany używając wzorca Singleton.

Przy wejściu do nowego stanu, aktualny jest pauzowany, aktualny jest wstawiany na szczyt stosu i następuje jego inicjalizacja. Taka sytuacja jest naturalna, gdy np. w trakcie rozgrywki Gracz wciśnie klawisz Esc. Gra jest pauzowana, na stos wrzucany jest stan menu w trakcie gry (tu inicjalizacja). Potem jest aktualizowany (sprawdzane jest położenie myszy, następuje ewentualne podświetlanie przycisków etc.) oraz odrysowywany. Gdy Gracz ponownie naciśnie Esc (powrót do gry) nastąpi zakończenie aktualnego stanu (menu zostaje schowane) i powrót do gry (rozpoczęcie od momentu, w którym została spauzowana).

Menedżer stanów posiada również metody podobne do tych znajdujących się w interfejsie IState: OnMouseClick, OnKeyboardClick, Draw, Update. Ich działanie jest bardzo proste – zawierają jedynie wywołania odpowiednich metod dla aktualnego (znajdującego się na szczycie stosu) stanu.

Poniżej kod klasy StateManager:

/// <summary>
/// StateManager is a Singleton
/// </summary>
public class StateManager
{
    /// <summary>
    /// Pauses current state and starts new one
    /// </summary>
    /// <param name="state">New state</param>
    public void PushState(IState state)
    {
        if (states.Count > 0)
            states.Peek().Pause();
        states.Push(state);
        states.Peek().Initialize();
    }

    /// <summary>
    /// Removes current state and resumes previous one
    /// </summary>
    public void PopState()
    {
        if (states.Count > 0)
            states.Pop();

        if (states.Count > 0)
            states.Peek().Resume();
    }

    public string CurrectStateName
    {
        get { return states.Peek().Name; }
    }

    public void OnMouseClick(MouseState mouseState)
    {
        states.Peek().OnMouseClick(mouseState);
    }

    public void OnKeyboardClick(KeyboardState keyboardState)
    {
        states.Peek().OnKeyboardClick(keyboardState);
    }

    public void Draw(GameTime gameTime)
    {
        states.Peek().Draw(gameTime);
    }

    public void Update(GameTime gameTime)
    {
        states.Peek().Update(gameTime);
    }

    public static StateManager Instance
    {
        get { return instance ?? (instance = new StateManager()); }
    }

    protected StateManager()
    {
        states = new Stack<IState>();
    }

    private static StateManager instance;
    private readonly Stack<IState> states;
}

Kod jest już dostępny w repozytorium, chciałbym od razu zaznaczyć, że może się on zmienić (i raczej na 99% się zmieni) wraz z rozwojem aplikacji. Koncepcja jednak pozostanie taka, jaka została zaprezentowana powyżej.

Be Sociable, Share!
czoper opublikowano dnia 2010-9-16 Kategoria: Daj się poznać, Programowanie, Theme Festival | Tagi:, ,

Odpowiedzi: 4 Zostaw komentarz

  1. #4czoper @ 2010-9-17 10:49

    „corpus delicti” pewnie użyję, jak będę pisał o próbie modelowania postaci w Blenderze – dzięki za podsunięcie pomysłu 😉

  2. #3macias @ 2010-9-17 10:39

    Unikam flame’ow, ale dopowiem tylko tyle, ze:

    ” chcę napisać o maszynie stanów -> „deus ex machina” ”

    ma to sie nijak do siebie. Rownie dobrze mozesz pisac w kazdym kolejnym tekscie:

    ” chcę napisać o XYZ -> „deus ex machina” ”

    „jak powiązać we wstępie „deus ex machina” i maszynę stanów?”

    Czyli gdyby spodobalo Ci sie wyrazenie np. „corpus delicti” to zastanawialbys sie jak je polaczyc z maszyna stanow? 🙂

    Z mojej strony to tyle, reszta bedzie redundantna.

  3. #2czoper @ 2010-9-17 07:57

    i vice-versa 😉

    Nigdzie w tekście nie napisałem, że odnoszę się do Boga kontrolującego maszynę (tym bardziej maszynę stanów). Żeby nie było nieporozumień przedstawię mój tok myślenia, który towarzyszył pisaniu notki:
    chcę napisać o maszynie stanów -> „deus ex machina” -> hmmm, jak jest „stan” po łacinie? -> jak powiązać we wstępie „deus ex machina” i maszynę stanów? -> wstęp 😉

    Chodziło mi bardziej o to, że maszyna stanów pojawia się niespodziewanie i zamiata problem zarządzania tym co, kiedy i jak rysować, uaktualniać i reagować na sterowanie.

    Clear?

  4. #1macias @ 2010-9-17 04:42

    — Panie prezydencie, jak fama glosi nie rozumie pan slow, ktorych pan uzywa.
    — Taaak? To niech pani powie tej famie, ze moze mnie pocalowac w d*pe i vice-versa.

    Deus ex machina nie oznacza boga kontrolujacego maszyne, ale boska interwencje („krolik z kapelusza”) — jest dokladnym zaprzeczeniem tego, jak tego terminu uzyles (zwlaszcza, ze Twoj automat jest deterministyczny!). Masz to zreszta wylozone kawa na lawe w tekscie, ktory linkujesz.

Zostaw odpowiedź

(Ctrl + Enter)