Как стать автором
Обновить

Еще одна простая стейт машина для Unity

Время на прочтение6 мин
Количество просмотров22K


Хочу поделиться еще одним вариантом реализации стейт машины (конечного автомата) для Unity. Статьи про конечные автоматы в привязке к Unity и/или C# на Хабре уже были, например, вот и вот, но я хочу продемонстрировать несколько иной подход, основанный на использовании компонентов Unity.

Ссылка на unitypackage с кодом из статьи.

Для тех, кто все еще не знает, что такое стейт машина
Для жаждущих определений дам ссылки на Википедию:

Конечный автомат (на русском)
Finite-state machine (на английском)

Сам же попробую описать простым языком на игровом примере.

Стейт машина — это набор состояний, например, состояния персонажа:
  • Idle (Отдых)
  • Run (Бег)
  • Jump (Прыжок)
  • Fight (Драка)
  • Dead (Мертв)

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

и переход между которыми осуществляется по удовлетворении заранее определенных условий, например:
  • Отдых->Бег, если нажата клавиша движения
  • Отдых->Прыжок, если нажата клавиша прыжка
  • Бег->Драка, если произошло столкновение с противником
  • Драка->Мертв, если закончилось здоровье
  • ...

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




Реализация


Реализация стейт-машины у нас будет состоять из трех классов: StateMachine, State и Transition, расположенных в одноименных файлах. Все три класса унаследованы от MonoBehaviour. Класс StateMachine используется напрямую, а вот от абстрактных State и Transition предлагается наследовать конкретные состояния и переходы. В результате, сама стейт машина, а также все ее состояния и переходы, являются компонентами и должны быть назначены какому-либо объекту в сцене. Ну, а для переключения состояний тогда можно воспользоваться уже имеющимся механизмом включения/выключения компонентов (свойство enabled). Это избавляет нас от необходимости создавать специализированные каллбэки для стейт машины, проверки на «включен/выключен» и тому подобное. Вместо этого используются привычные функции событий Unity: OnEnable, OnDisable, Update, Awake и другие. Правда, здесь имеются две тонкости:

  1. Стоит быть осторожным с событием Start: изначально состояния и переходы стейт-машины должны быть «выключены», а для выключенного компонента это событие произойдет не при старте сцены, а тогда, когда он будет в первый раз «включен». Таково стандартное поведение Unity.
  2. При наследовании от 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
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");
    }
}
и еще 3 класса для состояний. Базовый класс для состояний движения, передвигающий объект при помощи метода Translate компонента Transform :
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, то в инспекторе можно будет следить за сменой состояний в реальном времени.

Заключение


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

Конструктивная критика приветствуется.
Теги:
Хабы:
+19
Комментарии7

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн