游戏编程模式——命令模式(Command)

概念

命令模式(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 commands = new Stack();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

  • Unity样例代码:https://pan.baidu.com/s/1hRpCP9qvzqx0V3v3bjLbXw?pwd=salt


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部