游戏编程模式——命令模式(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<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格)或撤消/重做以外的情况下,这样可以达成需求,但在其他一些情况下你可能需要每次都实例化一个新的指令。比如在某战棋游戏中,移动指令可能代表了角色从移动到,这个两个坐标对于每次行动都不一样,因此需要每次执行时都重新实例化一个新的指令,否则新的坐标会覆盖前一次坐标,导致无法实现撤销的功能。

参考资料

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舒服嘉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值