Готовый перевод Game Programming Patterns / Паттерны программирования в играх: 1.6. State

Время исповеди: я слегка перестарался и вложил в эту главу слишком многое.

Якобы, это все о паттерне State design (Состояние).

Но я не могу говорить о нем и об играх не затрагивая фундаментального понятия конечных автоматов (или FSM). Но как только я начал погружаться в эту тему, я понял, что мог бы так же сделать неплохое введение в тему иерархических конечных автоматов и автоматов с магазинной памятью.

Это большая тема, и для краткости в примерах кода были опущены некоторые детали, которые вы должны будете заполнить по своему усмотрению.

Надеюсь они все же останутся достаточно понятными, чтобы видеть общую картину.

Если вы никогда не слышали о конечных автоматах - не грустите.

Они хорошо известы хакерам искуственного интеллекта и компиляторов, в кругах программистов других областей о них знают меньше.

Поскольку я считаю, что они должны быть более известны, я собираюсь применять их для решения разных задач.

Это перекликается с ранним периодом развития искуственного интеллекта.

В 50-е и 60-е большая часть исследований искуственного интеллекта была сосредоточена на обработке языка.

Многие используемые компиляторами техники распознавания языков программирования изобретены на основе распознавания человеческих.

Нам всем это знакомо

Мы работали над маленьким двухмерным платформером.

Наша задача состояла в реализации героини, которая была аватаром игрока в игровом мире.

Это означает, что она должна была реагировать на команды пользователя.

Нажимаем на кнопку В и она должна прыгнуть.

Довольно просто:

void Heroine::handleInput(Input input)

{

if (input == PRESS_B)

}

yVelocity_ = JUMP_VELOCITY;

setGraphics(IMAGE_JUMP)

;

}

}

Нашли ошибку?

Ничто не мешает совершать "воздушные прыжки": долбите "B", пока она в воздухе и она будет парить вечно.

Простой способ исправить это - добавляем булево поле isJumping_ к Heroine, которое отслеживает прыгнула ли наша героиня и делаем следующее:

void Heroine::handleInput(Input input)

{

if (input == PRESS_B)

{

if (!isJumping_)

{

isJumping_ = true;

// Jump...

}

}

}

Здесь также должен быть код, который устанавливает isJumping_ назад в false, когда героиня касается земли.

Я опустил его для краткости.

Затем, мы хотим чтобы героиня пригибалась если игрок нажимает вниз, а она при этом стоит на земле, и вставала когда игрок отпускает кнопку.

void Heroine::handleInput(Input input)

{

if (input == PRESS_B)

{

// Jump if not jumping...

}

else if (input == PRESS_DOWN)

{

if (!isJumping_)

{

setGraphics(IMAGE_DUCK)

;

}

}

else if (input == RELEASE_DOWN)

{

setGraphics(IMAGE_STAND)

;

}

}

А сейчас заметили ошибку?

С таким кодом игрок может:

зажав кнопку "вниз", чтобы пригнуться,

нажать "B" для прыжка из этого положения,

отпустить "вниз" пока он еще в воздухе.

Героиня переключится в стоячее положение в середине прыжка.

Время следующего флага...

void Heroine::handleInput(Input input)

{

if (input == PRESS_B)

{

if (!isJumping_ && !isDucking_)

{

// Jump...

}

}

else if (input == PRESS_DOWN)

{

if (!isJumping_)

{

isDucking_ = true;

setGraphics(IMAGE_DUCK)

;

}

}

else if (input == RELEASE_DOWN)

{

if (isDucking_)

{

isDucking_ = false;

setGraphics(IMAGE_STAND)

;

}

}

}

Далее, было бы здорово, если бы героиня атаковала сверху, если игрок нажимает кнопку вниз в середине прыжка:

void Heroine::handleInput(Input input)

{

if (input == PRESS_B)

{

if (!isJumping_ && !isDucking_)

{

// Jump...

}

}

else if (input == PRESS_DOWN)

{

if (!isJumping_)

{

isDucking_ = true;

setGraphics(IMAGE_DUCK)

;

}

else

{

isJumping_ = false;

setGraphics(IMAGE_DIVE)

;

}

}

else if (input == RELEASE_DOWN)

{

if (isDucking_)

{

// Stand...

}

}

}

Снова время искать баги.

Нашли?

Мы проверили, что вы не можете сделать прыжок в воздухе пока вы прыгаете, но вы можете прыгнуть когда вы пригнулись.

Еще одно поле...

С нашим подходом явно что-то не так.

Каждый раз, когда мы беремся за этот кусок кода, мы что-то ломаем.

Нам необходимо добавить еще кучу движений (мы даже еще не добавили ходьбу), но с такой скоростью все рухнет с кучей ошибок прежде, чем мы закончим.

Кажется, что те программисты, которых вы боготворите, кто всегда создает безупречный код, совершают это просто нечеловеческими усилиями.

На самом деле они интуитивно чувствуют какой тип кода потенциально ошибочный и просто избегают его.

Сложные ветвления и изменяющиеся состояния (поля, которые меняются с течением времени) как раз и являются таким потенциально ошибочным кодом.

Конечные автоматы спешат на помощь

В порыве разочарования вы сметаете со стола все, кроме бумаги и ручки и начинаете рисовать схему.

Вы рисуете квадрат для каждого действия, которое ваша героиня может сделать: стоит, прыжок, приседание и ныряние.

В случаях, когда она может реагировать на нажатие кнопки, вы рисуете стрелку перехода от одного состояния к другому, в которое она перейдет, и подписываете её названием кнопки.

Мои поздравления, вы только что создали конечный автомат.

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

Конечный автомат (КА) является самым простым из этого семейства.

Суть в том,

что автомат имеет конечное количество состояний.

В нашем случае это: стоит, прыгает, приседает и ныряет.

Автомат может находиться только в одном из состояний.

Наша героиня не может одновременно стоять и прыгать.

Это одна из причин, по которой мы будем использовать конечные автоматы.

Последовательность вводов или событий посылается автомату.

В нашем случае это простые нажатия и отпускание кнопок.

Каждое состояние имеет набор переходов, каждый из которых связан с соответствующим вводом и указывает на другое состояние.

Если при подаче ввода находится соответствующий переход из текущего состояния, то автомат переходит в него.

Например, при нажатии кнопки "вниз", если мы находимся в стоячем состоянии, происходит переход в состояние приседания.

Когда игрок нажимает "вниз" пока мы в прыжке, происходит переход в нырок.

Если не найдено ни одного перехода по данному вводу, то ввод игнорируется.

По сути, в этом вся соль автоматов: состояния, вводы и переходы.

Можно представить это как диаграмму.

К сожалению компилятор не поймет наши каракули, так что как же мы реализуем это?

Паттерн "Состояние" Банды Четырех - это один из вариантов того, как мы можем поступить, но можно и проще.

Моя любимая аналогия для КА это старые приключенческие игры типа Zork.

Мир состоит из комнат, каждая из которых соединена с другими проходами.

Вы исследуете мир вводя команды, типа "идти на север".

Выглядит прямо, как конечный автомат: каждая комната - это состояние.

Комната в который вы сейчас находитесь - это текущее состояние.

Входы каждой комнаты - это переходы.

Команды перехода - это вводы.

Enum и Switch

Проблема нашей героини в том, что некоторые комбинации флагов не должны существовать. Например: isJumping_ и isDucking_ не должны быть true одновременно.

Если у вас много флагом и только один может быть включен в каждый момент времени, то это явная подсказка, что вам нужен enum.

В нашем случае, перечисление идеально подойдет для представления состояний нашего КА и мы определим его так:

enum State

{

STATE_STANDING,

STATE_JUMPING,

STATE_DUCKING,

STATE_DIVING

};

Вместо кучи флагов у героини будет только поле state_.

Также, мы поменяли порядок ветвления.

До этого мы сначала делали проверку ввода, а потом состояния.

Это позволяло нашему коду обрабатывать вместе одно нажатие кнопки, но одно состояние было размазано по коду.

Нам необходимо собрать все это вместе, поэтому сначала мы проверим состояние.

Мы получаем:

void Heroine::handleInput(Input input)

{

switch (state_)

{

case STATE_STANDING:

if (input == PRESS_B)

{

state_ = STATE_JUMPING;

yVelocity_ = JUMP_VELOCITY;

setGraphics(IMAGE_JUMP)

;

}

else if (input == PRESS_DOWN)

{

state_ = STATE_DUCKING;

setGraphics(IMAGE_DUCK)

;

}

break;

case STATE_JUMPING:

if (input == PRESS_DOWN)

{

state_ = STATE_DIVING;

setGraphics(IMAGE_DIVE)

;

}

break;

case STATE_DUCKING:

if (input == RELEASE_DOWN)

{

state_ = STATE_STANDING;

setGraphics(IMAGE_STAND)

;

}

break;

}

}

Выглядит банально, но это реальное улучшение предыдущего кода.

У нас все еще есть условные ветвления, но мы уже упростили изменяемое состояние до одного поля.

Весь код для обработки одного состояния красиво собран в одном месте.

Это простейший путь реализации КА и в некоторых случаях это отличное решение.

В частности, наша героиня больше не сможеть быть в недопустимом состоянии.

Когда у нас были флаги, некоторые значения могли существовать, но не имели никакого смысла.

При использовании enum каждое значение верное.

Хотя ваши проблемы могут перерасти это решение.

Скажем, хотим мы добавить движение, при котором наша героиня на время пригибается для восполнения энергии и при отпускании выполняет спец. прием.

Когда она пригнулась мы должны отслеживать время восполнение энергии.

Мы добавляем поле chargeTime_ к Heroine в котором будем хранить длительность восполнения энергии спец. приема.

Предположим, что у нас уже есть метод update(), который вызывается в каждом кадре.

В него мы добавим:

void Heroine::update()

{

if (state_ == STATE_DUCKING)

{

chargeTime_++;

if (chargeTime_ > MAX_CHARGE)

{

superBomb();

}

}

}

Если вы догадались, что это паттерн Update Method - вы выиграли приз!

Нам необходимо сбросить таймер, когда наша героиня начинает пригибаться. Для этого мы немного изменим метод handleInput():

void Heroine::handleInput(Input input)

{

switch (state_)

{

case STATE_STANDING:

if (input == PRESS_DOWN)

{

state_ = STATE_DUCKING;

chargeTime_ = 0;

setGraphics(IMAGE_DUCK)

;

}

// обрабатываем другие нажатия ...

break;

// Другие состояния...

}

}

В конце концов, для добавления спец. приема нам необходимо внести изменения в два метода и добавить поле chargeTime_ к Heroine, не смотря на то, что оно имеет смысл только в состоянии приседания.

Для нас было бы предпочтительней иметь весь этот код и данные аккуратно лежащими в одном месте

Нам поможет "Банда Четырех".

Паттерн "Состояние"

Для тех, кто мыслит объектно-ориентированно, каждое условное ветвление - это возможность использовать динамическую диспетчеризацию (другими словами вызов виртуального метода в С++). Я думаю вы можете забраться достаточно далеко в эту кроличью нору.

Иногда, если это все, что вам нужно.

Есть историческое обоснование всего этого.

Многие из апостолов объектно-ориентированного подхода, такие как "Банда Четырех" с их паттернами проектирования, и Мартин Фаулер, автор "Рефакторинга", пришли из языка Smalltalk.

В нем ifThen - это просто метод вызываемый в условии, который по разному реализован для истинных и ложных объектов.

Но в нашем примере, мы достигли переломной точки, когда больше подходит объектно-ориентированное решение.

Это подводит нас к паттерну "Состояние".

Говоря словами "Банды четырех":

Разрешить объекту изменять свое поведение при изменении его внутреннего состояния.

По всей видимости объект будет менять свой класс.

Но это ни сколько не проясняет ситуацию.

Черт возьми, switch делал тоже самое.

Реализация описываемого паттерна, применительно к нашей героине будет выглядеть так:

Интерфейс state

Во-первых, мы определим интерфейс для состояния.

Каждая часть поведения зависит от состояния. Каждое место, где мы имели до этого switch станет виртуальным методом в интерфейсе.

В нашем случае, это handleInput() и update():

class HeroineState

{

public:

virtual ~HeroineState() {}

virtual void handleInput(Heroine& heroine, Input input)

{}

virtual void update(Heroine& heroine)

{}

};

Классы состояний

Для каждого состояния мы определим класс, реализующий интерфейс.

Их методы определят поведение героини, когда она будет в этом состоянии.

Другими словами, мы возьмем каждый case из нашего кода и переместим его в соответствующий этому состоянию класс.

К примеру:

class DuckingState : public HeroineState

{

public:

DuckingState()

: chargeTime_(0)

{}

virtual void handleInput(Heroine& heroine, Input input)

{

if (input == RELEASE_DOWN)

{

// Изменяем на состояние "стоит"

heroine.

setGraphics(IMAGE_STAND)

;

}

}

virtual void update(Heroine& heroine)

{

chargeTime_++;

if (chargeTime_ > MAX_CHARGE)

{

heroine.

superBomb();

}

}

private:

int chargeTime_;

};

Заметьте, что мы также переместили chargeTime_ из Heroine в класс DuckingState.

Это прекрасно: эта часть данных имеет смысл только в своем состоянии и теперь наша объектная модель это явно отражает.

Делегирование состояний

Следующее, мы дадим Heroine указатель на текущее состояние нашей героини, избавимся от этого здорового switch'a и делегируем состояния:

class Heroine

{

public:

virtual void handleInput(Input input)

{

state_->handleInput(*this, input)

;

}

virtual void update()

{

state_->update(*this)

;

}

// Другие методы...

private:

HeroineState* state_;

};

Для того, чтобы сменить состояние нам просто необходимо присвоить указатель на state_ другому объекту HeroineState.

Это паттерн "Состояние" во всей красе.

Он выглядит как паттерны Strategy и Type Object.

Во всех трех мы имеем главный объект, который делегирует управление.

Различия в целях.

В паттерне Strategy цель - отделить главный класс от некоторой части его поведения.

В паттерне Type Object цель - неделить некоторое количество объектов схожим поведением путем предоставления доступа к ссылке на тот же тип объекта.

В паттерне State цель - изменить поведение главного объекта путем изменения объекта, которому он делегирует.

Где объекты состояний?

Здесь я немного все приукрасил.

Для изменения состояний нам необходимо присваивать указатель на state_ новому объекту состояния, но откуда ему взяться?

В нашей enum-реализации это было не просто: значения enum - обычные примитивы, такие как числа.

Но теперь наши состояния являются классами, что означает, что нам нужны фактические экземпляры.

Существует два общих ответа:

Статичные состояния

Если объект состояния не имеет других полей, то данные, хранящиеся в нем - это указатель на таблицу внутренних виртуальных методов, которые могут быть вызваны.

В этом случае нет причин иметь более одного экземпляра: экземпляры в любом случае будут одинаковыми.

Если ваше состояние не имеет полей и только один виртуальный метод - вы можете упростить этот паттерн еще больше.

Замените каждый класс состояния функцией состояния - просто самой обычной функцией верхнего уровня.

Затем, поле state_ в главном классе станет простым указателем на функцию.

В этом случае вы можете просто создать статичный экземпляр.

Даже если у вас есть куча конечных автоматов, одновременно переходящих в одно состояние, они все смогут работать с одним экземпляром, поскольку ничего специфичного, с точки зрения компьютера, не происходит.

Это паттерн Flyweight

Где будет находиться этот статичный экземпляр - ваше дело.

Найдите место, подходщее по смыслу.

Ну например давайте разместим его в базовом классе состояния:

class HeroineState

{

public:

static StandingState standing;

static DuckingState ducking;

static JumpingState jumping;

static DivingState diving;

// Other code...

};

Каждое из этих статичных полей является экземпляром используемого игрой состояния.

Чтобы заставить героиню прыгнуть, состояние standing будет вести себя примерно так:

if (input == PRESS_B)

{

heroine.

state_ = &HeroineState::jumping;

heroine.

setGraphics(IMAGE_JUMP)

;

}

Создание экземпляров состояний

Тем не менее иногда это не взлетает.

Статичное состояние не сработает для состояния ducking.

Оно содержит поле chargeTime_ и эта особенность возникает только в момент приседания.

Это может случайно сработать в нашей игре, если у нас только одна героиня, но если мы попытаемся добавить игру вдвоем, когда на экране одновременно нужно управлять двумя персонажами, у нас будут проблемы.

В этом случае мы на самом деле должны создавать объект состояния, когда мы переходим к нему:

// In StandingState:

if (input == PRESS_DOWN)

{

delete heroine.

state_;

heroine.

state_ = new DuckingState();

// Other code...

}

Такое решение позволяет каждому КА иметь собственный экземпляр состояния.

По возможности я предпочитаю использовать статичные состояния, т.к. они не тратят память и ресурсы процессора на создание объектов при каждом изменении состояния.

Но для состояний, которые, э-э-э, больше являются состоянием, это тот путь по которому следует идти.

Когда вы динамически управляете состояниями, вы можете не беспокоиться о фрагментации и использовании памяти.

Паттерны Flyweight и Object Pool помогут вам с этим.

Входные и выходные действия.

Цель паттерна State - инкапсулировать все, что относится к поведению и данным одного состояния в отдельный класс.

Мы неплохо справились с этой задачей, но есть еще один нюанс.

Мы переместили обработку пользовательского ввода и обновление времени восполнения энергии в DuckingState, но у нас есть еще небольшой кусочек кода, специфичный для этого состояния, который находится вне этого класса.

Когда мы присаживаемся из положения стоя происходит следующая инициализация:

// In standing state:

if (input == PRESS_DOWN)

{

// Change state...

chargeTime_ = 0;

setGraphics(IMAGE_DUCK)

;

}

По идее, состояние ducking должно позаботиться о сбросе времени заряда (в конце концов именно этот объект содержит поле chargeTime_ )

и воспроизведении анимации.

Мы можем решить эту задачу передавая состоянию входное действие:

class DuckingState : public HeroineState

{

public:

virtual void enter(Heroine& heroine)

{

chargeTime_ = 0;

heroine.

setGraphics(IMAGE_DUCK)

;

}

// Other code...

};

Вернемся к классу Heroine и обернем изменение состояния в небольшую удобную функцию, которая вызывается при каждом новом состоянии:

void Heroine::changeState(HeroineState* state)

{

delete state_;

state_ = state;

state_->enter(*this)

;

}

В состоянии стоя, мы используем этот код следующим образом:

if (input == PRESS_DOWN)

{

heroine.

changeState(new DuckingState());

}

Теперь состояние ducking действительно инкапсулировано.

У входных действий есть одна замечательная особенность: они запускаются при входе в состояние независимо от того из какого состояния вы пришли.

Большинство реальных графов состояний имеют множество переходов в том же состоянии.

К примеру, возможно наша героиня может вести стрельбу из оружия и в состоянии стоя и сидя и в прыжке.

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

Входные действия позволяют вам держать этот код в одном месте.

Мы можем расширить этот подход используя выходные действия.

Это так же просто метод, который мы вызываем в момент выхода из состояния перед тем, как перейдем в новое состояние.

В чем подвох?

Я потратил достаточно времени объясняя вам конечные автоматы, а теперь я собираюсь выдернуть из под вас ковер.

Все, что я говорил до сих пор было верно и отлично подходило для решения определенных задач.

Но наряду со всем великолепием есть один страшный изъян.

Конечные автоматы помогают вам разобраться со спагетти-кодом путем введения в него ограниченных структур.

Все, что у вас есть - это фиксированный набор состояний, текущее состояние и некоторые жестко прописанные переходы.

Конечные автоматы даже не являются полными по Тьюрингу.

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

Машина Тьюринга - одна из наиболее выразительных моделей.

"Полнота по Тьюрингу" означает, что система (обычно язык программирования)

достаточно выразительна для реализации машины Тьюринга, что означает, что все языки программирования полные по Тьюрингу, в некотором смысле, одинаково выразительны.

Конечные автоматы не обладают такой выразительностью.

Если вы попытаетесь использовать конечные автоматы в чем либо более сложном, к примеру в игровом искуственном интеллекте, вы столкнетесь с ограничениями этой модели.

К счастью, наши предшественники нашли пути обхода некоторых из этих препятствий.

Заканчивая главу, познакомлю вас с некоторыми из этих путей.

Параллельные конечные автоматы

Мы решили дать нашей героине возможность носить оружие.

Эта возможность никак не влияет на то, что наша героиня могла делать раньше: бегать, прыгать, приседать и т.д.

Но так же она должна иметь возможность стрелять из оружия в любом из этих состояний.

Если мы хотим сохранить наше решение в рамках конечного автомата, мы должны будем удвоить количество имеющихся состояний.

Для каждого из имеющихся состояний нам потребуется еще одно в котором она будет делать тоже самое когда она вооружена: состояние стоя, состояние стоя с оружием, прыжок, прыжок с оружием, ну вы поняли идею.

Добавьте еще пару видов оружия и вы получите комбинаторный взрыв количества состояний.

А это не только огромное количество состояний, это еще и огромная избыточность: безоружные и вооруженные состояния практически идентичны, за исключением небольших участков кода обработки стрельбы.

Проблема в том, что мы затолкали в один конечный автомат две части состояния - то, что она делает и то, что она носит.

Чтобы смоделировать все возможные комбинации, нам потребуется состояние для каждой пары.

Решение очевидно: нужно иметь два раздельных конченых автомата.

Если мы хотим втиснуть в один конечный автомат n-ное количество состояний того, что она будет делать и m-ное количество состояний того, что она будет носить, то общее количество состояний будет n*m.

Если же у нас два конечных автомата, общее число состояний n+m.

Мы возьмем наш оригинальный конечный автомат, оставим в нем только ту часть, которая отвечает за то, что героиня делает, и оставим его в покое.

Затем объявим еще один конечный автомат, который будет отвечать за то, что героиня носит.

Класс Heroine теперь будет иметь два указателя "состояния":

class Heroine

{

// Other code...

private:

HeroineState* state_;

HeroineState* equipment_;

};

В целях демонстрации, для снаряжения героини мы будем использовать паттерн State полностью.

На практике же, так как у нас есть просто два состояния, логические флаги тоже вполне подойдут.

Когда героиня делегирует нажатия клавиш состоянию, она передает их для каждого из них:

void Heroine::handleInput(Input input)

{

state_->handleInput(*this, input)

;

equipment_->handleInput(*this, input)

;

}

В более полнофункциональной системе, вероятно, должен быть способ передавать ввод клавиш одному конечному автомату не передавая при этом другому.

Это предотвратит ошибочную попытку ответа обоих конечных автоматов на одно и то же нажатие.

Каждый конечный автомат сможет отвечать на вводы, создавать поведение и изменять свое состояние независимо от другого автомата.

Когда два набора состояний по большей части не связаны, все работает отлично.

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

К примеру, героиня не должна стрелять во время прыжка, или она не может атаковать сверху, если она вооружена.

Для решения этой задачи в коде одного из состояний, вероятно, придется сделать несколько проверок условий через IF, чтобы координировать состояния.

Конечно, не самое элегантное решение, но вполне справляющееся со своей задачей.

Иерархические конечные автоматы.

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

К примеру, героиня может быть в состоянии стоя, идти, бежать, скользить.

В каждом из них нажатие клавиши B приводит к прыжку, а нажатие вниз - приседание.

В простой реализации конечных автоматов иы имеем дублирование кода в каждом из этих состояний.

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

Если бы это был просто объектно-ориентированных код, а не конечный автомат, то одним из способов повторного использования кода могло бы быть наследование.

Мы можем объявить класс для состояния "на земле", который обрабатывает прыжок и приседание.

Положение стоя, ходьба, бег и скольжение будут наследоваться от него и добавлять свое собственное поведение.

Такой подход имеет как положительные, так и отрицательные последствия.

Наследование - мощная штука для повторного использования кода, но также мы получаем довольно сильную связь между частями кода.

Это тяжелый молоток, так что машите им осторожно.

Да, кстати, эта обычная структура называется иерерхическим конечным автоматом.

Состояние может иметь суперсостояние (оно само может создавать подсостояния).

Если при возникновении события оно не обрабатывается подсостоянием, оно всплывает вверх по цепочке суперсостояний.

Другими словами, работает как переопределение унаследованных методов.

Фактически, если мы используем паттерн State для реализации наших конечных автоматов, мы можем использовать наследование класса для реализации иерархии.

Определим базовый класс для суперсостояния:

class OnGroundState : public HeroineState

{

public:

virtual void handleInput(Heroine& heroine, Input input)

{

if (input == PRESS_B)

{

// Jump...

}

else if (input == PRESS_DOWN)

{

// Duck...

}

}

};

И каждое подсостояние наследует его:

class DuckingState : public OnGroundState

{

public:

virtual void handleInput(Heroine& heroine, Input input)

{

if (input == RELEASE_DOWN)

{

// Stand up...

}

else

{

// Didn't handle input, so walk up hierarchy.

OnGroundState::handleInput(heroine, input)

;

}

}

};

Конечно же это не единственный способ реализации иерархии.

Если вы не используете паттерн State Банды Четырех, то это не будет работать.

Вместо этого вы можете смоделировать цепочку суперсостояний текущего состояния явно используя стэк состояний, а не одно состояние в основном классе.

Текущее состояние находится вначале стэка, непосредственно под ним находятся его суперсостояния, а затем суперсостояния состояния и так далее.

Когда вы готовите некоторое поведение, специфичное для состояния, вы начинаете с вершины стека и спускаетесь вниз, пока одно из состояний не обработает его.

(Если никто не обработал его, вы просто игнорируете это)

Автоматы с магазинной памятью.

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

Строго говоря, стэк - это вообще нечто совершенно иное и предназначено для решения других задач.

Проблема заключается в том, что конечные автоматы не такого понятия, как история.

Вы знаете в каком состоянии вы сейчас, но не помните в каких состояниях вы уже были.

Нет простого пути вернуться в предыдущее состояние.

Пример: ранее мы позволили нашей героине вооружить себя до зубов.

Когда она стреляет из оружия, нам нужно новое состояние, которое воспроизводит анимацию стрельбы и пуль, а так же некоторые визуальные эффекты.

Поэтому мы на скорую руку сделали FiringState и сделали все состояния так, что героиня может стрелять при переходе между состояниями, когда нажата кнопка стрельбы.

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

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

Она может прекратить стрельбу когда она стоит, бежит, прыгает и приседает.

Когда стрельба окончена, она должна перейти обратно в то состояние, в котором была до этого.

Если мы придерживаемся жестких рамок конечных автоматов, то предыдущее состояние уже потеряно.

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

Что бы мы действительно хотели, так это просто способ сохранить состояние, в котором была героиня перед выстрелом, а затем вспомнить его позже.

И снова нам поможет теория конечных автоматов.

Подходящая нам структура данных называется автоматом с магазинной памятью.

Если конечный автомат имеет только один указатель на состояние, то автомат с магазинной памятью содержит стэк этих состояний.

Конечный автомат, при переходе в новое состояние, заменяет предыдущее.

Автомат с магазинной памятью так же позволяет это делать, но так же предоставляет вам две дополнительные операции:

Вы можете поместить новое состояние в стэк (push).

Текущее состояние всегда будет вначале стэка, так что это будет переход к новому состоянию.

Но он оставляет предыдущее состояние под текущим, а не отбрасывает его.

Вы можете выкинуть самое верхнее состояние из стека (pop).

В этом случае это состояние будет отброшено и состояние, бывшее под ним, станет новым текущим состоянием.

Это именно то, что нам нужно для реализации стрельбы.

Мы создаем всего одно состояние стрельбы.

Когда кнопка стрельбы нажата в любом другом состоянии, мы помещаем состояние стрельбы в стэк.

Когда анимация стрельбы прекращается, мы выталкиваем это состояние из стека и наш автомат переводит нас в то состояние, в котором мы были до этого.

Итак, насколько они полезны?

Даже со всеми этими расширениями конечные автоматы имеют довольно сильные ограничения.

Тенденции в современных играх с искуственнм интеллектом, это использование таких захватывающих вещей, как поведенческие деревья и системы планирования.

Если вы интересуетесь искуственным интеллектом. то эта статья лишь подогреет ваш интерес.

Но чтобы удовлетворить его вам нужно читать другие книги.

Это не означает, что конечные автоматы, автоматы с магазинной памятью и другие простые системы совсем бесполезны.

Это отличные инструменты для решения определенного рода задач.

Конечные автоматы будут полезны в следующих случаях:

У вас есть объект, чье поведение изменяется на основе внешнего состояния.

Состояние может быть жестко разделено на относительно небольшое количество различных опций.

Объект реагирует на последовательность нажатий или событий во времени.

В играх они наиболее известны тем, что используются в искуственном интеллекте, но так же они распространены в обработке пользовательского ввода, навигации по экранам меню, обработке текста, сетевых протоколах и другом асинхронном поведении.

Переведено на Нотабеноиде
http://notabenoid.org/book/51192/215215

Переводчики: bubenok, MrSmith33, Deo, kamillxiii

Настройки

Готово:

100.00% КП = 1.0