8.5 命令模式 (Command) 8.5 命令模式 (Command) 命令模式是一种行为型设计模式,旨在将请求封装成一个对象,从而使您可以用不同的请求对客户端进行参数化、队列化或日志请求,并且支持可撤销的操作。在游戏开发,尤其是 Unity3D 环境中,命令模式在处理用户输入、UI 交互、AI 行为以及实现可撤销操作等方面有着广泛的应用。 8.5.1 模式意图 命令模式的核心意图在于解耦请求的发送者和接收者。它通过引入“命令”对象作为中介,使得发送者无需知道接收者的具体实现,只需发出一个通用的“命令”即可。这种解耦带来了以下好处: 解耦发送者和接收者: 发送者不需要知道如何执行操作,只需要知道如何发出命令。接收者则专注于执行操作,无需关心命令的来源。
命令模式是一种行为型设计模式,旨在将请求封装成一个对象,从而使您可以用不同的请求对客户端进行参数化、队列化或日志请求,并且支持可撤销的操作。在游戏开发,尤其是 Unity3D 环境中,命令模式在处理用户输入、UI 交互、AI 行为以及实现可撤销操作等方面有着广泛的应用。
命令模式的核心意图在于解耦请求的发送者和接收者。它通过引入“命令”对象作为中介,使得发送者无需知道接收者的具体实现,只需发出一个通用的“命令”即可。这种解耦带来了以下好处:
解耦发送者和接收者: 发送者不需要知道如何执行操作,只需要知道如何发出命令。接收者则专注于执行操作,无需关心命令的来源。
请求队列和日志: 命令可以被存储、排队和记录日志,方便实现宏命令、操作历史记录、以及事务处理等功能。
支持撤销操作: 命令模式可以方便地实现撤销 (Undo) 和重做 (Redo) 功能,因为每个命令对象都包含了执行操作和撤销操作所需的信息。
参数化请求: 命令对象可以携带执行操作所需的参数,使得可以用不同的参数来参数化请求。
组合命令: 可以将多个简单命令组合成一个复合命令(宏命令),从而实现更复杂的操作。
命令模式主要包含以下几个核心角色:
Command (命令接口/抽象类):
声明执行操作的接口,通常包含一个 Execute() 方法。
有时也会包含 Undo() 方法,用于支持撤销操作。
ConcreteCommand (具体命令类):
实现 Command 接口,封装一个具体的请求。
通常会持有对 Receiver 对象的引用,并在 Execute() 方法中调用 Receiver 的方法来执行具体的操作。
如果支持撤销操作,还需要实现 Undo() 方法来撤销之前的操作。
Receiver (接收者):
知道如何执行与请求相关的操作。
负责执行具体的业务逻辑。
可以是任何类,甚至可以是客户端自身。
Invoker (调用者/请求者):
负责请求命令执行。
持有 Command 对象,并通过调用 Command 对象的 Execute() 方法来发起请求。
不知道具体的命令如何执行,只负责调用命令。
可以持有多个命令,例如命令队列、命令历史记录等。
Client (客户端):
创建具体的 Command 对象,并设置其 Receiver。
将 Command 对象传递给 Invoker。
以下是命令模式的 UML 类图,以及使用 Mermaid 绘制的 graph TD 图:
UML 类图:
Mermaid Graph TD 图:
流程说明:
客户端 (Client) 创建一个 ConcreteCommand 对象,并将 Receiver 对象设置到命令中。
客户端 (Client) 将 ConcreteCommand 对象传递给 调用者 (Invoker)。
调用者 (Invoker) 在需要的时候,调用 Command 对象的 Execute() 方法。
ConcreteCommand 的 Execute() 方法被调用,它会调用 Receiver 的相应方法来执行具体的业务逻辑。
Receiver 执行操作并完成请求。
为了更好地理解命令模式在 Unity3D 中的应用,我们通过一个简单的玩家控制示例来演示。假设我们需要控制一个游戏对象 (GameObject) 的移动和跳跃,并且希望支持撤销操作。
1. 定义 Command 接口 (ICommand.cs):
using UnityEngine; public interface ICommand { void Execute(); void Undo(); }
2. 创建具体命令类 (MoveCommand.cs, JumpCommand.cs):
using UnityEngine; public class MoveCommand : ICommand { private CharacterController _characterController; private Vector3 _direction; private float _speed; private Vector3 _previousPosition; // 用于撤销操作 public MoveCommand(CharacterController characterController, Vector3 direction, float speed) { _characterController = characterController; _direction = direction; _speed = speed; } public void Execute() { _previousPosition = _characterController.transform.position; // 记录执行前的位置 _characterController.Move(_direction * _speed * Time.deltaTime); } public void Undo() { _characterController.transform.position = _previousPosition; // 恢复到执行前的位置 } }
using UnityEngine; public class JumpCommand : ICommand { private Rigidbody _rigidbody; private float _jumpForce; private bool _isGrounded; // 假设需要检测是否在地面才能跳跃 (简化示例,实际可能更复杂) public JumpCommand(Rigidbody rigidbody, float jumpForce, bool isGrounded) { _rigidbody = rigidbody; _jumpForce = jumpForce; _isGrounded = isGrounded; } public void Execute() { if (_isGrounded) { _rigidbody.AddForce(Vector3.up * _jumpForce, ForceMode.Impulse); } } public void Undo() { // 跳跃操作的撤销相对复杂,这里为了简化示例,不做撤销跳跃的具体实现 // 实际应用中,可能需要记录跳跃前的状态,或者使用更复杂的撤销逻辑 Debug.LogWarning("Jump Undo is not fully implemented in this example."); } }
3. 创建接收者类 (PlayerController.cs):
接收者类负责处理玩家控制相关的逻辑,例如获取输入、执行移动和跳跃等。 在命令模式中,我们将把实际的移动和跳跃操作委托给 CharacterController 和 Rigidbody 组件,而不是直接在 PlayerController 中实现所有逻辑。
using UnityEngine; using System.Collections.Generic; public class PlayerController : MonoBehaviour { public float moveSpeed = 5f; public float jumpForce = 7f; public CharacterController characterController; public Rigidbody rigidbodyComponent; public Transform groundCheck; public float groundDistance = 0.2f; public LayerMask groundMask; private List<ICommand> _commandHistory = new List<ICommand>(); // 命令历史记录,用于撤销 private int _commandHistoryIndex = -1; // 当前命令历史索引 void Start() { if (characterController == null) characterController = GetComponent<CharacterController>(); if (rigidbodyComponent == null) rigidbodyComponent = GetComponent<Rigidbody>(); } void Update() { HandleInput(); } void HandleInput() { float horizontalInput = Input.GetAxis("Horizontal"); float verticalInput = Input.GetAxis("Vertical"); Vector3 moveDirection = new Vector3(horizontalInput, 0f, verticalInput).normalized; if (moveDirection.magnitude > 0) { ICommand moveCommand = new MoveCommand(characterController, moveDirection, moveSpeed); ExecuteCommand(moveCommand); } if (Input.GetButtonDown("Jump")) { bool isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask); // 检测是否在地面 ICommand jumpCommand = new JumpCommand(rigidbodyComponent, jumpForce, isGrounded); ExecuteCommand(jumpCommand); } if (Input.GetKeyDown(KeyCode.Z) && Input.GetKey(KeyCode.LeftControl)) // Ctrl+Z 撤销操作 { UndoCommand(); } if (Input.GetKeyDown(KeyCode.Y) && Input.GetKey(KeyCode.LeftControl)) // Ctrl+Y 重做操作 (简化的重做,需要更完善的实现) { RedoCommand(); } } void ExecuteCommand(ICommand command) { command.Execute(); // 添加到命令历史记录 if (_commandHistoryIndex < _commandHistory.Count - 1) { // 如果在撤销操作后执行新命令,则清空之后的历史记录 _commandHistory.RemoveRange(_commandHistoryIndex + 1, _commandHistory.Count - _commandHistoryIndex - 1); } _commandHistory.Add(command); _commandHistoryIndex++; // 可选:限制命令历史记录的大小,防止内存溢出 if (_commandHistory.Count > 20) // 例如,限制最多记录 20 个操作 { _commandHistory.RemoveAt(0); _commandHistoryIndex--; } } void UndoCommand() { if (_commandHistoryIndex >= 0) { _commandHistory[_commandHistoryIndex].Undo(); _commandHistoryIndex--; } } void RedoCommand() { if (_commandHistoryIndex < _commandHistory.Count - 1) { _commandHistoryIndex++; _commandHistory[_commandHistoryIndex].Execute(); // 重新执行命令 } } }
4. 创建调用者 (Invoker) - 在本例中,PlayerController 同时扮演了客户端和调用者的角色。
在 PlayerController 中,_commandHistory 列表和 ExecuteCommand 方法充当了调用者的角色,负责存储和执行命令。 HandleInput 方法则负责创建具体的命令对象并将其传递给调用者。
5. 客户端 (Client) - 在本例中,PlayerController 也扮演了客户端的角色。
PlayerController 脚本附加到游戏对象上,负责接收玩家输入,创建具体的 MoveCommand 和 JumpCommand 对象,并设置相应的接收者 ( CharacterController 和 Rigidbody )。
使用方法:
在 Unity 编辑器中创建一个新的 3D 项目。
创建一个 3D 对象 (例如 Cube) 作为玩家角色。
为玩家角色添加 CharacterController 和 Rigidbody 组件。
创建 C# 脚本 ICommand.cs, MoveCommand.cs, JumpCommand.cs, 和 PlayerController.cs,并将上述代码复制粘贴到相应的脚本文件中。
将 PlayerController.cs 脚本附加到玩家角色对象上。
在 PlayerController 组件的 Inspector 面板中,将 Character Controller 和 Rigidbody Component 属性拖拽设置为玩家角色对象自身的 CharacterController 和 Rigidbody 组件。
创建一个地面对象 (例如 Plane),并设置 Layer 为 "Ground" (或者您自定义的地面 Layer)。
在 PlayerController 组件的 Inspector 面板中,设置 Ground Mask 属性为 "Ground" Layer。
运行游戏,您可以使用 WASD 键控制角色移动,空格键跳跃,Ctrl+Z 撤销上一步操作,Ctrl+Y 重做 (简单的重做)。
代码详解:
ICommand 接口: 定义了命令的基本接口,包含 Execute() 和 Undo() 方法。
MoveCommand 和 JumpCommand: 具体命令类,实现了 ICommand 接口。
MoveCommand 使用 CharacterController.Move() 进行移动,并记录了移动前的 position 用于撤销。
JumpCommand 使用 Rigidbody.AddForce() 进行跳跃,但 Undo() 方法为了简化示例,只输出了警告信息,实际跳跃的撤销需要更复杂的逻辑。
PlayerController: 充当了接收者、客户端和调用者的角色。
HandleInput() 方法监听玩家输入,根据输入创建不同的命令对象 ( MoveCommand 或 JumpCommand )。
ExecuteCommand() 方法执行命令,并将命令添加到 _commandHistory 列表中。
UndoCommand() 和 RedoCommand() 方法实现了撤销和重做功能,通过操作 _commandHistory 列表来实现。
优点:
解耦: PlayerController (客户端/调用者) 与具体的移动和跳跃逻辑解耦,只需要创建和执行命令,无需关心命令的具体实现细节。
可扩展性: 很容易添加新的命令,例如攻击、冲刺等,只需要创建新的具体命令类并实现 ICommand 接口即可,无需修改 PlayerController 的核心逻辑。
可维护性: 每个命令类职责单一,代码结构清晰,易于维护和测试。
支持撤销/重做: 通过命令历史记录,可以方便地实现撤销和重做功能。
缺点:
类增多: 引入命令模式可能会导致类数量增加,对于简单的操作,可能会显得有些繁琐。
复杂性增加: 对于简单的场景,使用命令模式可能会增加代码的复杂性。需要权衡是否值得为了解耦和可扩展性而引入这种模式。
撤销操作的复杂性: 某些操作的撤销逻辑可能比较复杂,例如跳跃的撤销,需要仔细设计 Undo() 方法的实现。
命令模式在 Unity3D 游戏开发中有很多应用场景,以下是一些常见的例子:
用户输入处理: 将用户输入 (键盘、鼠标、触摸) 映射为命令,例如移动命令、射击命令、跳跃命令等。可以方便地实现输入队列、宏命令、以及输入重映射等功能。
UI 交互: 将 UI 按钮点击、菜单选择等操作封装成命令,例如打开面板命令、关闭面板命令、切换场景命令等。可以方便地实现 UI 操作的撤销和重做,以及 UI 操作的日志记录。
AI 行为: 将 AI 角色的行为 (移动、攻击、巡逻等) 封装成命令,可以方便地实现 AI 行为的规划、调度、以及行为树等复杂的 AI 逻辑。
游戏存档和回放: 可以将游戏操作记录为命令序列,用于游戏存档和回放功能。通过重放命令序列,可以还原游戏状态。
宏命令 (Macro Command): 将多个简单命令组合成一个复合命令,一次执行多个操作。例如,一个“组合攻击”宏命令可以包含多个攻击技能命令。
事务处理: 在需要保证操作原子性的场景中,可以使用命令模式来实现事务处理。可以将一系列操作封装成一个事务命令,要么全部执行成功,要么全部回滚。
命令模式是一种强大的行为型设计模式,通过将请求封装成对象,实现了请求发送者和接收者之间的解耦,提高了系统的灵活性、可扩展性和可维护性。在 Unity3D 游戏开发中,命令模式在处理用户输入、UI 交互、AI 行为以及实现可撤销操作等方面都有着广泛的应用价值。
虽然命令模式可能会增加代码的复杂性,但对于需要处理复杂操作逻辑、需要支持撤销/重做、或者需要提高系统扩展性的场景,命令模式是一个非常值得考虑的设计选择。 开发者需要根据具体的项目需求和场景,权衡使用命令模式的利弊,并合理地应用该模式来提升游戏项目的质量和开发效率。