Unity游戏开发中的命令模式:从撤销重做到AI系统的实战应用
1. 项目概述为什么Unity开发者绕不开命令模式如果你在Unity里做过稍微复杂一点的交互比如一个带撤销重做的关卡编辑器、一个技能释放队列或者一个需要响应多种输入的角色控制器那你大概率已经和“命令模式”打过照面了只是可能没意识到它的名字。这个模式在游戏开发里尤其是Unity这种以组件和事件驱动为主的引擎里出场率极高。它解决的痛点非常明确将“请求”或“操作”本身封装成一个独立的对象从而让你能把操作的具体执行者、发起者和执行时机彻底解耦。想想看你按下一个技能键角色播放动画、消耗法力、产生伤害特效、给敌人挂上Debuff……这一连串动作如果全写在一个PlayerController的Update里用一堆if-else判断代码很快就会变成一团乱麻难以维护更别提实现“撤销上个技能”这种功能了。命令模式就是把“释放火球术”这个操作打包成一个CastFireballCommand对象。这个对象知道执行它需要哪些数据施法者、目标位置、法术强度也知道具体该怎么执行调用哪些方法。于是你的输入系统只需要生成并发送这个命令对象至于谁接收、何时执行、甚至能不能执行都可以灵活安排。最近社区里讨论的“Unity MCP”、“Claude Code危险模式命令”其实背后都隐含着对更灵活、更可控的操作封装与执行流程的需求。而命令模式正是构建这种可控系统的经典基石。无论你是想优化UI框架如处理复杂的按钮点击链、管理游戏状态如实现回合制指令队列还是构建高级的AI行为树理解并善用命令模式都能让你的代码架构清晰好几个等级。接下来我会结合几个在Unity中非常实用的实例拆解命令模式的核心思想、多种实现变体以及那些官方手册里不会告诉你的实战技巧和深坑。2. 核心思路把“做什么”变成“什么东西”命令模式的核心用一个生活化的类比就是“点餐”。你去餐厅不是冲到厨房对厨师喊“把西红柿切成块打两个鸡蛋热锅下油……”你只需要告诉服务员“我要一份西红柿炒鸡蛋”。这张写着“西红柿炒鸡蛋”的订单就是一个“命令对象”。服务员调用者不需要知道菜怎么做他只需要把订单命令交给后厨接收者。后厨收到订单按照既定的流程执行方法完成烹饪。如果你想加菜、退菜、或者把炒鸡蛋换成蒸鸡蛋都只需要操作订单而不是去修改厨师。在软件里这就意味着我们把一个操作或请求封装成一个包含所有必要信息的独立对象。这样做的巨大优势在于解耦发起者与执行者按下按键的代码不需要知道角色控制器具体有哪些方法。支持命令队列与日志你可以把命令对象放进一个列表实现延迟执行、顺序执行、或者记录历史以供撤销。支持命令的组合与扩展可以轻松创建“宏命令”一个命令由多个子命令组合而成。实现撤销/重做Undo/Redo这是命令模式最经典的应用。因为命令对象封装了操作的所有信息它自然也可以实现一个反向操作Undo方法。在Unity的语境下我们通常需要定义几个关键角色ICommand接口这是所有命令的契约通常至少包含一个Execute()方法。这是模式的“标准答案”。具体命令类Concrete Command实现ICommand接口的类。它内部会持有接收者对象一个或多个并在Execute()方法中调用接收者的具体方法。它就像那张具体的点餐单。接收者Receiver真正执行操作的对象。比如一个Player类它有MoveTo(Vector3)、Jump()等方法。命令对象会调用这些方法。调用者/发起者Invoker要求命令执行请求的对象。在Unity里这可能是InputManager、UIButton、或者一个GameManager。它持有命令对象并在合适的时机如按键触发、回合结束调用其Execute()方法。客户端Client创建具体命令对象并设置其接收者的部分。通常在你的场景初始化或配置代码中。注意在简单的Unity项目中调用者和客户端有时会合并比如直接在MonoBehaviour的Update里检测输入并创建、执行命令。但随着系统复杂将它们分离是更清晰的做法。3. 基础实现从一个可撤销的移动系统开始理论说再多不如看代码。我们来实现一个Unity中最经典的需求一个可以撤销移动的单位控制系统。首先定义我们的命令接口。一个支持撤销的命令接口需要两个方法// ICommand.cs public interface ICommand { void Execute(); void Undo(); }接着创建接收者也就是我们的游戏单位// Unit.cs public class Unit : MonoBehaviour { [SerializeField] private float moveSpeed 5f; private Vector3 _targetPosition; // 这是接收者执行操作的方法 public void MoveTo(Vector3 position) { _targetPosition position; Debug.Log(${gameObject.name} 开始向 {position} 移动); // 在实际项目中这里会触发寻路或直接设置位置 StartCoroutine(MoveCoroutine(position)); } private IEnumerator MoveCoroutine(Vector3 target) { while (Vector3.Distance(transform.position, target) 0.1f) { transform.position Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime); yield return null; } transform.position target; } // 提供一个获取当前位置的方法用于命令记录状态 public Vector3 GetPosition() transform.position; }现在创建具体命令。这个命令需要知道移动哪个单位以及移动到哪里。为了支持撤销它还需要记住移动前的位置。// MoveCommand.cs public class MoveCommand : ICommand { private Unit _unit; private Vector3 _targetPosition; private Vector3 _previousPosition; // 关键用于撤销 public MoveCommand(Unit unit, Vector3 targetPosition) { _unit unit; _targetPosition targetPosition; } public void Execute() { // 执行前先记录当前位置 _previousPosition _unit.GetPosition(); // 调用接收者的方法 _unit.MoveTo(_targetPosition); } public void Undo() { // 撤销操作让单位回到之前的位置 _unit.MoveTo(_previousPosition); } }然后我们需要一个调用者来管理和执行这些命令。这里我们实现一个简单的命令历史记录器它既是调用者执行命令也充当了客户端的部分角色维护历史。// CommandInvoker.cs public class CommandInvoker : MonoBehaviour { private StackICommand _commandHistory new StackICommand(); // 用于撤销 private StackICommand _redoHistory new StackICommand(); // 用于重做 public void ExecuteCommand(ICommand command) { command.Execute(); _commandHistory.Push(command); _redoHistory.Clear(); // 执行新命令后重做历史清空 Debug.Log(命令已执行历史深度: _commandHistory.Count); } public void Undo() { if (_commandHistory.Count 0) { ICommand lastCommand _commandHistory.Pop(); lastCommand.Undo(); _redoHistory.Push(lastCommand); Debug.Log(撤销执行重做栈深度: _redoHistory.Count); } } public void Redo() { if (_redoHistory.Count 0) { ICommand lastUndoneCommand _redoHistory.Pop(); lastUndoneCommand.Execute(); _commandHistory.Push(lastUndoneCommand); } } }最后在客户端比如一个GameManager或测试脚本中把它们串联起来// ClientTester.cs public class ClientTester : MonoBehaviour { public Unit playerUnit; public CommandInvoker invoker; void Update() { if (Input.GetMouseButtonDown(0)) { // 获取鼠标点击的世界坐标假设在地面层 Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { // 1. 创建具体命令对象客户端职责 ICommand moveCommand new MoveCommand(playerUnit, hit.point); // 2. 通过调用者执行命令 invoker.ExecuteCommand(moveCommand); } } if (Input.GetKeyDown(KeyCode.Z) (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))) { invoker.Undo(); } if (Input.GetKeyDown(KeyCode.Y) (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl))) { invoker.Redo(); } } }实操心得与避坑指南状态记录是撤销的关键MoveCommand在Execute()时第一时间保存了_previousPosition。这是最容易出错的地方。如果等到_unit.MoveTo执行完再记录位置已经改变撤销就无法回到真正的“之前”状态了。对于更复杂的命令如改变角色属性、创建/销毁物体你需要记录足够的信息来恢复原状。命令对象的生命周期在这个例子中命令对象在执行后依然被历史栈引用所以不会销毁。如果你的命令对象很大包含大量资源引用需要注意内存管理。对于一次性命令可以在历史栈中只存储必要的数据序列化后的数据而非整个命令对象。Unity协程与命令的兼容性我们的Unit.MoveTo内部用了协程来实现平滑移动。这没问题但要注意命令的Execute()方法是同步调用并立即返回的它启动了协程但不会等待协程结束。这意味着从命令系统的视角看命令“已执行完毕”可以放入历史栈了。如果你的命令必须等待一个异步操作完成才算“执行完毕”则需要更复杂的机制比如让命令返回一个IEnumerator或Task由调用者来管理其生命周期。4. 高级应用构建一个灵活的技能系统基础移动的撤销重做只是开胃菜。命令模式在构建技能系统时更能大放异彩。一个技能通常包含条件检查法力、冷却、目标、前摇动画、伤害计算、特效播放、音效、后续效果击退、中毒等多个步骤。用命令模式封装技能逻辑将变得无比清晰且可复用。我们设计一个更复杂的技能命令结构。首先升级我们的命令接口增加一个“能否执行”的检查。// ISkillCommand.cs public interface ISkillCommand : ICommand { bool CanExecute(); float GetCooldown(); }然后创建一个基础的技能命令抽象类处理一些通用逻辑比如冷却。// SkillCommand.cs public abstract class SkillCommand : ISkillCommand { protected Unit _caster; protected float _cooldownTime; protected float _lastCastTime -Mathf.Infinity; public SkillCommand(Unit caster, float cooldown) { _caster caster; _cooldownTime cooldown; } public virtual bool CanExecute() { // 基础检查冷却是否结束 return Time.time _lastCastTime _cooldownTime; } public virtual void Execute() { if (!CanExecute()) { Debug.LogWarning(技能无法释放可能处于冷却中。); return; } // 执行具体技能逻辑 PerformSkill(); // 更新释放时间 _lastCastTime Time.time; } protected abstract void PerformSkill(); // 由子类实现具体效果 public virtual void Undo() { // 技能撤销通常很复杂可能需要回滚血量、状态等。 // 在实战中许多技能是不可撤销的如伤害这里我们简单处理。 Debug.Log(技能效果撤销演示实际可能无法完全撤销); // 例如可以将冷却时间回滚 _lastCastTime Time.time - _cooldownTime; // 假装没放过 } public float GetCooldown() _cooldownTime; }现在实现两个具体技能// FireballSkillCommand.cs public class FireballSkillCommand : SkillCommand { private Vector3 _targetPoint; private float _damage; private GameObject _fireballPrefab; public FireballSkillCommand(Unit caster, Vector3 target, float damage, GameObject prefab, float cooldown 2f) : base(caster, cooldown) { _targetPoint target; _damage damage; _fireballPrefab prefab; } protected override void PerformSkill() { Debug.Log(${_caster.name} 释放火球术目标点: {_targetPoint}); // 1. 消耗法力假设Unit有Mana属性 // _caster.ConsumeMana(30); // 2. 播放施法动画假设有Animator组件 // _caster.GetComponentAnimator().SetTrigger(CastFireball); // 3. 创建火球投射物 GameObject fireball GameObject.Instantiate(_fireballPrefab, _caster.transform.position Vector3.up, Quaternion.identity); Projectile projectile fireball.GetComponentProjectile(); if (projectile ! null) { projectile.Initialize(_targetPoint, _damage, _caster); } // 4. 播放音效 // AudioManager.Instance.PlaySFX(fireball_cast); } public override bool CanExecute() { // 在基础冷却检查上增加法力检查 // bool hasMana _caster.CurrentMana 30; // return base.CanExecute() hasMana; return base.CanExecute(); // 简化版 } } // HealSkillCommand.cs public class HealSkillCommand : SkillCommand { private Unit _target; private float _healAmount; public HealSkillCommand(Unit caster, Unit target, float healAmount, float cooldown 5f) : base(caster, cooldown) { _target target; _healAmount healAmount; } protected override void PerformSkill() { Debug.Log(${_caster.name} 对 {_target.name} 释放治疗术恢复 {_healAmount} 点生命。); // 假设Unit有Heal方法 // _target.Heal(_healAmount); // 播放治疗特效和音效 } public override void Undo() { // 治疗的撤销扣除被治疗的生命值需谨慎处理可能引发逻辑问题 Debug.Log($撤销治疗从 {_target.name} 扣除 {_healAmount} 点生命。); // _target.TakeDamage(_healAmount, _caster); // 注意这可能不是期望的逻辑 // 更安全的做法是在技能执行时记录目标的原始血量在Undo时恢复。 } }最后我们创建一个技能管理器调用者来绑定按键和技能命令。// SkillManager.cs public class SkillManager : MonoBehaviour { public Unit playerUnit; public GameObject fireballPrefab; private DictionaryKeyCode, ISkillCommand _skillKeyBindings new DictionaryKeyCode, ISkillCommand(); void Start() { // 绑定技能按键客户端配置职责 _skillKeyBindings[KeyCode.Q] new FireballSkillCommand(playerUnit, Vector3.zero, 50f, fireballPrefab, 1.5f); _skillKeyBindings[KeyCode.E] new HealSkillCommand(playerUnit, playerUnit, 30f, 3f); } void Update() { foreach (var kvp in _skillKeyBindings) { if (Input.GetKeyDown(kvp.Key)) { ISkillCommand command kvp.Value; // 在执行前进行最终检查例如目标是否有效 if (command.CanExecute()) { command.Execute(); // 这里也可以将命令发送给一个中央命令队列而不是立即执行 } else { Debug.Log($技能 {kvp.Key} 无法释放。); // 可以触发UI提示如冷却倒计时或法力不足 } } } // 鼠标右键设定火球目标 if (Input.GetMouseButtonDown(1)) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { // 动态更新Q键火球命令的目标点 // 注意这里我们直接替换了命令对象。更优雅的做法是命令内部持有可变的目标引用。 if (_skillKeyBindings.ContainsKey(KeyCode.Q) _skillKeyBindings[KeyCode.Q] is FireballSkillCommand fireballCmd) { // 由于命令对象可能正在被引用更好的设计是让命令的目标点可设置。 // 我们重构一下为FireballSkillCommand添加一个SetTarget方法。 // 这里为了演示我们重新创建命令不推荐在Update中频繁创建对象。 // _skillKeyBindings[KeyCode.Q] new FireballSkillCommand(playerUnit, hit.point, 50f, fireballPrefab, 1.5f); } } } } }高级技巧与深度思考命令与数据的分离注意FireballSkillCommand的构造参数包含了伤害、预制体等数据。在实际项目中这些数据最好从配置表如ScriptableObject中读取而不是硬编码在命令类里。命令类本身只定义流程数据由外部注入这样同一个FireballSkillCommand类可以配置出成百上千种不同的火球技能。条件检查的层级CanExecute()方法的设计很有讲究。我们将基础冷却检查放在基类将法力检查放在子类。你还可以设计一个“条件检查链”将距离检查、视野检查、状态检查是否被沉默等作为独立的“条件对象”组合起来让技能命令的CanExecute()依次通过所有条件检查使系统更加模块化。命令的合并与宏命令你可以创建一个CompositeSkillCommand它内部维护一个ListISkillCommand。它的Execute()方法会按顺序执行所有子命令。这可以用来实现“连招”或“组合技”。同样它的Undo()需要按相反顺序撤销所有子命令。异步技能与命令很多技能有吟唱时间Channeling。这要求命令的Execute()不能立即完成。一种处理方式是让Execute()返回一个IEnumerator由技能管理器通过协程来驱动。或者引入“命令状态”Pending, Casting, Finished由管理器在每帧更新。5. 架构演进命令队列与AI行为控制当你的游戏需要处理大量有序或并发的操作时比如RTS游戏中多个单位的指令、AI按优先级选择行为一个简单的Update循环调用就不够用了。这时引入命令队列Command Queue和优先级系统就非常有用。我们构建一个简单的AI行为系统AI会根据环境如发现敌人、血量低将不同的行为命令推入队列并按优先级执行。首先定义带优先级的命令接口// IPrioritizedCommand.cs public interface IPrioritizedCommand : ICommand { int Priority { get; } // 优先级数值越高越优先 bool IsInterruptible { get; } // 是否可被更高优先级命令中断 }实现一个AI命令队列管理器// AICommandScheduler.cs public class AICommandScheduler : MonoBehaviour { private ListIPrioritizedCommand _commandQueue new ListIPrioritizedCommand(); private IPrioritizedCommand _currentCommand; private bool _isExecuting false; public void EnqueueCommand(IPrioritizedCommand command) { _commandQueue.Add(command); // 按优先级排序优先级高的在前 _commandQueue.Sort((a, b) b.Priority.CompareTo(a.Priority)); Debug.Log($命令入队: {command.GetType().Name}, 当前队列长度: {_commandQueue.Count}); } void Update() { // 如果当前没有正在执行的命令且队列不为空则执行下一个 if (!_isExecuting _commandQueue.Count 0) { ExecuteNextCommand(); } // 可以在这里检查当前命令是否应该被中断例如AI受到攻击 // if (_currentCommand ! null ShouldInterrupt(_currentCommand)) { // InterruptCurrentCommand(); // } } private void ExecuteNextCommand() { if (_commandQueue.Count 0) return; _currentCommand _commandQueue[0]; _commandQueue.RemoveAt(0); Debug.Log($开始执行命令: {_currentCommand.GetType().Name}, 优先级: {_currentCommand.Priority}); _isExecuting true; // 假设命令是同步执行的。如果是异步需要更复杂的状态管理。 _currentCommand.Execute(); // 命令执行完毕这里简化处理实际需要命令回调通知 OnCommandFinished(); } private void OnCommandFinished() { Debug.Log($命令执行完毕: {_currentCommand.GetType().Name}); _currentCommand null; _isExecuting false; // 执行完后可以立即检查并执行下一个 } private void InterruptCurrentCommand() { if (_currentCommand ! null _currentCommand.IsInterruptible) { Debug.Log($中断命令: {_currentCommand.GetType().Name}); // 这里可以调用命令的一个OnInterrupt方法进行清理工作 _currentCommand null; _isExecuting false; } } }现在为AI创建几个具体的行为命令// PatrolCommand.cs public class PatrolCommand : IPrioritizedCommand { private AIUnit _aiUnit; private Vector3[] _waypoints; private int _currentWaypointIndex 0; public int Priority 1; // 巡逻优先级最低 public bool IsInterruptible true; public PatrolCommand(AIUnit aiUnit, Vector3[] waypoints) { _aiUnit aiUnit; _waypoints waypoints; } public void Execute() { Debug.Log(${_aiUnit.name} 开始巡逻。); PatrolToNextPoint(); } private void PatrolToNextPoint() { if (_waypoints.Length 0) return; _aiUnit.MoveTo(_waypoints[_currentWaypointIndex], OnReachedWaypoint); } private void OnReachedWaypoint() { _currentWaypointIndex (_currentWaypointIndex 1) % _waypoints.Length; PatrolToNextPoint(); // 循环巡逻 } public void Undo() { // 停止巡逻回到上一个位置或初始位置 _aiUnit.StopMoving(); Debug.Log(巡逻命令被撤销。); } } // AttackCommand.cs public class AttackCommand : IPrioritizedCommand { private AIUnit _aiUnit; private Unit _target; public int Priority 10; // 攻击优先级很高 public bool IsInterruptible false; // 攻击一旦开始不易被打断 public AttackCommand(AIUnit aiUnit, Unit target) { _aiUnit aiUnit; _target target; } public void Execute() { if (_target null || !_target.IsAlive) { Debug.Log(攻击目标无效命令取消。); return; } Debug.Log(${_aiUnit.name} 开始攻击 {_target.name}); _aiUnit.EngageTarget(_target); // 这里可以启动一个协程持续攻击直到目标死亡或超出范围 } public void Undo() { _aiUnit.StopAttacking(); Debug.Log(攻击命令被撤销。); } } // FleeCommand.cs public class FleeCommand : IPrioritizedCommand { private AIUnit _aiUnit; private Vector3 _safePoint; public int Priority 100; // 逃命优先级最高 public bool IsInterruptible false; public FleeCommand(AIUnit aiUnit, Vector3 safePoint) { _aiUnit aiUnit; _safePoint safePoint; } public void Execute() { Debug.Log(${_aiUnit.name} 血量过低开始逃往安全点 {_safePoint}); _aiUnit.MoveTo(_safePoint, OnReachedSafePoint); } private void OnReachedSafePoint() { Debug.Log(${_aiUnit.name} 已到达安全点。); // 到达后可以触发一个回血或待机命令 } public void Undo() { // 逃命命令通常不可撤销或者撤销意义不大 Debug.Log(逃命命令被撤销但可能已经跑远了。); } }AI单位本身需要感知环境并向调度器发送命令// AIUnit.cs public class AIUnit : MonoBehaviour { public AICommandScheduler scheduler; public float lowHealthThreshold 0.3f; private Unit _unit; // 假设继承或持有Unit组件 private Unit _detectedEnemy; void Start() { _unit GetComponentUnit(); // 开始一个默认的巡逻命令 Vector3[] patrolRoute new Vector3[] { new Vector3(10,0,0), new Vector3(-10,0,0) }; scheduler.EnqueueCommand(new PatrolCommand(this, patrolRoute)); } void Update() { // 1. 感知逻辑简化 CheckHealth(); DetectEnemy(); // 2. 基于感知结果生成并提交命令 if (_unit.CurrentHealth / _unit.MaxHealth lowHealthThreshold) { // 血量低生成逃命命令高优先级会打断当前巡逻 Vector3 safePoint FindSafePoint(); scheduler.EnqueueCommand(new FleeCommand(this, safePoint)); return; // 发现紧急情况本次Update不再处理其他命令生成 } if (_detectedEnemy ! null) { // 发现敌人生成攻击命令 scheduler.EnqueueCommand(new AttackCommand(this, _detectedEnemy)); _detectedEnemy null; // 清空避免每帧重复提交 } } private void CheckHealth() { /* 实现血量检查 */ } private void DetectEnemy() { /* 实现敌人检测 */ } private Vector3 FindSafePoint() { return Vector3.zero; /* 实现寻找安全点 */ } // 供命令调用的方法 public void MoveTo(Vector3 point, Action onArrived null) { /* 移动实现完成后回调onArrived */ } public void StopMoving() { /* 停止移动 */ } public void EngageTarget(Unit target) { /* 开始攻击目标 */ } public void StopAttacking() { /* 停止攻击 */ } }架构设计要点队列与优先级AICommandScheduler使用列表和排序来实现优先级队列。对于性能要求极高的场景如大量AI可以考虑使用真正的优先队列数据结构如PriorityQueue。命令中断IsInterruptible属性允许高优先级命令中断低优先级的、可中断的命令。中断发生时需要妥善清理被中断命令的状态例如停止移动、取消攻击动画。这需要在命令接口中增加OnInterrupt()方法。异步命令执行上面的例子假设命令是同步执行的。现实中移动、攻击都是持续过程。一种更健壮的设计是让Execute()方法启动一个异步操作并提供一个IsFinished属性或完成回调调度器在Update中检查命令状态只有当前命令完成后才执行下一个。感知与决策的分离AIUnit的Update负责感知检查血量、发现敌人和生成命令。这是一个简单的“感知-思考-行动”循环。更复杂的AI可以使用状态机或行为树而命令模式可以完美作为行为树的“叶节点”Action Node的实现方式。6. 性能优化与实战陷阱命令模式很强大但在Unity中不加思考地使用也可能带来性能和维护问题。下面是一些关键的优化点和常见陷阱。1. 命令对象的创建与池化在每帧都可能产生大量命令的场景如RTS单位群控频繁实例化new MoveCommand()会造成GC垃圾回收压力。解决方案是使用对象池Object Pool。// CommandPool.cs (简化示例) public class CommandPoolT where T : ICommand, new() { private StackT _pool new StackT(); public T Get() { if (_pool.Count 0) { return _pool.Pop(); } return new T(); } public void Release(T command) { // 重置命令状态到初始值 // 如果命令有Reset方法可以在这里调用 _pool.Push(command); } } // 使用方式 public class OptimizedMoveCommand : ICommand { public Unit TargetUnit; public Vector3 Destination; private Vector3 _previousPos; // 提供一个重置方法供对象池调用 public void Reset(Unit unit, Vector3 dest) { TargetUnit unit; Destination dest; _previousPos Vector3.zero; } public void Execute() { _previousPos TargetUnit.GetPosition(); TargetUnit.MoveTo(Destination); } public void Undo() { /* ... */ } } // 在客户端 CommandPoolOptimizedMoveCommand moveCommandPool new CommandPoolOptimizedMoveCommand(); ... var cmd moveCommandPool.Get(); cmd.Reset(selectedUnit, mouseWorldPos); invoker.ExecuteCommand(cmd); // 命令执行完毕并历史记录后如果确定不再需要可以释放回池需谨慎处理历史引用2. 命令的序列化与网络同步如果你想实现多人游戏中的操作同步或保存/加载游戏状态命令需要能被序列化转换成字节流或JSON。这意味着命令包含的数据如单位ID、目标坐标必须是可序列化的而命令对象本身可能无法直接序列化因为它包含对场景中MonoBehaviour的引用。常见的做法是使用“命令数据”和“命令工厂”。命令数据Command Data一个纯数据的结构体struct包含执行命令所需的所有信息如unitId,targetPosition。这个结构体是可序列化的。命令工厂Command Factory根据命令数据在需要执行时结合当前的游戏状态通过ID查找到真正的Unit对象重建出可执行的命令对象。[System.Serializable] public struct MoveCommandData { public int unitInstanceId; public SerializableVector3 targetPosition; } public class CommandFactory { public static ICommand CreateMoveCommandFromData(MoveCommandData data, WorldState world) { Unit unit world.FindUnitById(data.unitInstanceId); if (unit ! null) { return new MoveCommand(unit, data.targetPosition.ToVector3()); } return null; } }3. 撤销/重做的边界与复杂度不是所有操作都适合撤销。比如“保存游戏”、“退出到主菜单”这类命令撤销可能没有意义或实现成本极高。对于涉及随机数如暴击、网络请求、或外部系统调用的命令撤销可能非常困难甚至不可能。 在设计命令时要明确界定系统的撤销边界。对于不能撤销的命令可以让其Undo()方法为空或抛出一个异常。同时过于复杂的撤销逻辑如撤销一个已经影响了10个其他系统的操作可能会导致“撤销”本身的代码比“执行”还复杂这时需要重新考虑架构或许引入“快照”模式定期保存完整游戏状态比命令模式的撤销更合适。4. 命令与Unity生命周期的耦合MonoBehaviour命令持有对GameObject或Component的引用。如果命令被放入历史栈长期保存而对应的游戏对象被销毁了就会产生空引用异常。解决方法使用WeakReference存储对接收者的弱引用但Unity的UnityEngine.Object不能直接用WeakReference需要封装。使用Instance ID如上文序列化所述存储对象的实例ID在执行或撤销时通过ID查找对象。如果对象已销毁则命令执行失败或自动从历史中移除。及时清理历史在场景切换或游戏状态重大改变时清空命令历史栈。7. 模式变体与在Unity生态中的融合命令模式并非一成不变在Unity项目中它常常与其他模式或Unity特有系统结合演化出更实用的形态。1. 与Unity Event系统结合你可以将命令本身作为一个UnityEvent。这样在Inspector窗口中就可以可视化地配置命令序列非常适合关卡设计、剧情触发和UI交互。// UnityEventCommand.cs public class UnityEventCommand : MonoBehaviour { // 在Inspector中配置的一系列Action public UnityEvent onExecuteEvent; public UnityEvent onUndoEvent; // 注意UnityEvent不支持参数复杂撤销需另寻他法 public void Execute() { onExecuteEvent?.Invoke(); } public void Undo() { onUndoEvent?.Invoke(); } } // 在Inspector中你可以拖拽任何公开方法到 onExecuteEvent 上比如播放动画、激活物体、播放声音等。2. 与ScriptableObject结合打造数据驱动技能这是非常强大的一种用法。你可以为每种技能类型创建一个ScriptableObject作为数据资产命令对象则根据这些资产来执行。// SkillDataSO.cs [CreateAssetMenu(fileName NewSkillData, menuName Game/Skill Data)] public class SkillDataSO : ScriptableObject { public string skillName; public float cooldown; public float manaCost; public GameObject castEffectPrefab; public AnimationClip castAnimation; // ... 其他配置参数 } // DataDrivenSkillCommand.cs public class DataDrivenSkillCommand : ICommand { private SkillDataSO _skillData; private Unit _caster; private Unit _target; public DataDrivenSkillCommand(SkillDataSO data, Unit caster, Unit target) { _skillData data; _caster caster; _target target; } public void Execute() { // 使用 _skillData 中的所有配置来执行技能 Debug.Log($释放 {_skillData.skillName}); // _caster.ConsumeMana(_skillData.manaCost); // GameObject.Instantiate(_skillData.castEffectPrefab, _caster.transform.position, Quaternion.identity); // _caster.GetComponentAnimator().Play(_skillData.castAnimation.name); // ... 应用伤害、效果等 } public void Undo() { /* ... */ } }这样策划或设计师可以在不修改代码的情况下通过创建和配置不同的SkillDataSO资产来创造海量技能。3. 命令总线Command Bus或事件总线Event Bus对于大型项目一个中央化的命令/事件分发系统可以减少耦合。任何系统都可以向总线发送命令任何感兴趣的系统都可以监听并处理特定类型的命令。这更像是“中介者模式”或“观察者模式”与命令模式的结合。// 简化的命令总线示例 public static class CommandBus { private static DictionarySystem.Type, System.ActionICommand _handlers new DictionarySystem.Type, System.ActionICommand(); public static void SubscribeT(System.ActionT handler) where T : ICommand { var type typeof(T); if (!_handlers.ContainsKey(type)) { _handlers[type] (cmd) { }; } _handlers[type] (cmd) handler((T)cmd); } public static void UnsubscribeT(System.ActionT handler) where T : ICommand { /* ... */ } public static void SendT(T command) where T : ICommand { var type typeof(T); if (_handlers.ContainsKey(type)) { _handlers[type]?.Invoke(command); } } } // 使用技能系统订阅攻击命令 CommandBus.SubscribeAttackCommand(OnAttackCommandReceived); // 输入系统发送命令 CommandBus.Send(new AttackCommand(player, enemy));这种方式的优点是极大的灵活性缺点是执行流程不够直观调试起来可能比较困难。命令模式在Unity中就像一把瑞士军刀初看简单但组合和变化无穷。从实现一个简单的撤销功能到构建整个游戏的操作与AI系统它都能提供清晰、可扩展的解决方案。关键在于理解其“封装请求”的核心思想并根据你项目的具体规模与复杂度选择合适的实现变体。避免过度设计在需要解耦、需要记录操作、需要支持复杂队列的地方引入它你的代码库会因此变得更加健壮和优雅。

相关新闻

NBM5100A与PIC18F8722在低功耗物联网设备中的协同设计

NBM5100A与PIC18F8722在低功耗物联网设备中的协同设计

1. NBM5100A与PIC18F8722的协同设计背景在低功耗物联网设备设计中,CR2032等纽扣电池的寿命和电流输出能力一直是关键瓶颈。传统方案中,当设备需要短时大电流(如无线模块发射瞬间)时,电池内阻会导致电压骤降&#xff0c…

2026/7/28 11:01:14 阅读更多 →
如何用DeepSpeed ZeRO-3技术突破万亿参数训练瓶颈:5大策略实现性能飞跃

如何用DeepSpeed ZeRO-3技术突破万亿参数训练瓶颈:5大策略实现性能飞跃

如何用DeepSpeed ZeRO-3技术突破万亿参数训练瓶颈:5大策略实现性能飞跃 【免费下载链接】DeepSpeed DeepSpeed is a deep learning optimization library that makes distributed training and inference easy, efficient, and effective. 项目地址: https://gitc…

2026/7/28 11:00:14 阅读更多 →
5分钟掌握鸣潮工具箱:高效游戏优化的完整解决方案

5分钟掌握鸣潮工具箱:高效游戏优化的完整解决方案

5分钟掌握鸣潮工具箱:高效游戏优化的完整解决方案 【免费下载链接】WaveTools 🧰鸣潮工具箱 项目地址: https://gitcode.com/gh_mirrors/wa/WaveTools 鸣潮工具箱是一款专为《鸣潮》PC玩家设计的游戏优化工具,通过智能画质调节、多账号…

2026/7/28 11:00:14 阅读更多 →

最新新闻

纽扣电池增强方案NBM5100A:延长寿命与提升电流能力

纽扣电池增强方案NBM5100A:延长寿命与提升电流能力

1. 纽扣电池增强方案的核心挑战在物联网设备和便携式电子产品中,CR2032这类纽扣电池因其紧凑尺寸和稳定输出而广受欢迎。但这类电池存在两个致命短板:一是典型容量仅200-240mAh,在连续工作场景下寿命往往不足3个月;二是内部阻抗高…

2026/7/28 11:16:22 阅读更多 →
物联网设备安全芯片SE050的应用与STM32集成实战

物联网设备安全芯片SE050的应用与STM32集成实战

1. 为什么物联网设备需要专用安全芯片?在智能家居和工业物联网项目中,开发者常使用MCU内置的加密算法来实现基础安全功能。但去年某智能门锁厂商的教训让我意识到问题的严重性——他们采用STM32的硬件AES模块进行数据加密,却被黑客通过侧信道…

2026/7/28 11:16:22 阅读更多 →
Zookeeper--08---zk实现分布式锁、案例

Zookeeper--08---zk实现分布式锁、案例

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录zk实现分布式锁1.zk中锁的种类:2.zk如何上读锁3.zk如何上写锁4.⽺群效应可以调整成链式监听。解决这个问题。5.curator实现读写锁分布式锁案例案例分析依…

2026/7/28 11:16:22 阅读更多 →
Keil5 修改STM32单片机项目名称

Keil5 修改STM32单片机项目名称

Keil5 修改STM32单片机项目名称 目录 Keil5 修改STM32单片机项目名称引言步骤1、修改文件夹名称2、删除修改项目文件3、打开项目中修改属性4、修改输出文件名5、编译引言 在做STM32单片机的时候经常要移植工程或者例程。需要修改工程名称,百度的很多方法说的不清楚有…

2026/7/28 11:16:22 阅读更多 →
docker的基本操作命令

docker的基本操作命令

目录:1、镜像相关操作2、镜像复制(cp)3、日志(logs)查询3.1、通过时间点过滤日志4、查看 Docker 版本(version)查看 Docker 客户端和服务端版本信息。5、查看 Docker 系统信息(info)查看 Docker 运行环境信息。6、查看…

2026/7/28 11:16:22 阅读更多 →
计算机毕业设计之基于Javaweb的肇庆旅游系统的设计与实现lw

计算机毕业设计之基于Javaweb的肇庆旅游系统的设计与实现lw

本世纪以来,随着越来越多的人使用网络,互联网得到了极大的发展,各种网络资源呈一个爆发性的增长,越来越多的人通过各种各样的网络工具,例如一些专业旅游的网站,查询各种各样的信息,为了适应社会…

2026/7/28 11:15:22 阅读更多 →

日新闻

告别臃肿!3步让你的暗影精灵笔记本重获新生

告别臃肿!3步让你的暗影精灵笔记本重获新生

告别臃肿!3步让你的暗影精灵笔记本重获新生 【免费下载链接】OmenSuperHub Control Omen laptop performance, fan speeds, and keyboard lighting, and unlock power limits. 项目地址: https://gitcode.com/gh_mirrors/om/OmenSuperHub 你是否也曾为官方Om…

2026/7/28 0:00:43 阅读更多 →
RAG必踩坑!财报法规检索不准?这款开源工具让答案浮出水面,准确率飙升98.7%!

RAG必踩坑!财报法规检索不准?这款开源工具让答案浮出水面,准确率飙升98.7%!

做 RAG 的人应该都踩过这个致命的坑:把几百页的财报、法规、技术手册扔给向量库,问一个具体问题,搜出来的全是沾边但没用的内容 —— 关键信息要么被硬切块拆碎了,要么藏在几十条结果的最下面。语义相似≠真正相关,这个…

2026/7/28 0:00:43 阅读更多 →
抖音视频文案提取工具全指南:免费2026版、手机App、在线工具一网打尽

抖音视频文案提取工具全指南:免费2026版、手机App、在线工具一网打尽

2026年做短视频运营,从抖音上扒文案早就不是偷偷抄笔记的事了。我刚开始做内容的时候,每天刷半小时抖音,手动把爆款视频的口播敲进备忘录,一条2分钟的视频得花十来分钟,碰到语速快的还要反复回听。后来试了一圈工具&am…

2026/7/28 0:00:43 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/27 4:33:59 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/28 8:29:16 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/28 5:03:42 阅读更多 →

月新闻