Паттерн Состояние в разработке игр

Все мы делаем игры, и во всех наших играх всегда есть состояния. Главное меню, уровень, миниигра, пауза, переход между экранами и многое другое — все это подходит под определение состояния.
Если вы не хотите превратить ваш код в кашу из разбросанных и сильно связанных состояний, паттерн «Состояние» — то, что вам нужно!

Под катом объяснение что же это такое и пример применения в коде игры.



Суть паттерна — создать единый интерфейс для всех состояний игры и использовать одни и те же методы, наделенные разным поведением в зависимости от текущего состояния.

Диаграмма классов


Простейшая реализация паттерна для игры будет выглядит так



Интерфейс IGameState определяет методы, реализуемые всеми состояниями игры. MainMenuState и GameplayState — конкретные классы реализации игровых состояний. Они реализуют интерфейс IGameState. Класс StateManager отвечает за смену состояний.

Рассмотрим код


Сначала создаем интерфейс, который будут реализовывать все наши состояния. Для примера будем считать, что каждое состояние нашей игры будет будет обновляться каждый кадр функцией update() и уничтожаться функцией destroy().

package  
{
        public interface IGameState 
        {
                function update():void;
                function destroy():void;
        }

}


Теперь создадим конкретные состояния. Ниже приведен код состояния MainMenuState. Точно также выглядит код для GameplayState

package  
{
        public class MainMenuState implements IGameState 
        {
                private var stateManager:StateManager;

                public function MainMenuState(_stateManager:StateManager) 
                {
                        stateManager = _stateManager;
                        trace("Creating main menu");
                }

                public function update():void
                {
                        stateManager.changeState(StateManager.GAMEPLAY_STATE); //Где-то в основном цикле происходит переход в состояние игрового процесса
                }

                public function destroy():void
                {
                        trace("Destroying main menu");
                }
        }
}


Теперь нам нужен класс, который будет управлять состояниями, в том числе совершать переходы между ними. Назовем его StateManager.

package  
{
        public class StateManager 
        {
                public static const MAIN_MENU_STATE:int = 1;
                public static const GAMEPLAY_STATE:int = 2;

                private var currentState:IGameState;

                public function StateManager() 
                {

                }

                public function changeState(newState:int):void
                {
                        if (currentState)
                        {
                                currentState.destroy();
                        }
                        switch (newState) 
                        {
                                case MAIN_MENU_STATE:
                                        {
                                                currentState = new MainMenuState(this);
                                                break;
                                        }
                                case GAMEPLAY_STATE:
                                        {
                                                currentState = new GameplayState(this);
                                                break;
                                        }
                        }
                }
                public function update():void
                {
                        currentState.update();
                }
        }
}


В этом классе объявляются константы, соответствующие нашим состояниям. Функция changeState() реализует выбор и смену состояния. Как мы видим, нам не нужно держать переменные для каждого сотояния, достаточно просто использовать currentState. Все остальное за нас сделает полиморфизм :)

Тест


Таким образом остался последний штрих — написать простой тест.

package 
{
        import flash.display.Sprite;
        import flash.events.Event;

        public class Main extends Sprite 
        {
                private var stateManager:StateManager;

                public function Main():void 
                {
                        if (stage) init();
                        else addEventListener(Event.ADDED_TO_STAGE, init);
                }

                private function init(e:Event = null):void 
                {
                        removeEventListener(Event.ADDED_TO_STAGE, init);

                        // entry point

                        addEventListener(Event.ENTER_FRAME, onEnterFrame);

                        stateManager = new StateManager();
                        stateManager.changeState(StateManager.MAIN_MENU_STATE);

                }

                private function onEnterFrame(event:Event):void
                {
                        stateManager.update();
                }
        }
}


Компилируем и получаем в выводе:

[Starting debug session with FDB]
Creating main menu
Destroying main menu
Creating gameplay


Все работает!

В чем же прелесть такого подхода в реализации состояний? Самое главное — это изоляция состояний. То есть каждое состояние отвечает только за свой участок работы приложения. Это приводит к более понятному, логичному и отказоустойчивому коду, а также простоте расширения. Мы можем добавить сколько угодно состояний, не внося правки в кучу классов и при этом сохраняя архитектуру игры неизменной.

Исходники (делалось в FlashDevelop)

Для интересующихся могу порекоммендовать книги:

ActionScript 3.0. Шаблоны проектирования
Паттерны проектирования

Комментарии (22)

0
И чо создавать кучу классов состояний? Неудобно же.

Я всегда так делаю, в каждом классе где нужны состояния:

private var state:uint;
private const STATE_1:uint=1;
private const STATE_BLABLA:uint=2;


а в апдейте — switch(state) {}
0
Раньше делал также. Неудобно. Со временем получается лапша, в которой не разобраться и внесение изменений превращается в кошмар (добавили выезжающую менюху — перестали работать кнопки в меню)

Здесь же куча классов оказывается не такой уж и большой, а выгода огромна (высокий уровень инкапсуляции поведения, низкая связность, расширяемость).

Данный паттерн помогает именно что разделить состояния и не смешивать код.
+1
Не знаю не знаю, у меня все эвенты в главном классе слушаются на всю игру, и передаются в активный экран. Добавилась в главном меню всплывающая менюшка, а эвенты с неё всё равно в главное меню идут, там уже свич.
Состояния редко использую, наверное поэтому пока-что норм по простому.
+2
Тут дело не только в евентах менюхи. Тот же свитч (у самого такой был — на 1000 строк) — не самое лучшее решение.
Но смысл не в этом. Пользоваться паттернами стоит только тогда, когда это комфортно. Паттерны ради паттернов применять нельзя. Так что правильно и так и так.
Но вот как только сталкиваешься с менюхой более сложной, чем выбор уровня — сразу начинаются проблемы со свитчами и прочими лапшами. Но это только мой опыт. Каждый решает сам :-)
0
С меню то все ясно — там кода обычно немного. Но данный подход себя очень оправдывает даже во внутриигровой логике. Например так можно реализовать механизм AI (задачи юнитов), состояния мыши (т.е. при перетаскивании чего-либо — одно состояние, в дефолтном состоянии — другое, при открывшемся попапе — третье и т.д.), состояния клавиатуры и прочее-прочее. Основная суть — избавиться от if там где можно, т.к. в сложных ситуациях там образуется полотно трудноподдерживаемого кода.
0
Где там куча классов состояний в игре-то?
+3
0
как по мне так это круто :-)
0
Кому понравилось у него еще есть видео по патернам на примере старкрафта
Зовется PatternCraft:
www.youtube.com/user/johnlindquist/search?query=patterncraft
+1
IntellJ IDEA лучшая ^_^
0
Я вот думаю переходить на что-то дешевле 700 бачей FB. Думаю то ли FDT, то ли IDEA.
В IDEA работаешь? Может что посоветуешь :-)
0
имею в виду под мак конечно (на win вполне FD устраивал)
0
Сначала советую просто попробовать, если есть такая возможность ;)
IDEA мне ближе, потому что это тот же производитель, что и ReSharper для Visual Studio, без которого разработка на оной уже не мыслится. Сравнивать с FD бессмысленно, поскольку IDEA обладает наилучшей в своей отрасли функциональностью по рефакторингу, в то время как FD — никакой. Сам просидел на FD года 2-3, очень её уважаю, но факты есть факты :)
0
Ах да, в IDEA веду текущий проект — просто кайф. При этом проект легко открывается и в FD и в IDEA — полная совместимость. С FDT не работал.
0
Спасибо, буду пробовать :-)
0
После FB в FDT начнешь работать как если бы ничего и не менял.
+2
Можно еще больше упростить работу с состояниями, если в абстракции IGameState определить callback-метод onComplete. При этом можно будет отказаться от метода changeState и констант состояний, поскольку при создании очередного состояния в объект сразу можно передать callback-метод, определяющий какое будет следующее состояние.
public class IGameState 
{
        public var onComplete : Function;
        public function update():void {}
        public function destroy():void {}
}

public class MainMenuState extends IGameState 
{
        private var main:Main;

        public function MainMenuState(main:Main, onComplete : Function) 
        {
                this.main = main;               
        }

        public override function update():void
        {
                if (onComplete != null) onComplete(this); //Где-то в основном цикле происходит переход в состояние игрового процесса
        }
        
        public override function destroy() : void {}
}

public class Main extends Sprite 
{
                private var currentState:IGameState;

                public function Main():void 
                {
                        addEventListener(Event.ADDED_TO_STAGE, init);
                }

                private function init(e:Event = null):void 
                {                       
                        addEventListener(Event.ENTER_FRAME, onEnterFrame);
                        currentState = new MainMenuState(this, onMainMenuStateComplete);
                }

                private function onEnterFrame(event:Event):void
                {
                         if (currentState != null) currentState.update();
                }
                
                private function onMainMenuStateComplete(state:MainMenuState):void
                {       
                        currentState.destroy();
                        currentState = new GameplayState(this, onGameplayStateComplete);
                }
}
0
Изживем свитч из программирования окончательно :-). Спасибо, не знал такого приема.
0
А что если из состояния есть не одна возможность выхода? Например из меню выбора уровня можно вернуться в главное меню, или перейти в саму игру. Как тогда работает onComplete?
0
добавляешь два onComplete :-)
0
1) Если нужно что-то передать из состояния, то для этого в обработчик и передается сам объект состояния (state:MainMenuState).
2) Если у состояния несколько разных развязок, то например в MainMenuState можно было бы объявить еще несколько callback-методов на каждую из развязок.
0
Больше спасибо! Давно хотел поискать что-то о паттернах, и их практическом применении)) Оказывается я столько вылосипедов понаделал =))) Спасибо за книги!)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.