将一个请求(request)封装成一个对象,从而允许你使用不同的请求、队列或日志将客户端参数化,同 时支持请求操作的撤销与恢复。—— GoF
命令就是面向对象化的回调。——GoF
作者本人精炼后:
命令就是一个对象化(实例化)的方法调用(A command is a reifiedmethod call)
概要
优点
- 解耦
调用者和接收者 - 支持命令的
撤销和重做 - 方便实现
命令队列和日志记录
缺点
- 可能导致过多的具体命令类
- 学习成本较高
适用场景
- 操作控制
- 存档与回放
- 技能连招
- AI 行为控制
2.1 配置输入
起始
硬编码用户输入对应执行的行为(暂时能看到输入就行)
- 按空格执行 Jump()
- 按 F 执行 Fire()
public class CommandExample : MonoBehaviour
{
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
Jump();
else if (Input.GetKeyDown(KeyCode.F))
Fire();
}
private void Fire()
{
Debug.Log("Fire");
}
private void Jump()
{
Debug.Log("Jump");
}
}
新需求
- 需求:游戏允许用户配置他们的按钮与游戏行为之间的映射关系
- 研发提出质疑:“可是,我们的经验是反过来的呀,是修改行为对应的按钮”
- 策划:“这需求是老板提的,你们是质疑老板的高瞻远睹?”
- 研发:“。。。”
为了支持自定义配置,我们需要将那些对函数的直接调用替换为一些可动态改变的东西,这就用到命令模式
public class CommandExample : MonoBehaviour
{
private Command buttonSpace = new JumpCommand();
private Command buttonF = new FireCommand();
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
buttonSpace.Execute();
else if (Input.GetKeyDown(KeyCode.F))
buttonF.Execute();
}
}
// 命令基类
abstract class Command
{
public abstract void Excute();
}
// “跳跃”命令
class JumpCommand : Command
{
public override void Excute()
{
Debug.Log("Jump");
}
}
// “开火”命令
class FireCommand : Command
{
public override void Excute()
{
Debug.Log("Fire");
}
}
这样就可以方便切换按键映射的行为
public class CommandExample : MonoBehaviour
{
// ...
void Update()
{
// ...
else if (Input.GetKeyDown(KeyCode.Return))
SwitchCommand();
}
private void SwitchCommand()
{
// 按空格开火,按 F 跳跃
buttonSpace = new FireCommand();
buttonF = new JumpCommand();
Debug.Log("Command Switched");
}
}
2.2 关于角色的说明
需求又来了
- 需求:让命令控制任意角色
- 研发心想:老板放过我们吧。。。
- 解决:
- 让命令的执行支持参数,即可操作任何指定类型的实例
- 增加一个角色类,封装自身行为
public class CommandExample : MonoBehaviour
{
// ...
private GameActor gameActor = new GameActor();
void Update()
{
// 把获取输入对应命令的功能封装起来,延后执行
if (TryGetInputCommand(out Command command))
command.Excute(gameActor);
}
private bool TryGetInputCommand(out Command command)
{
command = null;
if (Input.GetKeyDown(KeyCode.Space))
command = buttonSpace;
else if (Input.GetKeyDown(KeyCode.F))
command = buttonF;
return command != null;
}
}
abstract class Command
{
public abstract void Excute(GameActor gameActor);
}
class JumpCommand : Command
{
public override void Excute(GameActor gameActor)
{
gameActor.Jump();
}
}
class FireCommand : Command
{
public override void Excute(GameActor gameActor)
{
gameActor.Fire();
}
}
class GameActor
{
public void Jump()
{
Debug.Log("Jump");
}
public void Fire()
{
Debug.Log("Fire");
}
}
- 我们可以让玩家控制游戏中的任何角色,只需向命令传入不同的角色(但不常用)
- 我们可以照搬上面的命令模式来作为 AI 引擎和受 AI 控制角色之间的接口:AI 简单地提供命令对象以供执行
- 这种解耦为我们提供了很大的灵活性。我们可以对不同的角色使用不同的 AI 模块
- 我们甚至可以将 AI 使用到玩家的角色身上
2.3 撤销和重做
双来需求了
- 需求:让命令支持撤销与重做
- 研发心想:终于提了个像样的需求,前两个需求麻烦撤销一下
- 解决:
- 只输出日志不太方便观察效果,将移动操作可视化
- 给命令增加“撤销”方法
- 增加命令队列,实现撤销与重做
第一步:将移动操作可视化
public class CommandExample : MonoBehaviour
{
// 绑定场景上的对象
[SerializeField] private Transform actorObj;
// private Command buttonSpace = new JumpCommand();
// private Command buttonF = new FireCommand();
// W、S 的按键命令,控制对象的前进与后退
private Command btnW = new MoveCommand(Vector3.forward);
private Command btnS = new MoveCommand(Vector3.back);
private GameActor gameActor;
void Start()
{
gameActor = new GameActor(actorObj);
}
//...
private bool TryGetInputCommand(out Command command)
{
command = null;
if (Input.GetKeyDown(KeyCode.W))
// command = buttonSpace;
command = btnW;
else if (Input.GetKeyDown(KeyCode.S))
// command = buttonF;
command = btnS;
return command != null;
}
}
abstract class Command
{
//...
}
// 删掉两个测试命令类
// class JumpCommand : Command { ... }
// class FireCommand : Command { ... }
class MoveCommand : Command
{
private Vector3 moveVec;
public MoveCommand(Vector3 moveVec)
{
this.moveVec = moveVec;
}
public override void Excute(GameActor gameActor)
{
gameActor.Move(moveVec);
}
}
class GameActor
{
private Transform target;
public GameActor(Transform actorObj)
{
target = actorObj;
}
// public void Jump() { ... }
// public void Fire() { ... }
public void Move(Vector3 moveVec)
{
target.position += moveVec;
Debug.Log($"Move: {moveVec}");
}
}
效果(按 W/S 控制前进/后退):
第二步:给命令增加“撤销”方法
abstract class Command
{
//...
public abstract void Undo();
}
class MoveCommand : Command
{
//...
private Vector3 oriPos;
//...
public override void Excute(GameActor gameActor)
{
// 执行前先保存原来位置,方便撤销
oriPos = gameActor.GetPos();
gameActor.Move(moveVec);
}
public override void Undo()
{
// 使用原来位置来实现撤销
gameActor.SetPos(oriPos);
}
}
class GameActor
{
//...
public void SetPos(Vector3 pos)
{
target.position = pos;
}
public Vector3 GetPos()
{
return target.position;
}
}
第三步:增加命令列表,实现撤销与重做
- 想要实现撤销,就得将执行过的命令放进命令列表中,所以每个命令都得独立
public class CommandExample : MonoBehaviour
{
[SerializeField] private Transform actorObj;
// 这里不需要了,得更改构建命令对象的方式
// private Command btnW = new MoveCommand(Vector3.forward);
// private Command btnS = new MoveCommand(Vector3.back);
private GameActor gameActor;
// 已执行的命令列表
private List<Command> commandList = new List<Command>();
// 当前的命令下标
private int commandIdx = -1;
// 当前命令
private Command CurrentCommand => commandIdx >= 0 ? commandList[commandIdx] : null;
// 下个命令
private Command NextCommand => commandIdx + 1 < commandList.Count ? commandList[commandIdx + 1] : null;
//...
void Update()
{
if (TryGetInputCommand(out Command command))
{
// command.Excute(gameActor);
ExcuteCommand(command);
}
else if (Input.GetKeyDown(KeyCode.Z))
{
// 按 Z 撤销
UndoCommand();
}
else if (Input.GetKeyDown(KeyCode.Y))
{
// 按 Y 重做
RedoCommand();
}
}
private void RedoCommand()
{
if (NextCommand == null)
return;
NextCommand.Excute();
commandIdx++;
Debug.Log($"Redo Command. idx: {commandIdx}");
}
private void UndoCommand()
{
if (CurrentCommand == null)
return;
CurrentCommand.Undo();
commandIdx--;
Debug.Log($"Undo Command. idx: {commandIdx}");
}
private void ExcuteCommand(Command command)
{
command.Excute();
// 将后面的命令清空
if (commandIdx + 1 < commandList.Count)
commandList.RemoveRange(commandIdx + 1, commandList.Count - commandIdx - 1);
commandList.Add(command);
commandIdx++;
Debug.Log($"Excute Command. idx: {commandIdx}");
}
private bool TryGetInputCommand(out Command command)
{
// 按 WSAD 得到对应的命令对象
command = null;
if (Input.GetKeyDown(KeyCode.W))
command = new MoveCommand(gameActor, Vector3.forward);
else if (Input.GetKeyDown(KeyCode.S))
command = new MoveCommand(gameActor, Vector3.back);
else if (Input.GetKeyDown(KeyCode.A))
command = new MoveCommand(gameActor, Vector3.left);
else if (Input.GetKeyDown(KeyCode.D))
command = new MoveCommand(gameActor, Vector3.right);
return command != null;
}
abstract class Command
{
// 执行方法去掉参数,参数的获取挪到构建函数中实现
// public abstract void Excute(GameActor gameActor);
public abstract void Excute();
//...
}
class MoveCommand : Command
{
//...
private GameActor gameActor;
public MoveCommand(GameActor actor, Vector3 moveVec)
{
this.gameActor = actor;
this.moveVec = moveVec;
this.oriPos = actor.GetPos();
}
// public override void Excute(GameActor gameActor)
public override void Excute()
{
// oriPos = gameActor.GetPos();
gameActor.Move(moveVec);
}
//...
}
//...
}
效果(WSAD 移动,Z 撤销,Y 重做):
最后
最后附上 UML(跟上面的代码实现有点对不上就是了,主打一个装):


