Archiwa tagu: maszyna stanów

Maszyny stanów

Niniejszy artykuł jest skrótem wykładu pt. „Maszyny stanów” (stanowiącego trzecią część cyklu „Programowanie gier bez Unity”), który odbył się 3 grudnia 2014 r. Slajdy z prezentacji można znaleźć pod tym adresem. Zachęcam do pobrania i rzucania na nie okiem podczas czytania artykułu – choćby dlatego, że w poniższym tekście brakuje kolorowania składni.

Problem

Powiedzmy, że piszemy grę i chcemy w niej dodać kilka scen…

Zaraz, zaraz. Czym są sceny? Najlepiej chyba podać przykłady. Jedną sceną jest menu główne. Drugą: sama rozgrywka. Kolejnymi scenami mogą być: intro, opcje gry, ekran wyboru poziomu…

Teraz – kiedy wiemy już, czym są sceny – zastanówmy się, jak dokładnie będą one wyglądać w naszej grze. Dla uproszczenia załóżmy, że całe sterowanie będzie się odbywało za pomocą klawiatury.

  • Zaczynamy od menu głównego. Chcemy, żeby po naciśnięciu klawisza [1] zaczynała się nowa gra (tzn. pojawiał się ekran wyboru poziomu). Po naciśnięciu [2] mają się pojawić creditsy, zaś [Esc] zamknie cały program.
  • Podczas wyświetlania creditsów ma się odtwarzać animowany film z nazwiskami wszystkich twórców. Jeśli znudzi nam się oglądanie go, to naciskając [Esc] możemy wrócić do menu głównego.
  • Ekran wyboru poziomu oferuje nam (póki co) tylko pierwszy level. Po naciśnięciu [1] ma się rozpocząć gra na tym właśnie pierwszym poziomie.
  • W samej grze chcemy zaimplementować opcję szybkiego wyjścia – naciskamy [Esc] i cały program się zamyka.

Problem określony. Warto go mieć w głowie, gdyż będę się do niego odwoływał prawie do końca artykułu.

W każdym razie – pora wziąć się za programowanie… (Na marginesie – przykłady kodu będą w C++, ale wiedzę z tego artykułu można zastosować do dowolnego języka obiektowego.)

Próba rozwiązania – podejście pierwsze

Jesteśmy już po wykładzie Daltona pt. „Jak działa gra”, więc wiemy, że w silniku mamy pętlę główną, która wykonuje w kółko input(), update() i render(). Powiedzmy, że piszemy akurat tę pierwszą funkcję.

Zakładamy, że znajdujemy się w menu głównym i chcemy zaprogramować wychodzenie z gry przy pomocy [Esc].

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        exit();
    }
}

Na razie wszystko w porządku. Dodajmy więc puszczenie filmiku z creditsami po naciśnięciu klawisza [2]:

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        exit();
    }
    else if (input == KEY_2)
    {
        startDisplayingCredits();
    }
}

Nadal wszystko w porzą… A nie, jednak nie. Jeżeli naciśniemy [2], a potem po raz kolejny [2], to filmik zacznie się znowu odtwarzać od początku. Przytrzymując ten klawisz zobaczymy właściwie tylko pierwszą klatkę. Trzeba to naprawić!

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        exit();
    }
    else if (input == KEY_2)
    {
        if (!displayingCredits)
        {
            displayingCredits = true;
            startDisplayingCredits();
        }
    }
}

Jak widać, zadeklarowaliśmy sobie (gdzieś w klasie) dodatkową flagę. Mówi nam ona o tym, czy w danym momencie wyświetlają się creditsy.

Wygląda na to, że wszystko działa tak, jak powinno. Zatem dodajmy teraz możliwość przedwczesnego wyłączenia filmiku po naciśnięciu [Esc]:

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        if (!displayingCredits)
        {
            exit();
        }
        else
        {
            displayingCredits = false;
            stopDisplayingCredits();
        }
    }
    else if (input == KEY_2)
    {
        if (!displayingCredits)
        {
            displayingCredits = true;
            startDisplayingCredits();
        }
    }
}

Creditsy załatwione, więc zajmijmy się teraz ekranem wyboru poziomu.

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        // ...
    }
    else if (input == KEY_1)
    {
        showLevelsToChoose();
    }
    else if (input == KEY_2)
    {
        // ...
    }
}

W powyższym kodzie mamy podobnego buga jak wcześniej. Zatem naprawiamy:

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        // ...
    }
    else if (input == KEY_1)
    {
        if (!displayingLevels)
        {
            displayingLevels = true;
            showLevelsToChoose();
        }
    }
    else if (input == KEY_2)
    {
        // ...
    }
}

Okazuje się jednak, że to nie jedyny błąd. Co się stanie po wciśnięciu [1] podczas oglądania creditsów? No właśnie…

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        // ...
    }
    else if (input == KEY_1)
    {
        if (!displayingLevels && !displayingCredits)
        {
            displayingLevels = true;
            showLevelsToChoose();
        }
    }
    else if (input == KEY_2)
    {
        // ...
    }
}

A czy z pozostałymi ifami jest wszystko w porządku? Otóż nie:

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        if (!displayingCredits && !displayingLevels)
        {
            exit();
        }
        else if (!displayingLevels)
        {
            displayingCredits = false;
            stopDisplayingCredits();
        }
    }
    else if (input == KEY_1)
    {
        // ...
    }
    else if (input == KEY_2)
    {
        // ...
    }
}

Podobnie w przypadku klawisza [2]:

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        // ...
    }
    else if (input == KEY_1)
    {
        // ...
    }
    else if (input == KEY_2)
    {
        if (!displayingCredits && !displayingLevels)
        {
            displayingCredits = true;
            startDisplayingCredits();
        }
    }
}

I tak dalej… Namęczyliśmy się, a większość naszej pracy polegała na szukaniu i naprawianiu błędów. A przecież nie napisaliśmy jeszcze wszystkiego, co założyliśmy na początku! Zostało startowanie gry na pierwszym poziomie i wychodzenie z niej za pomocą [Esc]. Ostateczny kod wyglądałby mniej więcej tak:

void handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        if (!displayingCredits && !displayingLevels)
        {
            exit();
        }
        else if (!displayingLevels && !runningGame)
        {
            displayingCredits = false;
            stopDisplayingCredits();
        }
    }
    else if (input == KEY_1)
    {
        if (!displayingLevels && !displayingCredits && !runningGame)
        {
            displayingLevels = true;
            showLevelsToChoose();
        }
        else if (displayingLevels)
        {
            runningGame = true;
            runGame(1);
        }
    }
    else if (input == KEY_2)
    {
        if (!displayingCredits && !displayingLevels && !runningGame)
        {
            displayingCredits = true;
            startDisplayingCredits();
        }
    }
}

Chyba widać, że z naszym podejściem jest coś nie tak. Skończyliśmy z bardzo skomplikowanym kodem, pełnym rozbudowanych ifów i brzydkich flag. Być może kryją się w nim jeszcze jakieś błędy. O dodawaniu kolejnych scen nie chcę nawet wspominać – wystarczy sobie wyobrazić, ile to pracy będzie wymagało.

Ewidetnie potrzebujemy innego podejścia.

Próba rozwiązania – podejście drugie

Spójrzmy na ten problem nieco inaczej – jak na graf. Wierzchołkami będą sceny (nazwijmy je stanami), zaś krawędziami – przejścia pomiędzy nimi. Okazuje się, że można to wtedy rozrysować w poniższy sposób (oczywiście Exit nie jest osobnym stanem, ale musiałem go jakoś zaznaczyć).

graf_5Uzyskaliśmy właśnie całkiem prostą reprezentację problemu. Przy okazji zauważamy bardzo ważną rzecz: możemy znajdować się tylko w jednym stanie jednocześnie. Zatem wszystkie nasze flagi (displayingLevels, displayingCredits…) są bez sensu – jeśli dobrze je zaprogramujemy, to w danym momencie tylko jedna będzie ustawiona na true. Wobec tego powinna wystarczyć tylko jedna zmienna. Może enum?

void handleInput(Input input)
{
    switch (currentState)
    {
    case MAIN_MENU:
        if (input == KEY_ESC)
        {
            exit();
        }
        else if (input == KEY_1)
        {
            showLevelsToChoose();
            currentState = LEVEL_CHOICE;
        }
        else if (input == KEY_2)
        {
            startDisplayingCredits();
            currentState = CREDITS;
        }
        break;
    case CREDITS:
        if (input == KEY_ESC)
        {
            stopDisplayingCredits();
            currentState = MAIN_MENU;
        }
        break;
    case LEVEL_CHOICE:
        if (input == KEY_1)
        {
            runGame(1);
            currentState = GAME;
        }
        break;
    case GAME:
        if (input == KEY_ESC)
        {
            exit();
        }
        break;
    }
}

Takie rozwiązanie okazuje się dużo prostsze i bardziej eleganckie. Odseparowujemy od siebie poszczególne stany, dzięki czemu łatwo zrozumieć, co robi dana funkcja. Ponadto dzięki tym cechom jest ona dużo mniej podatna na błędy.

Niestety, jest też kilka wad. Dodawanie kolejnych scen jest dość uciążliwe – musimy dodać nową wartość enuma, musimy dodać nowy case w switchu – i to aż trzy razy, w każdej z funkcji handleInput(), update(), render(). W tym artykule dla uproszczenia zajmujemy się tylko pierwszą z nich, ale przecież w prawdziwym kodzie musielibyśmy pamiętać o wszystkich. Switche są też dość rozbudowane, przez co wspomniane funkcje robią się bardzo długie.

No i… chcielibyśmy dodać animowane przejścia pomiędzy scenami. Operując na enumie i switchu byłoby to dość skomplikowane. I brzydkie. Trzeba więc pomyśleć o czymś innym…

Warto tutaj zaznaczyć, że powyższe rozwiązanie już jest maszyną stanów. Stało się nią, gdy przedstawiliśmy problem w postaci grafu. Tym niemniej, wspomniane wyżej wady sprawiają, że taka implementacja nie do końca nas satysfakcjonuje; musimy poszukać jakiejś innej.

Maszyna stanów

Jak już wspomniałem, z maszyną stanów (ang. Finite State Machine – FSM) mamy do czynienia, gdy przedstawimy jakiś problem w postaci grafu. A dokładniej:

  • mamy pewien skończony zbiór stanów (wierzchołki),
  • pomiędzy stanami istnieją przejścia (krawędzie),
  • możemy znajdować się tylko w jednym stanie jednocześnie.

To jest jednak tylko abstrakcyjna idea – jej implementacja to już zupełnie inna historia. Wiemy, że maszynę stanów możemy zaimplementować przy pomocy typu wyliczeniowego. Dlaczego więc tego sposobu nie zawarłem w obecnym podrozdziale, tylko w poprzednim? Ponieważ istnieje rozwiązanie znacznie lepsze, sprawdzone i dużo częściej wykorzystywane (można je zatem nazywać wzorcem projektowym).

Spróbujemy do niego dojść sami. Co tak naprawdę powoduje kłopoty w naszym kodzie? Rozbudowany switch. Musimy się go jakoś pozbyć!

Spójrzmy więc na problem nieco inaczej (znowu!). Powiedzmy, że jesteśmy szefem w pewnej firmie i mamy kilku pracowników. Za każdym razem, gdy otrzymujemy zadanie do wykonania, zastanawiamy się, któremu pracownikowi je przekazać – i dopiero po tym mu je wręczamy. Nasi pracownicy są case’ami, zaś to zastanawianie się to właśnie switch. I to on powoduje problem – przez niego mamy pracę do wykonania.

szef_2Możemy podejść do problemu w inny sposób. Zwalniamy wszystkich pracowników poza jednym – aktualnie pracującym. Jemu przekazujemy (bez zastanawiania się) wszystkie otrzymane zadania. Gdy pracownik stwierdzi, że wykonał swoją pracę, zwalniamy go i zatrudniamy innego. Teraz temu kolejnemu przekazujemy każde otrzymane zadanie… I tak w kółko.

szef_3Pozbyliśmy się switcha. Żeby jednak zaimplementować ten pomysł, będziemy musieli sobie przypomnieć co nieco o polimorfizmie.

Polimorfizm

Czym więc jest polimorfizm? Najłatwiej wyjaśnić to na przykładzie. Stwórzmy sobie bardzo ogólną, abstrakcyjną klasę, np. reprezentującą zwierzę.

class Zwierze
{
public:
    virtual void dajGlos() = 0;
};

W rzeczywistości nie istnieją zwierzęta. Głupio to brzmi, ale tak jest – istnieją psy, koty, krowy, ale nie ma osobników, które byłby po prostu „zwierzęciem”. Wobec tego nie będziemy tworzyli obiektów klasy Zwierze. Stąd taka jej implementacja – jest to interfejs, który mówi nam „każde zwierzę może dać głos”. I tylko tyle, nic więcej.

Zatem po co nam taka klasa? Ponieważ możemy zdefiniować jej klasy pochodne:

class Pies : public Zwierze
{
public:
    void dajGlos() {cout << "Hau hau" << endl;}
};

class Kot : public Zwierze
{
public:
    void dajGlos() {cout << "Miau miau" << endl;}
};

class Krowa : public Zwierze
{
public:
    void dajGlos() {cout << "Muuuu" << endl;}
};

Klasy Pies, Kot i Krowa dziedziczą po klasie Zwierze, czyli implementują interfejs Zwierze. Przypomnijmy – interfejs ten mówi nam: „każde zwierzę może dać głos”. Dziedziczenie mówi: „jestem zwierzęciem”. Wobec tego taka konstrukcja oznacza, że zarówno pies, jak i kot czy krowa mogą dać głos. A jaki dźwięk to oznacza w każdym z tych przypadków (czy „Hau hau”, czy „Miau miau”, czy może „Muuuu”) – o tym decydują już poszczególne klasy.

Mając tak zorganizowaną hierarchię klas, możemy pod zmienną typu Zwierze* upchnąć dowolne zwierzę: psa, kota i krowę. Dzięki odpowiedniemu interfejsowi kompilator wie, że każde zwierzę może dać głos. Zatem poniższy kod jest w pełni poprawny:

Zwierze* zwierzeta[3] = {new Pies, new Kot, new Krowa};

for (int i = 0; i < 3; i++)
{
    zwierzeta[i]->dajGlos();
}

Co się stanie w poszczególnych iteracjach pętli? O tym zdecydują konkretne klasy. W zależności od tego, czy pod daną zmienną upchnęliśmy psa, kota czy krowę, zostanie wywołana metoda odpowiedniej klasy. Wynik działania programu będzie następujący:

Hau hau
Miau miau
Muuuu

To jest właśnie polimorfizm, czyli wielopostaciowość – jedna metoda (w tym przypadku dajGlos()) może mieć kilka postaci – może być używana na kilka sposobów.

Implementacja maszyny stanów

Uzbrojeni w tę wiedzę możemy przejść do implementacji maszyny stanów. Przede wszystkim będziemy mieli interfejs definiujący nam abstrakcyjny „stan”. Odwołując się do przykładu z szefem – będzie to nasz abstrakcyjny „pracownik”. Określamy czynności, które każdy pracownik musi umieć wykonać (np. pracuj()). Nieważne, czy jest to sekretarka, czy marketingowiec; nieważne też, co dla nich oznacza pracuj(). Muszą po prostu umieć „pracować”.

class State
{
public:
    virtual void handleInput(Input) = 0;
    virtual void update(float) = 0;
    virtual void render() = 0;
    virtual void show() = 0;
    virtual void hide() = 0;
};

Nazwa State jest oczywiście umowna, w różnych miejscach można się spotkać także z innymi, np. Screen czy Scene. Znaczenie pierwszych trzech metod jest raczej oczywiste, dwie ostatnie wyjaśnię później.

Tworzymy maszynę stanów, czyli naszego szefa:

class StateMachine
{
private:
    State* currentState;
public:
    void handleInput(Input);
    void update(float);
    void render();
    void changeState(State*);
};

Znowuż – można się spotkać z różnymi nazwami tej klasy (StateMachine, ScreenManager, Director…). My pozostaniemy przy tej pierwszej.

Nasz szef posiada obecnego pracownika (currentState). Zmienna jest typu State*, więc będziemy mogli do niej upchnąć dowolną klasę dziedziczącą po State. Oprócz tego szef ma również metodę changeState(State*), którą można przetłumaczyć jako „zmień obecnego pracownika na tego, którego podam ci w parametrze”.

Tak wygląda implementacja handleInput(), update() i render():

void StateMachine::handleInput(Input input)
{
    currentState->handleInput(input);
}

void StateMachine::update(float deltaTime)
{
    currentState->update(deltaTime);
}

void StateMachine::render()
{
    currentState->render();
}

Przekazujemy po prostu otrzymane zadanie do aktualnego pracownika. Nie zastanawiamy się, kto to jest i czym się zajmuje. Nie wiemy, czy aktualnie pracuje dla nas sprzątaczka czy marketingowiec. Nie jest to ważne. Dostajemy zadanie i przekazujemy je dalej – jakiemuś pracownikowi. To, jak wykona otrzymane zadanie, to już jego sprawa (w tym miejscu korzystamy z polimorfizmu!).

Tak wygląda zmiana pracownika:

void StateMachine::changeState(State* newState)
{
    currentState->hide();
    delete currentState;
    currentState = newState;
    currentState->show();
}

Tutaj widać, do czego służą metody State::hide() oraz State::show(). Pierwsza oznacza „pożegnaj się, bo zaraz zostaniesz zwolniony”, zaś druga „przywitaj się, właśnie cię zatrudniliśmy”. Jak się potem okaże, będą one bardzo przydatne.

Jeszcze mała uwaga na marginesie. Nie musimy za każdym razem tworzyć nowego stanu i delete’ować starego. Możemy wszystkie możliwe stany trzymać w jakiejś strukturze danych i tylko zaznaczyć, który aktualnie dla nas pracuje. Dla uproszczenia przykładu przyjąłem jednak, że przy każdej zmianie stanu będziemy tworzyć nowy za pomocą new, a stary kasować przy użyciu delete.

Teraz zajmijmy się konkretnymi stanami, czyli konkretnymi pracownikami (sekretarka, marketingowiec, sprzątacz, konsultant…). Na pierwszy ogień idzie MainMenu:

class MainMenu : public State
{
public:
    void handleInput(Input);
    void update(float);
    void render();
    void show();
    void hide();
};

void MainMenu::handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        exit();
    }
    else if (input == KEY_1)
    {
        // showLevelsToChoose();
        stateMachine->changeState(new LevelChoice);
    }
    else if (input == KEY_2)
    {
        // startDisplayingCredits();
        stateMachine->changeState(new Credits);
    }
}

Jak widać, wszystko co związane z jednym stanem, trzymamy w jednej klasie. Dlatego też zrezygnowaliśmy z funkcji showLevelsToChoose() i startDisplayingCredits(), które znajdowały się w tych miejscach w rozwiązaniu z enumem. Tym razem znajdą się gdzie indziej.

Kolejny konkretny stan – Credits:

class Credits : public State
{
public:
    void handleInput(Input);
    void update(float);
    void render();
    void show();
    void hide();
};

void Credits::handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        // stopDisplayingCredits();
        stateMachine->changeState(new MainMenu);
    }
}

void Credits::show()
{
    startDisplayingCredits();
}

void Credits::hide()
{
    stopDisplayingCredits();
}

Przydały nam się metody show() i hide() – dzięki nim mogliśmy wywołanie startDisplayingCredits() przenieść z klasy MainMenu do Credits (co ma więcej sensu); stopDisplayingCredits() również znalazło się w bardziej logicznym miejscu.

Trzeci stan – ekran wyboru poziomu:

class LevelChoice : public State
{
public:
    void handleInput(Input);
    void update(float);
    void render();
    void show();
    void hide();
};

void LevelChoice::handleInput(Input input)
{
    if (input == KEY_1)
    {
        // runGame(1);
        stateMachine->changeState(new Game(1));
    }
}

void LevelChoice::show()
{
    showLevelsToChoose();
}

Jak przekazać, na którym poziomie mamy rozpocząć grę? Wcześniej wywoływaliśmy funkcję runGame() z odpowiednim parametrem. Teraz tworzymy obiekt gry z numerem levelu podanym jako parametr konstruktora.

W końcu ostatni stan – Game – wzbogacony właśnie o ten konstruktor i prywatne pole oznaczające poziom, z którego zaczynamy grę. Warto zauważyć, że interfejs określa tylko minimalną funkcjonalność naszej klasy – możemy ją dowolnie rozbudowywać.

class Game : public State
{
private:
    int level;
public:
    Game(int);
    void handleInput(Input);
    void update(float);
    void render();
    void show();
    void hide();
};

Game::Game(int lvl)
    : level(lvl)
{}
 
void Game::handleInput(Input input)
{
    if (input == KEY_ESC)
    {
        exit();
    }
}

void Game::show()
{
    runGame(level);
}

I… Koniec.

Udało nam się zaimplementować maszynę stanów w sposób elegancki i zgodny z zasadami programowania obiektowego. Wydzieliliśmy funkcjonalność każdej sceny do osobnej klasy, uzyskaliśmy również krótkie i zrozumiałe funkcje. Dzięki temu kod jest mało podatny na błędy. W dodatku dodawanie kolejnych scen jest proste – polega po prostu na napisaniu kolejnej klasy.

Ze wszystkich przedstawionych rozwiązań problemu to jest zdecydowanie najlepsze.

Inne zastosowania

Maszyny stanów można wykorzystać również do innych rzeczy.

Po pierwsze, łatwo jest z ich pomocą zaimplementować animowane przejścia pomiędzy scenami. Przypomnijmy sobie graf scen. Teraz na każdej krawędzi (czyli przejściu) postawmy dodatkowy wierzchołek – specjalny stan TransitionState, który w parametrach konstruktora otrzymuje stan obecny i przyszły. Jego jedynym zadaniem jest odegrać ładną animację przejścia i przełączyć się na stan docelowy. Voilà!

Drugim zastosowaniem może być użycie FSM do obsługi stanów głównego bohatera, takich jak bieganie, skakanie, walka czy po prostu stanie w miejscu. Każdy z nich wymaga innej animacji i innej obsługi wejścia – przykładowo podczas skoku powinniśmy ignorować spację, żeby nie skoczyć w powietrzu drugi raz. Przykład ten jest dobrze opisany w tym artykule.

Maszynę stanów można również zastosować do modelowania prostej sztucznej inteligencji. Za pomocą grafu stanów da się przedstawić całkiem skomplikowane zachowania – które później wystarczy zaprogramować w sposób analogiczny do przedstawionego w niniejszym artykule. Po przykład takiego grafu stanów (opisującego zachowanie strażnika patrolującego pewien obszar) odsyłam do slajdów z prezentacji (link na początku artykułu).

Podsumowanie

Maszyny stanów pozwalają nam uprościć wiele rzeczy. Jeśli pomyślimy nad danym problemem jak nad grafem, może się on okazać całkiem prosty do rozwiązania. Kolejną zaletą jest lepsza organizacja kodu – wydzielenie funkcjonalności jednego stanu do jednej klasy, a co za tym idzie poprawa czytelności i mniejsza podatność na błędy.

Jednak czy zawsze warto stosować tę koncepcję, a w szczególności tę ostatnią implementację z kilkoma klasami? Oczywiście że nie. Jeśli mamy zastąpić dwa proste ify dodatkowymi klasami, to zupełnie nie warto. To by nam tylko skomplikowało kod. Nie wpychajmy wzorców projektowych na siłę wszędzie gdzie się da.

Moja rada jest taka: jeśli wiemy, że nasz system będzie dość duży, od razu stosujmy maszynę stanów. Jeśli jest mały – zróbmy go po prostu na ifach lub enumach. Gdy jednak zaczyna się rozrastać, rozważmy zrobienie z niego maszyny stanów. Opłaci nam się to w przyszłości.

Literatura uzupełniająca

Jeśli jesteś zainteresowany tematyką wzorców projektowych, to szczególnie polecam dwie książki:

  • „Game Programming Patterns” (Robert Nystrom) – można ją za darmo przeczytać na stronie autora. Jeden rozdział poświęcony jest właśnie maszynom stanów.
  • „Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku” (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) – nie jest to łatwa lektura, ale zawiera skondensowaną wiedzę, a poza tym wymaga rozumienia koncepcji OOP i znajomości odpowiedniej terminologii. Na Wikipedii znajduje się jej streszczenie (choć bardziej polecam całość!).