接着上一篇说的随谈过去2019参与的一款Unity项目开发,这篇专门记录下FSM的使用和别人写的一个具体实现。
其实很多普遍功能,网上一搜一般都可以找到具体实现,而且有很多重复的文章。我平时要了解学习某个功能,一般都会看多篇文章,而不是只看一篇,以此了解下不同人的想法。我这里再写一篇,只是让自己重新梳理一下流程,留个记录,也方便自己以后忘了可以直接看自己当时的思路。
Demo里我使用了别人实现的SuperStateMachine和Unity自带的Animator组件(当然,Animator也是一个状态机)来控制物体的行为和状态间的切换。SuperStateMachine的源码不多,我会讲下它的具体实现。由于Animator看不到源码,所以就只是讲下它的使用。不过两个状态机用起来都很方便(写完Demo后,感觉Animator没那么方便…)。
先上Demo的效果图
什么是状态机这里就不多说了,这里直接贴上源码。图中的逻辑也不难,代码并不多,简单的怪物生成逻辑、文本显示逻辑和相机的控制也不说,感兴趣的可以看下原工程,文章末尾放出链接。只关注状态机,先来SuperStateMachine:
/*
* original code 源码地址
* https://github.com/liangdong-xd/SuperMario64HD/blob/master/Assets/SuperCharacterController/Core/SuperStateMachine.cs
*/
using System;
using System.Collections;
using System.Reflection;
using System.Collections.Generic;
using UnityEngine;
/*
* 这个状态机和别的不太一样,一般都是把状态机和状态分成单独的两个类
* 然后不同的状态都继承状态类去具体实现状态中的那三个方法
* 而这个状态机直接在派生的状态机中实现各个状态的三个方法
* 这样的好处是,这个状态机所在的对象的属性是共享给所有状态的
* 而独立成类的状态,每个都需要单独保存一份对象属性的引用,像Animator的使用就是如此
*/
public class SuperStateMachine : MonoBehaviour
{
// 这个只是描述一个状态,具体的状态不需要继承它来实现
struct State {
public Action onEnter;
public Action onUpdate;
public Action onExit;
public Enum current;
}
// 保存当前状态的三个回调
State state = new State();
// 通过该枚举变量来切换当前状态
public Enum currentState {
get => state.current;
set {
ChangingState();
state.current = value;
TranslateState();
}
}
// 用于记录上一个状态,切换状态时,需执行上一状态的onExit()回调
public Enum lastState {get; private set;}
// 进入当前状态的时间
protected float timeEnteredState;
// 每帧更新函数,用于执行当前状态的onUpdate()回调
void Update() {
OnEarlyUpdate();
state.onUpdate();
OnLateUpdate();
}
// 状态更新之前执行的更新,方便派生类实现具体的逻辑
virtual protected void OnEarlyUpdate() {}
// 状态更新之后执行的更新,方便派生类实现具体的逻辑
virtual protected void OnLateUpdate() {}
// 开始改变状态
void ChangingState() {
lastState = currentState;
timeEnteredState = Time.time;
}
// 切换状态
void TranslateState() {
// 此时state保存的还是上一个状态的数据。先执行上一个状态的onExit()
if (state.onExit != null) {
state.onExit();
}
// 获取并更新当前状态的数据
state.onEnter = GetAction("EnterState");
state.onUpdate = GetAction("UpdateState");
state.onExit = GetAction("ExitState");
// 执行当前状态的onEnter()
if (state.onEnter != null) {
state.onEnter();
}
}
// 通过键值对保存各个状态的回调数据,避免每次都通过映射来获取
Dictionary<Enum, Dictionary<string, Action>> cache = new Dictionary<Enum, Dictionary<string, Action>>();
// 通过name来获取当前状态的对应回调
Action GetAction(string name) {
// 用于保存当前状态的三个回调
Dictionary<string, Action> actions;
// 首次获取状态回调,缓存中是不存在数据的,需先new一个
if (!cache.TryGetValue(currentState, out actions)) {
actions = new Dictionary<string, Action>();
cache.Add(currentState, actions);
}
// name对应的回调
Action action = null;
// 首次获取该回调,也是不存在的,需通过映射来获取。之后都直接通过缓存获取就行了
if (!actions.TryGetValue(name, out action)) {
// 状态回调是在状态机的派生类中实现的,命名规则举个例子:Idle_EnterState()这样
var methodInfo = GetType().GetMethod(currentState.ToString() + "_" + name, BindingFlags.Instance | BindingFlags.NonPublic); // 这是C#的官方API
if (methodInfo != null) {
// 通过映射信息创建回调
action = (Action)Delegate.CreateDelegate(typeof(Action), this, methodInfo);
} else {
// 映射信息为空,避免调用回调时报错,直接赋值一个空函数
action = DoNothing;
}
// 保存到该状态对应的回调变量里
actions.Add(name, action);
}
/*
* 其实那个cache字典就是一个enum对应一个状态的回调数据的字典
* 状态回调数据的字典通过一个string对应一个回调
*/
// 返回对应的回调
return action;
}
static void DoNothing() {}
}
状态机的逻辑实现就是这样了,很简洁。我是精简了一些源码的,具体内容可以点开代码开头的链接。
用法也很简单,贴个缩略版代码:
public class FSMAI : SuperStateMachine
{
public enum EntityState {
Idle,
// 其它更多的state
}
void Awake() {
// 切换到Idle状态
currentState = EntityState.Idle;
}
// 注意命名,前缀必须跟上面定义的enum一致,后缀则必须用那三个,然后中间一个下划线
void Idle_EnterState() {
}
void Idle_UpdateState() {
}
void Idle_ExitState() {
}
将上面的脚本挂到对象上,写好控制对象的逻辑就可以控制对象的行为了。至于截图中的效果,点开文章末尾的github链接就可以看到源码了。代码有点点长,所以就不贴出来了。
SuperStateMachine的整体就这样了。下面再简单说下Animator,用Animator也可以实现同样的效果,虽然平时都是用Animator播放动画。
在Animator的编辑面板建四个空的State如下图所示。因为角色本身就挂了个Animator播放动画用的,而且每个对象只能挂一个Animator组件,不过Animator是个分层状态机,所以新加个层Behaviors,然后在这里建State。
选中State,到Inspector面板给当前的State添加一个脚本-AddBehavior:
脚本内容大概就这样:
public class Idle : StateMachineBehaviour
{
// 这种将State独立成一个类的,就有前面说的那个问题了:需单独保存一份控制对象的数据
private object data;
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
// 避免每次进来都调用,确保只执行一次
if (data == null) {
data = animator.gameObject.GetComponent<Component>();
}
// Animator版的状态切换
animator.Play("Idle", animator.GetLayerIndex("Behaviors"));
}
public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
}
}
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
}
}
Animator的用法大致就这样了。还是那句话,图片中的效果,感兴趣的可以点开github地址。
最后就是Demo代码的地址了:Unity-SimpleFSMDemo