
Хочу поделиться еще одним вариантом реализации стейт машины (конечного автомата) для Unity. Статьи про конечные автоматы в привязке к Unity и/или C# на Хабре уже были, например, вот и вот, но я хочу продемонстрировать несколько иной подход, основанный на использовании компонентов Unity.
Ссылка на unitypackage с кодом из статьи.
Для тех, кто все еще не знает, что такое стейт машина
Для жаждущих определений дам ссылки на Википедию:
Конечный автомат (на русском)
Finite-state machine (на английском)
Сам же попробую описать простым языком на игровом примере.
Стейт машина — это набор состояний, например, состояния персонажа:
из которых в текущий момент времени активно может быть только одно; то есть, согласно означенному списку, персонаж может:
и переход между которыми осуществляется по удовлетворении заранее определенных условий, например:
Для наглядности представим вышеописанный конечный автомат в виде графа, где зеленым обозначено начальное состояние стейт машины, красным — конечное.

Конечный автомат (на русском)
Finite-state machine (на английском)
Сам же попробую описать простым языком на игровом примере.
Стейт машина — это набор состояний, например, состояния персонажа:
- Idle (Отдых)
- Run (Бег)
- Jump (Прыжок)
- Fight (Драка)
- Dead (Мертв)
из которых в текущий момент времени активно может быть только одно; то есть, согласно означенному списку, персонаж может:
- либо отдыхать
- либо бежать
- либо прыгать
- либо драться
- либо быть мертвым
и переход между которыми осуществляется по удовлетворении заранее определенных условий, например:
- Отдых->Бег, если нажата клавиша движения
- Отдых->Прыжок, если нажата клавиша прыжка
- Бег->Драка, если произошло столкновение с противником
- Драка->Мертв, если закончилось здоровье
- ...
Для наглядности представим вышеописанный конечный автомат в виде графа, где зеленым обозначено начальное состояние стейт машины, красным — конечное.

Реализация
Реализация стейт-машины у нас будет состоять из трех классов: StateMachine, State и Transition, расположенных в одноименных файлах. Все три класса унаследованы от MonoBehaviour. Класс StateMachine используется напрямую, а вот от абстрактных State и Transition предлагается наследовать конкретные состояния и переходы. В результате, сама стейт машина, а также все ее состояния и переходы, являются компонентами и должны быть назначены какому-либо объекту в сцене. Ну, а для переключения состояний тогда можно воспользоваться уже имеющимся механизмом включения/выключения компонентов (свойство enabled). Это избавляет нас от необходимости создавать специализированные каллбэки для стейт машины, проверки на «включен/выключен» и тому подобное. Вместо этого используются привычные функции событий Unity: OnEnable, OnDisable, Update, Awake и другие. Правда, здесь имеются две тонкости:
- Стоит быть осторожным с событием Start: изначально состояния и переходы стейт-машины должны быть «выключены», а для выключенного компонента это событие произойдет не при старте сцены, а тогда, когда он будет в первый раз «включен». Таково стандартное поведение Unity.
- При наследовании от State придется переопределять (override) метод FixedUpdate (если он вам нужен, конечно же): он реализован в классе State для того, чтобы в Inspector'е всегда показывалась галочка «включить/выключить» для состояния. При наличии этой галочки можно наблюдать за переключением состояний в реальном времени, самый что ни на есть «визуальный дебаг».
Перейдем, наконец, к коду (с комментариями на русском):
Transition
using UnityEngine; /// Базовый класс для переходов. /// Наследуемые компоненты должны быть выключены (disabled) в Inspector'е. public abstract class Transition : MonoBehaviour { /// Целевое состояние (куда переходим). /// Задается в Inspector'е. [SerializeField] State targetState; /// Проперти для получения целевого состояния. /// Используется в State при необходимости перехода. public State TargetState { get { return targetState; } } /// Когда переход должен произойти, необходимо в /// наследнике установить это проперти в true. /// Проверяется оно в State. public bool NeedTransit { get; protected set; } }
State
using UnityEngine; using System.Collections.Generic; /// Базовый класс для состояний. /// Наследуемые компоненты должны быть выключены (disabled) в Inspector'е. public abstract class State : MonoBehaviour { /// Список исходящих переходов. /// Задается в Inspector'е. [SerializeField, Tooltip("List of transitions from this state.")] List<Transition> transitions = new List<Transition> (); /// Возвращает следующее состояние, если должен быть /// совершен переход, иначе возвращает null. /// Вызывается из StateMachine. public virtual State GetNext() { foreach (var transition in transitions) { if (transition.NeedTransit ) return transition.TargetState; } return null; } /// Выключает состояние и переходы из него. /// Будет вызван OnDisable, если его реализовать в потомке. public virtual void Exit() { if(enabled) { foreach(var transition in transitions) { transition.enabled = false; } enabled = false; } } /// Включает состояние и переходы из него. /// Будет вызван OnEnable, если его реализовать в потомке. public virtual void Enter() { if(!enabled) { enabled = true; foreach(var transition in transitions) { transition.enabled = true; } } } /// Этот метод реализован для того, чтобы в Inspector'е всегда /// отображался чекбокс enabled/disabled для состояний. /// В потомке его придется переопределять при необходимости. protected virtual void FixedUpdate() { } }
StateMachine
using UnityEngine; /// Класс стейт машины. public class StateMachine : MonoBehaviour { /// Начальное состояние. /// Задается в Inspector'е. [SerializeField] State startingState; /// Текущее состояние. State current; /// Доступ к текущему состоянию. public State Current { get { return current; } } /// Инициализация (переход в начальное состояние). void Start() { Reset(); } /// Переводит стейт машину в начальное состояние. public void Reset() { Transit(startingState); } /// На каждом кадре проверяет, не нужно ли совершить /// переход. Если нужно - совершает. void Update () { if(current == null) return; var next = current.GetNext(); if(next != null) Transit(next); } /// Собственно, переход. /// Выходит из текущего состояния, /// делает следующее текущим и /// входит в него. void Transit(State next) { if(current != null) current.Exit(); current = next; if(current != null) current.Enter(); } }
Использование получившейся стейт машины
Создадим небольшой тестовый проект, в котором будем двигать куб вправо-влево по экрану. Проект можно создать как в 2D, так и в 3D, отличия должны быть только визуальные. Создадим сцену или воспользуемся дефолтной. В ней уже будет камера, а теперь добавим еще и куб при помощи меню GameObject->Create Other->Cube. Кубу нужно задать позицию по оси X равную -4, так как далее он будет двигаться на 8 юнитов в каждую сторону. Кроме куба создадим дочерний ему пустой объект для нашей стейт машины. Для этого выделим куб в Hierarchy и используем меню GameObject->Create Empty Child. Нагляднее будет переименовать его в StateMachine.
Получится
что-то такое

Следующим шагом созданим скрипты. Нам понадобится 4 скрипта, это класс перехода по таймеру:
TimerTransition
и еще 3 класса для состояний. Базовый класс для состояний движения, передвигающий объект при помощи метода Translate компонента Transform :using UnityEngine; using System.Collections; /// Переход по таймеру. public class TimerTransition : Transition { /// Время в секундах. Задается в Inspector'е. [SerializeField, Tooltip("Time in seconds.")] float time; /// Событие "включения". /// Запускает таймер и обнуляет свойство NeedTransit. void OnEnable() { NeedTransit = false; StartCoroutine("Timer"); } /// Таймер, реализованный при помощи корутины. /// По истечении времени устанавливает свойство NeedTransit в true. IEnumerator Timer() { yield return new WaitForSeconds(time); NeedTransit = true; } /// Событие "выключения". /// Останавливает таймер. void OnDisable() { StopCoroutine("Timer"); } }
TranslateState
и унаследованные от него классы конкретных состояний:using UnityEngine; /// Этот класс двигает заданный Transform при помощи метода Translate. public class TranslateState : State { /// Transform, задается в Inspector'е. [SerializeField] Transform transformToMove; /// Скорость в юнитах в секунду. Задается в Inspector'е. [SerializeField, Tooltip("Speed in units per second.")] Vector3 speed; /// Двигаем заданный Transform. void Update () { var step = speed * Time.deltaTime; transformToMove.Translate(step.x, step.y, step.z); } }
MoveRight
/// Состояние движения вправо. /// Этот класс нужен для того, чтобы /// состояние имело уникальное "имя". public class MoveRight : TranslateState { }
MoveLeft
/// Состояние движения влево. /// Этот класс нужен для того, чтобы /// состояние имело уникальное "имя". public class MoveLeft : TranslateState { }
Теперь, когда все необходимые классы готовы, нужно собрать стейт машину из компонентов. Для этого выделим в Hierarchy наш объект с именем StateMachine и навесим на него все компоненты как на картинке:
Картинка
Не забудем «выключить» компоненты состояний и переходов, но не саму стейт машину.
Заполним наши компоненты следующим образом:
Готовая стейт-машина
Поля, предназначенные для состояний и переходов можно заполнить перетаскиванием соответствующих компонентов. Не забудьте задать StartingState стейт машине и добавить переходы в списки Transitions состояний!
Теперь можно запускать сцену. Если все сделано верно, куб будет двигаться по экрану вправо-влево. Если выделить объект StateMachine в Hierarchy, то в инспекторе можно будет следить за сменой состояний в реальном времени.
Заключение
В заключение хочу заметить, что, хотя данная реализация стейт машины и не лишена недостатков, она вполне подходит для использования в небольших проектах. Для проектов покрупнее, на мой взгляд, само перетаскивание компонентов в инспекторе может оказаться довольно неприятной работой.
Конструктивная критика приветствуется.
