概念
命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
简单来说就是把命令封装成一个对象,并实现请求者和执行者的解耦,进而可以改变请求者和执行者的组合。此外,命令模式也很适合实现撤销功能。
常见应用
游戏开发中常见的应用是输入处理和撤消/重做。
输入处理:输入模块或AI发出命令,由指定对象执行命令,实现输入模块(或AI)和对象的解耦。于是只要让角色都能执行对应的指令,玩家就可以控制不同的角色;AI也可以控制不同的敌人,同样的敌人也可以受到不同的AI控制(比如高难度下高进攻性的AI或低难度下的摸鱼AI)。
撤销重做:通过保存执行过的指令的队列,反向撤销这些指令进行的操作,就可以实现重做的功能。比如悔棋。
注意:以下代码为基于Unity的C#,但并未经过验证,可以当作伪代码,其他语言需针对自身特性,但思路一致。
输入处理
通常我们代码会写成下面这样:
private void InputHandler()
{
if (Input.GetButton("Jump")) Jump();
if (Input.GetButton("Fire")) Fire();
// 此处省略其他输入处理
}
private void Jump()
{
// 跳跃功能实现
}
// 此处省略其他功能实现
但如此一来,输入和动作就耦合到一个模块中了。
为了实现命令模式,首先要实现指令基类:
public abstract class Command
{
public abstract void Execute(GameObject obj);
}
随后实现指令子类
class JumpCommand: Command
{
public override void Execute(GameObject obj)
{
obj.SendMessage("Jump");
}
}
// 此处省略其他子类
在输入处理程序中,保存每一个按键所对应命令的指针:
public GameObject obj; // 受控对象
private readonly _jumpCommand = new JumpCommand();
// 此处省略其他指令的实例化
private void InputHandler()
{
if (Input.GetButton("Jump")) _jumpCommand.Execute(obj);
// 此处省略其他输入处理
}
撤销/重做
在原本基类的基础上,还需要实现一个Undo方法,用以撤销Execute进行的操作。由于撤销的操作对象是不言而喻的,因此在Execute中用变量_obj记下操作对象,在Undo时针对_obj进行撤销。基类变成下面这样:
public abstract class Command
{
public abstract void Execute(GameObject obj);
public abstract void Undo();
}
public class MoveCommand: Command
{
private GameObject _obj;
public override void Execute(GameObject obj)
{
_obj = obj;
_obj.SendMessage("Move");
}
public override void Undo()
{
_obj.SendMessage("MoveUndo");
}
}
其中,_obj也可以在构造函数中指定,Execute就可以不必每次都传入obj了。
随后,新建一个CommandManager来管理执行过的指令:
public class CommandManager : MonoBehaviour
{
private static Stack<Command> commands = new Stack<Command>();
public static void Add(Command cmd)
{
commands.Push(cmd);
}
public static void Undo()
{
if (commands.Count > 0)
commands.Pop().Undo();
}
}
撤销时的顺序应该是后执行的指令先撤销,即先进后出(FILO),因此应该使用栈(stack)来保存指令序列。撤销时,只要调用弹出栈顶的命令,并调用其Undo方法即可。
修改后的输入处理程序如下:
public GameObject obj; // 受控对象
private readonly _moveCommand = new MoveCommand();
// 此处省略其他指令的实例化
private void InputHandler()
{
if (Input.GetButton("Move"))
{
_moveCommand.Execute(obj);
CommandManager.Add(_moveCommand);
}
if (Input.GetButton("MoveUndo"))
{
CommandManager.Undo();
}
// 此处省略其他输入处理
}
重做功能与之类似,只需在CommandManager中额外加一个栈来保存被撤销的指令,重做时依次调用栈顶的Execute方法即可。
也可以简单地使用顺序表来实现这个功能,使用一个指针来代表下一个空位。撤销时指针-1,并撤销当前命令;重做时执行当前命令,指针+1;新的命令存到指针指向的位置后指针+1,并将当前位置置为Null以防止重做。
注意:此处的代码存在问题,即每次存入指令序列的都是同一个指令,在指令的操作很简单(如前移1格)或撤消/重做以外的情况下,这样可以达成需求,但在其他一些情况下你可能需要每次都实例化一个新的指令。比如在某战棋游戏中,移动指令可能代表了角色从移动到,这个两个坐标对于每次行动都不一样,因此需要每次执行时都重新实例化一个新的指令,否则新的坐标会覆盖前一次坐标,导致无法实现撤销的功能。
参考资料
"命令模式 | 菜鸟教程." https://www.runoob.com/design-pattern/command-pattern.html
"Command · Design Patterns Revisited · Game Programming Patterns." http://gameprogrammingpatterns.com/command.html#directions-for-actors