Паттерн State Machine и как он применяется в Восхождении за кофе
Идея
У объекта выделяется несколько состояний, имеющих разную логику. Каждое состояние прописывается отдельно, для каждого указываются условия переходов в другие состояния. Затем объект переключается между ними и применяет соответствующую логику. Так сложная система разделяется на более простые и управление становится проще.
Зачем?
Машина состояний позволяет разбить один большой скрипт на много маленьких.
State Machine + Locomotion
В разделении можно пойти ещё дальше. Если часть состояний отличаются только анимацией и условиями переходов, то есть физика у них общая (но не у всех!), это наверняка приведёт к дублированию кода в состояниях.
Чтобы этого избежать, переключением физики может заниматься отдельный механизм - так называемая локомоция. Логические состояния отделяются от физических. Например, логические состояния: стоит, бежит, прыгает; физические: в воздухе, на земле, в воде.
Логические состояния управляют анимацией и физическим состоянием. Выигрыш в том, что несколько логических состояний могут использовать одно и то же физическое.
Как это реализовано в Восхождении за кофе
Для состояний создан абстрактный класс CatState. Геттер LocomotionMode обеспечивает связь каждого логического состояния с физическим. Открытые методы позволяют связать жизненный цикл состояния с жизненным циклом объекта, а также выполнять действия при переходе между состояниями. Сейчас публичные методы вызывают закрытые - это сделано для единообразия по аналогии с Enter() и OnEnter(). В будущем в открытых методах может появиться более сложная логика.
public abstract class CatState
{
protected abstract LocomotionMode LocomotionMode { get; }
protected readonly Cat cat;
public CatState(Cat cat)
{
this.cat = cat;
}
public void Enter()
{
cat.Locomotion.SetMode(LocomotionMode);
OnEnter();
}
public void Update() => OnUpdate();
public void FixedUpdate() => OnFixedUpdate();
public void Exit() => OnExit();
protected abstract void OnEnter();
protected abstract void OnUpdate();
protected abstract void OnFixedUpdate();
protected abstract void OnExit();
}
Рассмотрим сразу реализацию конкретного состояния. При переходе в состояние Бег включается соответствующая анимация. Каждый кадр выполняются проверки условий для перехода в другие состояния.
public sealed class CatRun : CatState
{
protected override LocomotionMode LocomotionMode { get => LocomotionMode.Grounded; }
public CatRun(Cat cat) : base(cat) { }
protected override void OnEnter() => cat.animator.Play("Run");
protected override void OnExit() { }
protected override void OnUpdate()
{
if (!cat.Locomotion.IsGrounded)
cat.StateMachine.ChangeState(cat.FallState);
else if (cat.Impact == 0)
cat.StateMachine.ChangeState(cat.IdleState);
}
protected override void OnFixedUpdate() { }
}
Теперь взглянем собственно на машину состояний. Она хранит текущее состояние, управляет переключением и жизненным циклом состояний. В каждом методе стоит проверка на факт инициализации. Если инициализации не было, значит текущее состояние пустое, и его обработка приведёт к ошибкам. Поэтому меняем ошибку на более понятную.
using UnityEngine;
public class CatStateMachine
{
public CatState CurrentState { get; private set; }
private bool _isInitialized = false;
public void Initialize(CatState initialState)
{
if (!_isInitialized)
{
if (initialState == null)
{
Debug.LogError("CatStateMachine must not be initialized with a null initial state.");
return;
}
CurrentState = initialState;
CurrentState.Enter();
_isInitialized = true;
}
else Debug.LogWarning("CatStateMachine is already initialized.");
}
public void ChangeState(CatState state)
{
if (!_isInitialized)
{
Debug.LogError("CatStateMachine is not initialized");
return;
}
else
{
CurrentState.Exit();
CurrentState = state;
CurrentState.Enter();
}
}
public void Update()
{
if (_isInitialized) CurrentState.Update();
else Debug.LogError("CatStateMachine is not initialized");
}
public void FixedUpdate()
{
if (_isInitialized) CurrentState.FixedUpdate();
else Debug.LogError("CatStateMachine is not initialized");
}
}
Заметим, что флаги всё равно присутствуют. Но теперь они влияют только на переход между состояниями + находятся в отдельном месте, а не в главном скрипте Cat.cs.
Метод Tick() обрабатывает пользовательский ввод (в данном случае - движение влево или вправо) в соответствии с установленным режимом и меняет параметры RigidBody2D или Transform. Нажатие кнопки прыжка не нужно проверять каждый кадр, поэтому прыжок вынесен в отдельный метод.
public enum LocomotionMode
{
Grounded, Airborne, NoControl
}
public class CatLocomotion
{
private LocomotionMode mode = LocomotionMode.Grounded;
private readonly Rigidbody2D rb;
private readonly BoxCollider2D body;
private readonly Transform transform;
// Могут быть нужны и другие компоненты, но эти - основные
// =====================================
// Флаги
// =====================================
public bool IsGrounded
{
get
{
//...
}
}
// --- Прыжок с буферизацией ---
public bool JumpPerformed
{
get
{
if (jumpPerformed)
{
jumpPerformed = false;
return true;
}
return false;
}
}
public CatLocomotion(
Rigidbody2D rb,
BoxCollider2D body,
Transform transform
)
{
this.rb = rb;
this.body = body;
this.transform = transform;
}
public void SetMode(LocomotionMode mode) => this.mode = mode;
public void Tick(float input, float dt)
{
// Это FixedUpdate() для физики
}
///
/// Попытка прыжка. Проверяет землю, поддерживает буферизацию.
///
public void Jump()
{
//...
}
}
Наконец, посмотрим, как все эти навороты связаны с персонажем. Оставлен только код, относящийся к теме.
В Awake() создаётся машина и все её состояния. В Start() происходит инициализация. В Update() и FixedUpdate() вызываются одноимённые методы CatStateMachine.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(BoxCollider2D))]
public class Cat : MonoBehaviour
{
public Rigidbody2D rb;
private BoxCollider2D boxCollider;
public Animator animator;
// текущий ввод (горизонтальный)
public float Impact { get; private set; } = 0F;
// --- Машина состояний ---
public CatStateMachine StateMachine { get; private set; }
public CatIdle IdleState { get; private set; }
public CatRun RunState { get; private set; }
public CatJump JumpState { get; private set; }
public CatFall FallState { get; private set; }
public CatInteracting InteractingState { get; private set; }
public CatLocomotion Locomotion { get; private set; }
// ============================================================
// Жизненный цикл
// ============================================================
void Awake()
{
boxCollider = GetComponent
Выражаю благодарность Повелителю блинов за совет реализовать этот паттерн.




Комментарии (0)
Войдите, чтобы написать комментарий
Войти