LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(7):命令模式(Command)


一、命令模式核心要素

  • Command 接口
public interface ICommand {
    void Execute();
    void Undo();
}
  • ConcreteCommand
    封装对具体 Receiver 的一次操作,并保存足够状态以便撤销。

  • Receiver
    真正执行业务逻辑的对象,如角色、UI、场景管理器。

  • Invoker
    请求者,调用 Execute() 并将命令对象入栈,用于后续撤销/重做。

  • Client
    创建具体命令并将 Receiver、参数注入,然后交给 Invoker 执行。

    CommandPattern


二、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);
}

四、扩展与优化

  1. 命令队列
    支持异步执行、节流处理:

    private readonly Queue<ICommand> _queue = new();
    void Update() {
        if (_queue.Count > 0) {
            invoker.ExecuteCommand(_queue.Dequeue());
        }
    }
    public void Enqueue(ICommand cmd) => _queue.Enqueue(cmd);
    
  2. 命令池化
    高频创建命令时 reuse 对象,避免 GC 压力。

  3. 参数化撤销
    对于复杂命令可存储更丰富状态(如动画帧、物理状态)。

  4. 序列化与重放
    将命令序列化为 JSON/二进制,实现网络同步或存档回放。


五、注意事项

场景 陷阱 建议
撤销状态不足 命令未保存完全的先前状态,Undo 恢复效果不准确 在构造时缓存全部必要状态,或在 Execute 前 snapshot 全局状态
命令过多未清理 长时间游戏后 Undo/Redo 栈过大,内存/性能压力 限制栈深度,或分段清理历史
依赖 MonoBehaviour 命令对象持有对 MonoBehaviour 的直接引用,难以 Mock 测试 命令和 Invoker 依赖纯 C# 接口,业务逻辑注入 Receiver 实现
并发安全 在多线程或协程中并发执行命令,可能引发数据竞争 只在主线程执行,或为命令队列添加锁

六、小结

  1. 职责分离:命令只封装请求,不包含业务逻辑细节,Receiver 承担真正执行;
  2. 状态管理:在命令中缓存执行前后足够的信息,以支持精确撤销;
  3. 接口驱动:使用 ICommandIInvoker 抽象,便于单元测试与拓展;
  4. 宏与队列:结合宏命令与队列,实现复杂的连击、教程演示、脚本化场景;
  5. 性能考量:命令对象可复用或池化,避免频繁 new 和 GC;