前言
最近在学习Unity游戏设计模式,看到两本比较适合入门的书,一本是unity官方的 《Level up your programming with game programming patterns》 ,另一本是 《游戏编程模式》
这两本书介绍了大部分会使用到的设计模式,因此很值得学习
本专栏暂时先记录一些教程部分,深入阅读后续再更新
文章目录
有限状态机
在游戏中有一个可玩的角色,通常情况他是站在地面上的,当控制控制器,他就会进入行走状态,当按下跳跃,则他会跳到半空,下落后又会回到站立状态
如果你做过动画机Animator,你一定能理解这个状态切换是如何实现的。上述的状态图类似于流程图,但是有一些区别:
- 它由多种状态构成,且每个时间点只有一个状态是处于活动的
- 每个状态可以根据运行条件转换为其他状态
- 当发生状态转换时,原来的状态由活动切换为不活动,而转换的目标状态变为活动
我们将上图这样的状态模型称为有限状态机FSM,有限状态机在角色AI,程序设计尤其操作系统中十分常见。
有限状态机由数量有限的状态构成,它有一个初始状态,并包含了其他状态以及转换状态的条件。状态机在任意时刻都处于其中的某一状态,并且在一定条件下会从一种状态切换为另一种状态,以响应转换的外部输入。
状态模式不仅可以用于管理角色,道具的状态,甚至可以用于管理整个游戏系统的状态。包括Mono Behavior的生命周期,实际上也可视作一种状态模式
如何实现状态模式
状态模式看起来似乎很简单,我们只需要让对象进行状态判断,根据状态来选择行为就行了。
那我是不是可以定义一个枚举类型来分出状态,然后让角色根据他们所处的状态在内部进行行为切换就行了呢?
csharp
public enum EnemyState
{
Idle,
Walk,
Jump
}
public class Enemy : MonoBehaviour
{
private EnemyState state;
private void Update()
{
GetInput();
switch (state)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Walk:
Walk();
break;
case EnemyState.Jump:
Jump();
break;
}
}
}
看起来实现了状态模式,但显然这种实现是依托答辩。
首先,难道我们每定义一个角色,就需要在其内部管理它自身的状态?
齐次,如果我们每添加一个状态,就需要一个Switch Case,那代码会有多冗余?
最后,上述代码显然是高耦合的,如果我们需要添加或者删去某状态,那么所有使用了该状态的代码都需要被修改。
因此,用枚举类型实现状态显然不合适,记住设计模式的重要原则,对拓展开放,对修改关闭
因此同理,让所有角色继承一个状态基类,在基类中定义各种状态实现的方法,并在子类中重写状态实现的虚方法也是不行的,因为基类一旦改变子类也要改变。
所以,我们需要在不修改角色代码的情况下,既要实现状态的拓展和删除,又要方便我们对每个状态的角色事件进行定义。一个想法就是让状态持有角色并在状态中完成业务处理逻辑,而非角色根据状态来实现业务逻辑。
这个想法很像我之前学习的一个案例(也许是工厂模式),银行有很多业务,但是如果每增加一个业务就需要修改银行类的代码,显然违背了开闭原则,因此银行应当只负责返回给用户相应的业务,而具体的业务逻辑则需要业务类本身来执行。就方便对银行业务进行增减。
因此角色的状态事件则需要由状态类本身来进行定义,好处是减少了耦合,代码也会更加清晰。但坏处是我们可能要为每个角色类定义多个衍生出来的状态类,类的数量会爆炸式的增长(此时用命名空间和程序集来管理多个相关类的好处就凸显出来了)
csharp
// 定义抽象状态类
public abstract class CharacterState
{
protected Character character;
public void SetCharacter(Character _character)
{
this.character = _character;
}
// 抽象方法,子类需要实现具体行为
public abstract void Update();
}
// 具体状态类:IdleState
public class IdleState : CharacterState
{
public override void Update()
{
Debug.Log("角色处于闲置状态");
// 检查是否应该转换到其他状态,如按下移动键则切换至MoveState
if (Input.GetKey(KeyCode.W))
{
character.ChangeState(new MoveState());
}
}
}
// 具体状态类:MoveState
public class MoveState : CharacterState
{
public override void Update()
{
Debug.Log("角色正在移动");
// 检查是否应返回闲置状态或切换至其他状态
if (!Input.GetKey(KeyCode.W))
{
character.ChangeState(new IdleState());
}
}
}
------------------------------------------------------
// 角色类持有当前状态并处理状态切换
public class Character : MonoBehaviour
{
private CharacterState currentState;
public void ChangeState(CharacterState newState)
{
if (currentState != null)
{
currentState.SetCharacter(null);
}
currentState = newState;
currentState.SetCharacter(this);
}
void Update()
{
currentState.Update();
}
}
在上述例子中,我们把状态的业务逻辑本身定义到了状态类中,并将对应的持有角色传入状态类,那么当角色进行状态改变时,则行为逻辑也就切换为对应状态类提供的Update方法。由状态类中对角色逻辑进行处理。
为了进一步解除角色类和状态类的耦合(角色未必需要有状态切换的需求),可以创建一个抽象的上下文类(Context),由它来持有当前状态并处理状态之间的切换:
管理StateSystem的文件
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace StatePattern.StateSystem
{
/// <summary>
/// 状态抽象基类
/// </summary>
public abstract class State
{
public abstract void Handle();
}
/// <summary>
/// 状态生命周期抽象基类
/// </summary>
public abstract class StateBehaviour:State
{
// 状态持有者
protected ContextBehaviour Context;
// 几个用于状态生命周期调度的抽象方法
public abstract void Update();
public abstract void Enter();
public abstract void Exit();
}
/// <summary>
/// 管理状态的上下文基类
/// </summary>
public class Context
{
// 当前状态
private State _state;
public void SetState<T>(T state) where T:State
{
_state = state;
}
public State GetState()
{
return _state;
}
public void Requst()
{
_state?.Handle();
}
}
/// <summary>
/// 上下文管理状态生命周期基类
/// </summary>
public class ContextBehaviour : Context
{
// 当前持有状态
private StateBehaviour _stateBehaviour;
// 覆盖父类的获取状态方法
public new void SetState<T>(T state) where T:StateBehaviour
{
_stateBehaviour = state;
}
public new StateBehaviour GetState()
{
return _stateBehaviour;
}
// 几个用于状态生命周期调度的虚方法
public virtual void ChangeState(StateBehaviour stateBehaviour)
{
_stateBehaviour.Exit();
SetState(stateBehaviour);
_stateBehaviour.Enter();
}
public virtual void Update()
{
_stateBehaviour.Update();
}
public virtual void NotifyStateEnter()
{
_stateBehaviour.Enter();
}
public virtual void NotifyStateExit()
{
_stateBehaviour.Exit();
}
}
}
角色基类定义代码:
csharp
using StatePattern.StateSystem;
using System;
using UnityEngine;
using UnityEngine.UI;
namespace CharacterClass
{
#region 基类定义
/// <summary>
/// 角色状态基类
/// </summary>
public abstract class CharacterState : StateBehaviour { }
/// <summary>
/// 角色状态上下文基类
/// </summary>
public class CharacterContext : ContextBehaviour { }
/// <summary>
/// 角色基类
/// </summary>
public class Character : MonoBehaviour
{
private CharacterContext _context;
public CharacterContext Context => _context;
public Button StateChangeBtn;
private void Start()
{
var riginState = new IdleState();
_context = new CharacterContext();
_context.SetState(riginState);
var newState = new MoveState();
StateChangeBtn.onClick.AddListener(() => { ChangeState(newState); });
}
private void Update()
{
_context.Update();
}
public void ChangeState(CharacterState characterState)
{
_context.ChangeState(characterState);
}
}
#endregion
/// <summary>
/// 角色状态类IdleState
/// </summary>
public class IdleState : CharacterState
{
public override void Update()
{
Debug.Log("处于IdleState");
}
public override void Enter()
{
Debug.Log("进入IdleState");
}
public override void Exit()
{
Debug.Log("退出IdleState");
}
public override void Handle()
{
Debug.Log("IdleState下执行事件");
}
}
/// <summary>
/// 角色状态类MoveState
/// </summary>
public class MoveState : CharacterState
{
public override void Update()
{
Debug.Log("处于MoveState");
}
public override void Enter()
{
Debug.Log("进入MoveState");
}
public override void Exit()
{
Debug.Log("退出MoveState");
}
public override void Handle()
{
Debug.Log("MoveState下执行事件");
}
}
}
我们把Character脚本挂载,然后传入Button用于手动切换状态
这样我们就实现状态模式了。上面的代码写的实在太漂亮了,我都忍不住想夸我自己
我们还有更丧心病狂的想法,如果我们需要管理的状态不是单个,而是一系列的状态,那么我们可能就需要维护一个状态队列或者状态栈,此时一个状态切换上下文已经不够我们用了,我们需要一个状态机!
csharp
public class NullState : StateBehaviour
{
public override void Handle()
{
throw new System.NotImplementedException();
}
public override void Update()
{
throw new System.NotImplementedException();
}
public override void Enter()
{
throw new System.NotImplementedException();
}
public override void Exit()
{
throw new System.NotImplementedException();
}
}
public class StateMachine
{
private ContextBehaviour _contextBehaviour;
public ContextBehaviour ContextBehaviour => _contextBehaviour;
private NullState _nullState = new NullState();
private StateBehaviour _prevState= new NullState();
public StateMachine (ContextBehaviour contextBehaviour)
{
_contextBehaviour = contextBehaviour;
}
public StateMachine (ContextBehaviour contextBehaviour,StateBehaviour riginState)
{
_contextBehaviour = contextBehaviour;
_contextBehaviour.SetState(riginState);
}
private Queue<StateBehaviour> _stateQueue = new Queue<StateBehaviour>();
public void StateEnQueue(StateBehaviour stateBehaviour)
{
_stateQueue.Enqueue(stateBehaviour);
}
public StateBehaviour StateDeQueue()
{
if (_stateQueue.Count > 0)
{
return _stateQueue.Dequeue();
}
else
{
return _nullState;
}
}
public void Update()
{
_contextBehaviour.Update();
}
public void NextState()
{
_prevState = _contextBehaviour.GetState();
_contextBehaviour.ChangeState(StateDeQueue());
}
public void PrevState()
{
_contextBehaviour.ChangeState(_prevState);
}
}
这样我们就不是让角色持有上下文,而是让角色持有状态机本身。
在某些需要的时候更新状态机就可以处理一系列状态。我们就可以对状态进行各种操作,例如回到上一个状态,例如在一个事件中根据我们的需要传入一系列状态,并按照我们的想法对状态机中的状态进行触发。甚至多个角色持有同个状态机,一个状态机持有多个状态的上下文等等奇思妙想。