一、命令模式核心要素
- Command 接口
public interface ICommand {
void Execute();
void Undo();
}
ConcreteCommand
封装对具体 Receiver 的一次操作,并保存足够状态以便撤销。Receiver
真正执行业务逻辑的对象,如角色、UI、场景管理器。Invoker
请求者,调用Execute()并将命令对象入栈,用于后续撤销/重做。Client
创建具体命令并将 Receiver、参数注入,然后交给 Invoker 执行。
二、Unity 示例:角色移动与攻击命令
2.1 定义 Receiver
public class Player : MonoBehaviour {
public float moveSpeed = 5f;
public void MoveTo(Vector3 position) {
// 直接瞬移或启动寻路
transform.position = Vector3.MoveTowards(
transform.position, position, moveSpeed * Time.deltaTime);
}
public void Attack(Enemy enemy) {
// 播放攻击动画并扣血
animator.SetTrigger("Attack");
enemy.TakeDamage(weaponDamage);
}
}
2.2 实现具体命令
// 移动命令
public class MoveCommand : ICommand {
private Player _player;
private Vector3 _target;
private Vector3 _previous; // 用于撤销
public MoveCommand(Player player, Vector3 target) {
_player = player;
_target = target;
_previous = player.transform.position;
}
public void Execute() {
_player.MoveTo(_target);
}
public void Undo() {
_player.MoveTo(_previous);
}
}
// 攻击命令
public class AttackCommand : ICommand {
private Player _player;
private Enemy _enemy;
private int _damageDealt;
public AttackCommand(Player player, Enemy enemy) {
_player = player;
_enemy = enemy;
}
public void Execute() {
_damageDealt = _enemy.CurrentHealth;
_player.Attack(_enemy);
}
public void Undo() {
_enemy.RestoreHealth(_damageDealt);
}
}
2.3 构建 Invoker
public class CommandInvoker {
private readonly Stack<ICommand> _undoStack = new Stack<ICommand>();
private readonly Stack<ICommand> _redoStack = new Stack<ICommand>();
public void ExecuteCommand(ICommand command) {
command.Execute();
_undoStack.Push(command);
_redoStack.Clear();
}
public void Undo() {
if (_undoStack.Count == 0) return;
var cmd = _undoStack.Pop();
cmd.Undo();
_redoStack.Push(cmd);
}
public void Redo() {
if (_redoStack.Count == 0) return;
var cmd = _redoStack.Pop();
cmd.Execute();
_undoStack.Push(cmd);
}
}
2.4 客户端组合:输入处理
public class PlayerController : MonoBehaviour {
public Player player;
public Camera mainCamera;
private CommandInvoker invoker = new CommandInvoker();
void Update() {
// 点击地面移动
if (Input.GetMouseButtonDown(1)) {
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out var hit)) {
var moveCmd = new MoveCommand(player, hit.point);
invoker.ExecuteCommand(moveCmd);
}
}
// 按键 undo/redo
if (Input.GetKeyDown(KeyCode.Z)) invoker.Undo();
if (Input.GetKeyDown(KeyCode.Y)) invoker.Redo();
// 主动触发攻击
if (Input.GetMouseButtonDown(0)) {
if (Physics.Raycast(mainCamera.ScreenPointToRay(Input.mousePosition), out var hit)) {
var enemy = hit.collider.GetComponent<Enemy>();
if (enemy != null) {
var atkCmd = new AttackCommand(player, enemy);
invoker.ExecuteCommand(atkCmd);
}
}
}
}
}
三、宏命令与操作录制
3.1 宏命令封装
public class MacroCommand : ICommand {
private readonly List<ICommand> _commands = new List<ICommand>();
public void Add(ICommand cmd) => _commands.Add(cmd);
public void Execute() {
foreach (var cmd in _commands) cmd.Execute();
}
public void Undo() {
for (int i = _commands.Count - 1; i >= 0; i--)
_commands[i].Undo();
}
}
3.2 示例:录制简单连招
// 在 Controller 中:
private MacroCommand combo = new MacroCommand();
// 录制阶段
if (recording && Input.GetMouseButtonDown(0)) {
var atk = new AttackCommand(player, target);
combo.Add(atk);
}
// 播放阶段
if (playCombo) {
invoker.ExecuteCommand(combo);
}
四、扩展与优化
命令队列
支持异步执行、节流处理:private readonly Queue<ICommand> _queue = new(); void Update() { if (_queue.Count > 0) { invoker.ExecuteCommand(_queue.Dequeue()); } } public void Enqueue(ICommand cmd) => _queue.Enqueue(cmd);命令池化
高频创建命令时 reuse 对象,避免 GC 压力。参数化撤销
对于复杂命令可存储更丰富状态(如动画帧、物理状态)。序列化与重放
将命令序列化为 JSON/二进制,实现网络同步或存档回放。
五、注意事项
| 场景 | 陷阱 | 建议 |
|---|---|---|
| 撤销状态不足 | 命令未保存完全的先前状态,Undo 恢复效果不准确 | 在构造时缓存全部必要状态,或在 Execute 前 snapshot 全局状态 |
| 命令过多未清理 | 长时间游戏后 Undo/Redo 栈过大,内存/性能压力 | 限制栈深度,或分段清理历史 |
| 依赖 MonoBehaviour | 命令对象持有对 MonoBehaviour 的直接引用,难以 Mock 测试 |
命令和 Invoker 依赖纯 C# 接口,业务逻辑注入 Receiver 实现 |
| 并发安全 | 在多线程或协程中并发执行命令,可能引发数据竞争 | 只在主线程执行,或为命令队列添加锁 |
六、小结
- 职责分离:命令只封装请求,不包含业务逻辑细节,Receiver 承担真正执行;
- 状态管理:在命令中缓存执行前后足够的信息,以支持精确撤销;
- 接口驱动:使用
ICommand、IInvoker抽象,便于单元测试与拓展; - 宏与队列:结合宏命令与队列,实现复杂的连击、教程演示、脚本化场景;
- 性能考量:命令对象可复用或池化,避免频繁
new和 GC;