在游戏开发领域,命令模式和有限状态机是两种常见的设计模式,它们分别用于处理请求的封装和对象状态的管理。本文将介绍如何在 Unity 开发中将命令模式与有限状态机相结合,从而实现敌人 AI 的功能。
命令模式
封装
“命令模式”既然叫做了命令,那肯定有一个 key
用于标识命令,还要有对应命令的功能。常用的 key
有两种:字符串 string
和枚举 enum
。字符串的优点在于灵活,而枚举则比较死板(同时这也是它的优点,至少能知道 key
在什么范围)。这里使用字符串作为 key
来标识命令。所有的命令基本上都有四个功能(有的可能不需要):初始化、执行、退出、撤销。那么可以将这四个功能提取出来,封装成接口。使用泛型作为函数参数可以灵活运用在多个场景中,并且命令可以拿到命令发出者本身的属性。
public interface ICommand<T>
{
void Init(T obj);
void Execute(T obj);
void Exit(T obj);
void UoDo(T obj);
}
具体的命令,具体实现。
public class Move : ICommand<PlayerCharacterController>
{
public void Init(PlayerCharacterController obj) => obj.animator.SetBool("isMove", true);
public void Execute(PlayerCharacterController obj) => obj.animator.SetBool("isMove", true);
public void Exit(PlayerCharacterController obj) => obj.animator.SetBool("isMove", false);
public void UoDo(PlayerCharacterController obj){}
}
为保证命令可以跨类使用,这里可以将对命令的管理封装成一个公共管理类出来。
public class CmdManager<T>
{
public ICommand<T> curCmd
{
get; private set;
}
private static Dictionary<string, ICommand<T>> cmds = new Dictionary<string, ICommand<T>>();
public void AddCmd(string name, ICommand<T> cmd)
{
if (cmds.ContainsKey(name))
{
Debug.LogWarning($"索引{name}已存在");
}
else
{
cmds.Add(name, cmd);
}
}
public void ReplaceCmd(string name, ICommand<T> cmd)
{
if (cmds.ContainsKey(name))
{
cmds[name] = cmd;
}
else AddCmd(name, cmd);
}
public void RemoveCmd(string name)
{
if (cmds.ContainsKey(name))
{
cmds.Remove(name);
}
else Debug.LogWarning($"索引命令<{name}>并不存在");
}
public void SwitchCmd(string name)
{
if (cmds.ContainsKey(name))
{
curCmd = cmds[name];
}
else Debug.LogWarning($"索引命令<{name}>并不存在");
}
}
你也可以添加一个栈用于存储使用过的命令,实现回溯操作,这里就不过多的介绍了。
使用
既然已经介绍了命令的封装,那肯定要说说命令的使用了。下面以一个简单的代码来介绍如何使用命令。
public class PlayerCharacter : MonoBehaviour
{
private Animator anim;
public int curJumpNum = 2;
public CmdManager<PlayerCharacter> cmr = new CmdManager<PlayerCharacter>();
public ICommand<PlayerCharacter> state
{
get => cmr.curCmd;
set => state = value;
}
public class PlayerJump : ICommand<PlayerCharacter>
{
public void Init(PlayerCharacter obj){}
public void Execute(PlayerCharacter obj)
{
if (obj.curJumpNum <= 0) return;
obj.anim.Play("Jump");
obj.curJumpNum--;
}
public void Exit(PlayerCharacter obj) => obj.anim.SetBool("isJump", false);
public void UoDo(PlayerCharacter obj){}
}
void Start()
{
anim.GetComponent<Animator>();
cmr.AddCmd("jump", new PlayerJump());
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
cmr.SwitchCmd("jump");
curCmd.Execute(this);
}
}
}
当按下空格键时,角色会跳起,并且具有二段跳功能。在命令中可以获取命令发出者的变量,因此你可以将一些判断逻辑写到命令中去。
有限状态机
想一想,是不是每个状态都有三个过程:开始,更新,结束(没有撤销)。这与命令是不是很像?如果你能想到这点,那么你基本上已经掌握了命令模式。既然状态机和命令很像,可以封装一个状态基类继承命令接口,就可以使用之前命令模式封装好的功能了。
public abstract class StateBase<T> : ICommand<T>
{
public void Init(T obj) => OnBegin(obj);
public void Execute(T obj) => OnUpdate(obj);
public void Exit(T obj) => OnEnd(obj);
public void UoDo(T obj) { }
public abstract void OnBegin(T obj);
public abstract void OnUpdate(T obj);
public abstract void OnEnd(T obj);
}
每次实现状态只要继承 StateBase
就可以了,并且可以使用 CmdManager
来管理状态(当然,也可以直接使用命令来实现状态,但这每次实现都会多一个函数不是吗)。
ps:由于篇幅有限,只能介绍这么多了,如果有更好的方法,欢迎评论。