命令式编程设计模式是最初的四人帮之一,每当你想跟踪特定的一系列动作时都很有用。如果您玩过使用撤消/重做功能的游戏,或者将输入历史记录保存在列表中,您可能已经看到命令模式在起作用。想象一个策略游戏,用户可以在实际执行之前计划几个回合。这就是命令模式。
命令模式允许将动作表示为对象。将动作封装为对象使您能够创建灵活且可扩展的系统,用于控制 GameObjects 响应用户输入的行为。其工作原理是将一个或多个方法调用封装为“命令对象”,而不是直接调用方法。然后,您可以将这些命令对象存储在一个集合中,如队列或堆栈,它作为小缓冲区工作。
以这种方式存储命令对象使您能够通过潜在地延迟以后播放的一系列操作来控制命令对象的执行时间。同样,您可以重做或撤消它们,并增加额外的灵活性来控制每个命令对象的执行。
以下是该模式在不同游戏流派中的一些常见应用:
- 在实时战略游戏中,命令模式可用于将单位和建筑动作排队。然后游戏将在资源可用时执行每个命令。
- 在回合制策略游戏中,玩家可以选择一个单位,然后将其移动或动作存储在队列或其他集合中。回合结束时,游戏将执行玩家队列中的所有命令。
- 在益智游戏中,命令模式可以让玩家撤消和重做动作。
- 在格斗游戏中,特定命令列表中的读取按钮按下或游戏垫移动可能会导致连击和特殊动作。
试试 GitHub 上的示例项目,它展示了游戏开发中不同的编程设计模式,包括命令模式。
在这个例子中,玩家可以点击左侧的按钮在迷宫中移动。随着玩家的移动,您可以看到移动轨迹。但更重要的是,您可以撤消和重做以前的操作。
要在项目中查找相应的场景,请转到名为“9 Command”的文件夹。
要实现命令模式,您需要一个包含您操作的一般对象。此命令对象将保存要执行的逻辑以及如何撤消它。
有许多方法可以实现这一点,但下面是使用名为 ICommand 的接口的简单版本:
公共接口ICommand
{
void Execute ( ) ;
void Undo();
}
在这种情况下,每个游戏动作都将应用 ICommand 接口(您也可以用抽象类来实现 ) 。
每个命令对象将负责自己的执行和撤消方法。因此,向游戏中添加更多命令不会影响任何现有的命令。
然后,CommandInvoker 类负责执行和撤消命令。除了 ExecuteCommand 和 UndoCommand 方法之外,它还有一个撤消堆栈来保存命令对象的序列。
在示例项目中,您可以在一个小迷宫周围移动您的播放器。改变玩家位置的简单选项是创建 PlayerMover。
为此,你需要用Vector3进入Move方法引导玩家沿着四个指南针方向前进。您还可以使用光线投射来检测相应 LayerMask 中的墙壁。当然,实现要应用到命令模式的内容与模式本身是分开的。
要遵循命令模式,请将 PlayerMover 的 Move 方法捕获为对象。不要直接调用 Move,而是创建一个实现 ICommand 接口的新类 MoveCommand。
public class MoveCommand :ICommand
{
PlayerMover playerMover;
Vector3 movement;
公共 MoveCommand(PlayerMover 播放器,Vector3 moveVector)
{
this.playerMover = player;
this.movement = moveVector;
}
public void Execute ( )
{
com.move(move ) ;
}
public void Undo ( )
{
playerMover.Move(-movement);
}
}
无论你想要完成什么逻辑都在这里,所以调用Move with the movement vector。
ICommand 还需要一个 Undo 方法来将场景恢复到以前的状态。在这种情况下,撤消逻辑会减去移动向量,实质上将玩家推向相反的方向。
MoveCommand 存储它需要执行的任何参数。用构造函数设置这些。在这种情况下,您可以保存相应的 PlayerMover 组件和运动矢量。
创建命令对象并保存其所需参数后,使用 CommandInvoker 的静态 ExecuteCommand 和 UndoCommand 方法在 MoveCommand 中传递。这将运行 MoveCommand 的执行或撤消,并跟踪撤消堆栈中的命令对象。
InputManager 不直接调用 PlayerMover 的 Move 方法。相反,添加一个额外的方法 RunMoveCommand 来创建新的 MoveCommand 并将其发送到 CommandInvoker。
然后,设置 UI Buttons 的各种 onClick 事件,使用四个运动向量调用 RunPlayerCommand。
请查看示例项目,了解 InputManager 的实施详细信息。您还可以使用键盘或游戏垫设置自己的输入。您的玩家现在可以浏览迷宫。"单击""撤消""按钮,以便您可以回溯到起始方格。"
实现可重放性或可撤消性与生成命令对象集合一样简单。您还可以使用命令缓冲区以特定控件的顺序播放操作。
例如,想想一个格斗游戏,一系列特定的按钮点击触发组合移动或攻击。用命令模式存储玩家动作使设置这些组合变得简单得多。
另一方面,命令模式引入了更多的结构,就像其他设计模式一样。您必须决定这些额外的类和接口在哪些方面为在您的应用程序中部署命令对象提供足够的好处
学习了基础知识后,您可以影响命令的时序,并根据上下文顺序或反向播放。
在合并命令模式时考虑以下事项:
- 创建更多命令:示例项目仅包括一种类型的命令对象,即 MoveCommand。您可以创建任意数量的实现 ICommand 的命令对象,并使用 CommandInvoker 对其进行跟踪。
- 添加重做功能需要添加另一个堆栈:撤消命令对象时,将其推送到跟踪重做操作的单独堆栈上。这样,您可以快速循环浏览撤消历史记录或重做这些操作。当用户调用一个全新的移动时清除重做堆栈(您可以在示例项目中找到实现)。
- 为命令对象的缓冲区使用不同的集合:如果要先进先出 (FIFO) 行为,队列可能会更方便。如果使用列表,则跟踪当前活动的索引;活动索引之前的命令不可撤消。索引后的命令可重做。
- 限制堆栈的大小:撤消和重做操作会迅速失控。将堆栈限制为最少的命令数。
- 向构造函数传递任何必要的参数:这有助于封装 MoveCommand 示例中看到的逻辑。
- CommandInvoker 和其他外部对象一样,看不到命令对象的内部工作原理,只能调用 Execute 或 Undo。为命令对象提供调用构造函数时工作所需的任何数据。