8.5 命令模式 (Command)


文档摘要

8.5 命令模式 (Command) 8.5 命令模式 (Command) 命令模式是一种行为型设计模式,旨在将请求封装成一个对象,从而使您可以用不同的请求对客户端进行参数化、队列化或日志请求,并且支持可撤销的操作。在游戏开发,尤其是 Unity3D 环境中,命令模式在处理用户输入、UI 交互、AI 行为以及实现可撤销操作等方面有着广泛的应用。 8.5.1 模式意图 命令模式的核心意图在于解耦请求的发送者和接收者。它通过引入“命令”对象作为中介,使得发送者无需知道接收者的具体实现,只需发出一个通用的“命令”即可。这种解耦带来了以下好处: 解耦发送者和接收者: 发送者不需要知道如何执行操作,只需要知道如何发出命令。接收者则专注于执行操作,无需关心命令的来源。

8.5 命令模式 (Command)

8.5 命令模式 (Command)

命令模式是一种行为型设计模式,旨在将请求封装成一个对象,从而使您可以用不同的请求对客户端进行参数化、队列化或日志请求,并且支持可撤销的操作。在游戏开发,尤其是 Unity3D 环境中,命令模式在处理用户输入、UI 交互、AI 行为以及实现可撤销操作等方面有着广泛的应用。

8.5.1 模式意图

命令模式的核心意图在于解耦请求的发送者接收者。它通过引入“命令”对象作为中介,使得发送者无需知道接收者的具体实现,只需发出一个通用的“命令”即可。这种解耦带来了以下好处:

  • 解耦发送者和接收者: 发送者不需要知道如何执行操作,只需要知道如何发出命令。接收者则专注于执行操作,无需关心命令的来源。

  • 请求队列和日志: 命令可以被存储、排队和记录日志,方便实现宏命令、操作历史记录、以及事务处理等功能。

  • 支持撤销操作: 命令模式可以方便地实现撤销 (Undo) 和重做 (Redo) 功能,因为每个命令对象都包含了执行操作和撤销操作所需的信息。

  • 参数化请求: 命令对象可以携带执行操作所需的参数,使得可以用不同的参数来参数化请求。

  • 组合命令: 可以将多个简单命令组合成一个复合命令(宏命令),从而实现更复杂的操作。

8.5.2 模式结构

命令模式主要包含以下几个核心角色:

  • Command (命令接口/抽象类):

    • 声明执行操作的接口,通常包含一个 Execute() 方法。

    • 有时也会包含 Undo() 方法,用于支持撤销操作。

  • ConcreteCommand (具体命令类):

    • 实现 Command 接口,封装一个具体的请求。

    • 通常会持有对 Receiver 对象的引用,并在 Execute() 方法中调用 Receiver 的方法来执行具体的操作。

    • 如果支持撤销操作,还需要实现 Undo() 方法来撤销之前的操作。

  • Receiver (接收者):

    • 知道如何执行与请求相关的操作。

    • 负责执行具体的业务逻辑。

    • 可以是任何类,甚至可以是客户端自身。

  • Invoker (调用者/请求者):

    • 负责请求命令执行。

    • 持有 Command 对象,并通过调用 Command 对象的 Execute() 方法来发起请求。

    • 不知道具体的命令如何执行,只负责调用命令。

    • 可以持有多个命令,例如命令队列、命令历史记录等。

  • Client (客户端):

    • 创建具体的 Command 对象,并设置其 Receiver

    • Command 对象传递给 Invoker

以下是命令模式的 UML 类图,以及使用 Mermaid 绘制的 graph TD 图:

UML 类图:

@startuml class Client class Invoker class Command { +Execute() +Undo() } class ConcreteCommand { -receiver : Receiver +ConcreteCommand(receiver : Receiver) +Execute() +Undo() } class Receiver { +Action() } Client --|> Invoker : 使用 Client --|> Command : 创建 Invoker --|> Command : 持有和调用 ConcreteCommand --|> Command : 实现 ConcreteCommand --> Receiver : 关联 Command <|-- ConcreteCommand @enduml

Mermaid Graph TD 图:

流程说明:

  1. 客户端 (Client) 创建一个 ConcreteCommand 对象,并将 Receiver 对象设置到命令中。

  2. 客户端 (Client)ConcreteCommand 对象传递给 调用者 (Invoker)

  3. 调用者 (Invoker) 在需要的时候,调用 Command 对象的 Execute() 方法。

  4. ConcreteCommandExecute() 方法被调用,它会调用 Receiver 的相应方法来执行具体的业务逻辑。

  5. Receiver 执行操作并完成请求。

8.5.3 代码实践 (Unity3D C#)

为了更好地理解命令模式在 Unity3D 中的应用,我们通过一个简单的玩家控制示例来演示。假设我们需要控制一个游戏对象 (GameObject) 的移动和跳跃,并且希望支持撤销操作。

1. 定义 Command 接口 (ICommand.cs):

using UnityEngine; public interface ICommand { void Execute(); void Undo(); }

2. 创建具体命令类 (MoveCommand.cs, JumpCommand.cs):

  • MoveCommand.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; // 恢复到执行前的位置 } }
  • JumpCommand.cs: 负责使游戏对象跳跃。
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):

接收者类负责处理玩家控制相关的逻辑,例如获取输入、执行移动和跳跃等。 在命令模式中,我们将把实际的移动和跳跃操作委托给 CharacterControllerRigidbody 组件,而不是直接在 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 脚本附加到游戏对象上,负责接收玩家输入,创建具体的 MoveCommandJumpCommand 对象,并设置相应的接收者 ( CharacterControllerRigidbody )。

使用方法:

  1. 在 Unity 编辑器中创建一个新的 3D 项目。

  2. 创建一个 3D 对象 (例如 Cube) 作为玩家角色。

  3. 为玩家角色添加 CharacterControllerRigidbody 组件。

  4. 创建 C# 脚本 ICommand.cs, MoveCommand.cs, JumpCommand.cs, 和 PlayerController.cs,并将上述代码复制粘贴到相应的脚本文件中。

  5. PlayerController.cs 脚本附加到玩家角色对象上。

  6. PlayerController 组件的 Inspector 面板中,将 Character ControllerRigidbody Component 属性拖拽设置为玩家角色对象自身的 CharacterControllerRigidbody 组件。

  7. 创建一个地面对象 (例如 Plane),并设置 Layer 为 "Ground" (或者您自定义的地面 Layer)。

  8. PlayerController 组件的 Inspector 面板中,设置 Ground Mask 属性为 "Ground" Layer。

  9. 运行游戏,您可以使用 WASD 键控制角色移动,空格键跳跃,Ctrl+Z 撤销上一步操作,Ctrl+Y 重做 (简单的重做)。

代码详解:

  • ICommand 接口: 定义了命令的基本接口,包含 Execute()Undo() 方法。

  • MoveCommand 和 JumpCommand: 具体命令类,实现了 ICommand 接口。

    • MoveCommand 使用 CharacterController.Move() 进行移动,并记录了移动前的 position 用于撤销。

    • JumpCommand 使用 Rigidbody.AddForce() 进行跳跃,但 Undo() 方法为了简化示例,只输出了警告信息,实际跳跃的撤销需要更复杂的逻辑。

  • PlayerController: 充当了接收者、客户端和调用者的角色。

    • HandleInput() 方法监听玩家输入,根据输入创建不同的命令对象 ( MoveCommandJumpCommand )。

    • ExecuteCommand() 方法执行命令,并将命令添加到 _commandHistory 列表中。

    • UndoCommand()RedoCommand() 方法实现了撤销和重做功能,通过操作 _commandHistory 列表来实现。

优点:

  • 解耦: PlayerController (客户端/调用者) 与具体的移动和跳跃逻辑解耦,只需要创建和执行命令,无需关心命令的具体实现细节。

  • 可扩展性: 很容易添加新的命令,例如攻击、冲刺等,只需要创建新的具体命令类并实现 ICommand 接口即可,无需修改 PlayerController 的核心逻辑。

  • 可维护性: 每个命令类职责单一,代码结构清晰,易于维护和测试。

  • 支持撤销/重做: 通过命令历史记录,可以方便地实现撤销和重做功能。

缺点:

  • 类增多: 引入命令模式可能会导致类数量增加,对于简单的操作,可能会显得有些繁琐。

  • 复杂性增加: 对于简单的场景,使用命令模式可能会增加代码的复杂性。需要权衡是否值得为了解耦和可扩展性而引入这种模式。

  • 撤销操作的复杂性: 某些操作的撤销逻辑可能比较复杂,例如跳跃的撤销,需要仔细设计 Undo() 方法的实现。

8.5.4 应用场景

命令模式在 Unity3D 游戏开发中有很多应用场景,以下是一些常见的例子:

  • 用户输入处理: 将用户输入 (键盘、鼠标、触摸) 映射为命令,例如移动命令、射击命令、跳跃命令等。可以方便地实现输入队列、宏命令、以及输入重映射等功能。

  • UI 交互: 将 UI 按钮点击、菜单选择等操作封装成命令,例如打开面板命令、关闭面板命令、切换场景命令等。可以方便地实现 UI 操作的撤销和重做,以及 UI 操作的日志记录。

  • AI 行为: 将 AI 角色的行为 (移动、攻击、巡逻等) 封装成命令,可以方便地实现 AI 行为的规划、调度、以及行为树等复杂的 AI 逻辑。

  • 游戏存档和回放: 可以将游戏操作记录为命令序列,用于游戏存档和回放功能。通过重放命令序列,可以还原游戏状态。

  • 宏命令 (Macro Command): 将多个简单命令组合成一个复合命令,一次执行多个操作。例如,一个“组合攻击”宏命令可以包含多个攻击技能命令。

  • 事务处理: 在需要保证操作原子性的场景中,可以使用命令模式来实现事务处理。可以将一系列操作封装成一个事务命令,要么全部执行成功,要么全部回滚。

8.5.5 总结

命令模式是一种强大的行为型设计模式,通过将请求封装成对象,实现了请求发送者和接收者之间的解耦,提高了系统的灵活性、可扩展性和可维护性。在 Unity3D 游戏开发中,命令模式在处理用户输入、UI 交互、AI 行为以及实现可撤销操作等方面都有着广泛的应用价值。

虽然命令模式可能会增加代码的复杂性,但对于需要处理复杂操作逻辑、需要支持撤销/重做、或者需要提高系统扩展性的场景,命令模式是一个非常值得考虑的设计选择。 开发者需要根据具体的项目需求和场景,权衡使用命令模式的利弊,并合理地应用该模式来提升游戏项目的质量和开发效率。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U