在您的Unity项目中实现常见的游戏编程设计模式可以帮助您高效地构建和维护一个干净、组织良好且易于阅读的代码库。设计模式减少了重构和测试时间,加快了开发过程,为您的游戏、团队和业务的增长奠定了坚实的基础。
将设计模式视为您无法复制并粘贴到代码中的完成解决方案,而是作为可以帮助您构建更大、可扩展应用程序的额外工具。
此页面解释了命令设计模式。
这里的内容基于免费电子书,通过游戏编程模式提升你的代码水平。
在Unity最佳实践中心或通过这些链接查看Unity游戏编程设计模式系列的更多文章。
- 对象池
- 状态模式
- 观察者模式
- MVP/MVC模式
- 工厂模式
命令编程设计模式是原始四人帮之一,当你想跟踪一系列特定的动作时,它非常有用。如果你玩过使用撤销/重做功能或将你的输入历史保存在列表中的游戏,你可能已经见过命令模式的应用。想象一个策略游戏,用户可以在实际执行之前规划几个回合。那就是命令模式。
该命令模式允许将动作表示为对象。将动作封装为对象使您能够创建一个灵活且可扩展的系统,以便根据用户输入控制游戏对象的行为。这通过将一个或多个方法调用封装为“命令对象”而不是直接调用方法来工作。然后你可以将这些命令对象存储在一个集合中,比如队列或栈,这样就可以作为一个小缓冲区。
以这种方式存储命令对象使您能够通过可能延迟一系列操作以便稍后播放来控制它们的执行时机。同样,您可以重做或撤消它们,并增加额外的灵活性来控制每个命令对象的执行。
以下是该模式在不同游戏类型中的一些常见应用:
- 在实时战略游戏中,命令模式可以用来排队单位和建筑的动作。游戏将根据资源的可用性执行每个命令。
- 在一款回合制策略游戏中,玩家可以选择一个单位,然后将其移动或行动存储在队列或其他集合中。在回合结束时,游戏将执行玩家队列中的所有命令。
- 在一个益智游戏中,命令模式可以让玩家撤销和重做动作。
- 在格斗游戏中,读取特定指令列表中的按钮按压或游戏手柄动作可能会导致连击和特殊动作。
尝试在 GitHub 上的示例项目,该项目展示了游戏开发中不同的编程设计模式,包括命令模式。
在这个示例中,玩家可以通过点击左侧的按钮在迷宫中移动。当你的玩家移动时,你可以看到一条移动的轨迹。但更重要的是,您可以撤消和重做之前的操作。
要在项目中找到相应的场景,请转到名为“9 Command”的文件夹。
要实现命令模式,您需要一个通用对象来包含您的操作。该命令对象将保存要执行的逻辑以及如何撤销它。
有多种方法可以实现这一点,但这里有一个使用名为 ICommand 的接口的简单版本:
公共接口 ICommand
{
执行();
void Undo();
}
在这种情况下,每个游戏玩法动作将应用 ICommand 接口(您也可以通过抽象类来实现这一点)。
每个命令对象将负责其自己的 执行 和 撤销 方法。所以向你的游戏添加更多命令不会影响任何现有命令。
CommandInvoker 类负责执行和撤销命令。除了ExecuteCommand和UndoCommand方法外,它还有一个撤销栈来保存命令对象的序列。
在示例项目中,您可以在一个小迷宫中移动您的玩家。将玩家位置移动的一个简单选项是创建一个 PlayerMover。
要做到这一点,您需要将一个 Vector3 传递给 Move 方法,以引导玩家沿四个方向移动。您还可以使用射线投射来检测适当 LayerMask 中的墙壁。当然,实现您想要应用于命令模式的内容与模式本身是分开的。
要遵循命令模式,将 PlayerMover’s Move 方法捕获为一个对象。不要直接调用Move,而是创建一个新类MoveCommand,实现ICommand接口。
公共类 MoveCommand :命令
{
玩家移动器 playerMover;
向量3 运动;
公共 MoveCommand(PlayerMover player, Vector3 moveVector)
{
this.playerMover = player;
this.movement = moveVector;
}
公共无效 执行()
{
playerMover.Move(movement);
}
public void Undo()
{
playerMover.Move(-movement);
}
}
无论你想实现什么逻辑,都在这里进行,所以调用 Move 并传入移动向量。
ICommand 还需要一个 Undo 方法来将场景恢复到之前的状态。在这种情况下,撤销 逻辑减去移动向量,基本上将玩家推向相反的方向。
该MoveCommand存储执行所需的任何参数。用构造函数设置这些。在这种情况下,您保存适当的PlayerMover组件和运动向量。
一旦您创建了命令对象并保存其所需的参数,请使用命令调用者的静态执行命令和撤销命令方法来传入您的移动命令。这会运行MoveCommand’s Execute或Undo并在撤消堆栈中跟踪命令对象。
输入管理器InputManager并不直接调用PlayerMover’s移动方法。相反,添加一个额外的方法,RunMoveCommand,以创建一个新的 MoveCommand 并将其发送到 CommandInvoker。
然后,设置UI按钮的各种onClick事件,以调用RunPlayerCommand与四个移动向量。
查看 示例项目 以获取 InputManager 的实现细节。您还可以使用键盘或游戏手柄设置自己的输入。您的玩家现在可以在迷宫中导航。点击撤消按钮,以便您可以回到起始方块。
实现可重玩性或可撤销性就像生成一组命令对象一样简单。您还可以使用命令缓冲区按顺序回放带有特定控制的操作。
例如,想象一下一个格斗游戏,其中一系列特定的按钮点击会触发连击动作或攻击。使用命令模式存储玩家动作使得设置这些组合变得简单得多。
另一方面,命令模式引入了更多的结构,就像其他设计模式一样。您需要决定这些额外的类和接口在您的应用程序中部署命令对象是否提供了足够的好处。
一旦你掌握了基础知识,你就可以根据上下文影响命令的时机,并顺序或反向播放它们。
在采用命令模式时,请考虑以下几点:
- 创建更多命令:该示例项目仅包含一种类型的命令对象,即 MoveCommand。您可以创建任意数量的命令对象,这些对象实现了ICommand并使用CommandInvoker进行跟踪。
- 添加重做功能是添加另一个堆栈的问题:当您撤消一个命令对象时,将其推送到一个单独的堆栈上,以跟踪重做操作。通过这种方式,您可以快速浏览撤消历史记录或重做这些操作。当用户调用一个全新的动作时,清空重做栈(您可以在示例项目中找到实现)。
- 使用不同的集合来作为您的命令对象缓冲区:如果您想要先进先出(FIFO)行为,队列可能会更方便。如果您使用列表,请跟踪当前活动索引;活动索引之前的命令是可撤销的。索引后的命令是可重做的。
- 限制堆栈的大小:撤消和重做操作可能会迅速失控。将堆栈限制为最少的命令数量。
- 将任何必要的参数传递给构造函数:这有助于封装逻辑,如 MoveCommand 示例中所示。
- 命令调用者CommandInvoker与其他外部对象一样,看不到命令对象的内部工作,只能调用执行或撤销。在调用构造函数时,给命令对象提供任何需要的数据。