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


Хочу поділитися ще одним варіантом реалізації стейт машини (кінцевого автомата) для 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 from transitions 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;

/// Поточний стан.
Current State;

/// Доступ до поточного стану.
public Current State 
{
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, то в інспектора можна буде стежити за зміною станів в реальному часі.

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

Конструктивна критика вітається.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.