MyFramework:CommandSystem 命令系统的实现解析

📅 2026/6/18 5:03:33
MyFramework:CommandSystem 命令系统的实现解析
在游戏项目里系统之间互相调用是很常见的事情。比如 UI 要关闭窗口角色要移动摄像机要缩放某个对象要播放动画某个系统要延迟执行一段逻辑。最直接的写法当然是调用函数window.setVisible(false); camera.setOrthoSize(5.0f); obj.setPosition(pos);这种写法简单也很直观。但项目复杂以后直接调用会遇到一些问题。有些操作需要延迟执行。有些操作需要等到主线程执行。有些操作需要统一打印日志。有些操作需要执行前回调、执行后回调。有些操作还没有执行接收者就已经销毁了。如果这些逻辑全部散落在业务代码里后期会变得非常难维护。所以在 MyFramework 中我做了一个统一的命令系统CommandSystem。它的核心目的不是为了套一个“命令模式”而是为了把操作的创建、延迟、执行、中断、回收和接收者生命周期统一管理起来。项目地址GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub一、为什么不直接调用函数直接调用函数的问题不在于它不能用而在于它缺少统一生命周期。例如一个窗口执行延迟关闭delay 0.5 秒后关闭窗口如果 0.5 秒还没到窗口已经被销毁了这个延迟逻辑还要不要执行再比如一个角色执行移动命令角色移动到某个位置如果命令还在等待执行角色已经死亡或者离开场景这个命令就不能继续访问原对象。这些问题如果每个系统自己处理就会出现大量重复判断。CommandSystem 的作用就是把这些问题集中起来。一次操作不再只是一次函数调用而是一个带状态的命令对象。二、Command 不是简单的 execute在 MyFramework 中一个 Command 不只是一个execute()函数。它还会保存很多执行相关的信息比如protected CommandReceiver mReceiver; protected float mDelayTime; protected bool mIgnoreTimeScale; protected bool mThreadCommand; protected bool mDelayCommand; protected EXECUTE_STATE mCmdState; protected LOG_LEVEL mCmdLogLevel;这些字段说明一件事Command 是一个带生命周期的操作请求。它不仅知道自己要执行什么还知道谁是接收者是否延迟执行是否忽略时间缩放是否来自线程命令当前是否已经执行是否需要输出日志所以 CommandSystem 处理的不是“函数怎么调用”而是“这个操作应该在什么时间、什么状态下执行以及执行完以后如何清理”。三、CommandReceiver 的作用Command 一般不会孤立存在它通常会绑定一个接收者。这个接收者就是CommandReceiver。可以简单理解为Command 负责描述要做什么 CommandReceiver 负责接收这个命令 CommandSystem 负责什么时候执行这个命令这样做的好处是命令和具体系统之间不会完全写死。比如窗口、摄像机、场景对象、可移动对象都可以作为命令接收者。CommandSystem 不需要知道每个接收者内部具体怎么实现它只负责统一调度命令。四、立即命令的执行流程普通命令进入 CommandSystem 后会走一套统一流程。大致可以理解为创建命令 ↓ 绑定接收者 ↓ 设置命令状态 ↓ 执行开始回调 ↓ 调用 execute ↓ 执行结束回调 ↓ 回收到对象池这和直接调用函数最大的区别是直接调用只关心“执行”。CommandSystem 还关心“执行前”和“执行后”。比如命令执行前可以统一打印日志、记录状态、进入性能采样。命令执行后可以统一回调、清理状态、回收到对象池。这些逻辑如果全部写在业务函数里会非常分散。统一放到 CommandSystem 中命令的行为就会更加可控。五、延迟命令的处理方式CommandSystem 中比较重要的一部分是延迟命令。很多游戏逻辑都需要延迟执行比如延迟关闭窗口延迟播放动画延迟执行引导步骤延迟切换状态延迟发送某个事件如果每个系统都自己维护计时器就会变得很乱。所以延迟命令会进入 CommandSystem 统一管理。流程大致是pushDelayCommand ↓ 加入延迟命令列表 ↓ 每帧更新剩余时间 ↓ 时间到达后加入执行列表 ↓ 统一执行命令 ↓ 执行完回收到对象池这样所有延迟操作都可以通过统一入口推进。业务系统只需要提交命令不需要自己额外维护一套延迟列表。六、为什么需要命令缓冲区在 CommandSystem 中命令并不是随便直接插入正在遍历的列表。因为有些命令可能来自不同调用时机甚至可能来自子线程。如果在主线程遍历命令列表时另一个地方同时往列表里添加或删除命令就可能导致列表状态不稳定。所以 MyFramework 中会把命令缓冲分成不同阶段处理。可以简单理解为输入缓冲 ↓ 主线程同步 ↓ 处理缓冲 ↓ 本帧执行列表输入缓冲负责收集新提交的命令。处理缓冲负责主线程当前正在推进的延迟命令。执行列表负责本帧真正要执行的命令。这样可以避免一边遍历一边修改同一个列表。这也是命令系统里比较重要的一个细节。它不是为了把代码写复杂而是为了让命令提交和命令执行之间有清晰边界。七、命令如何中断延迟命令还有一个问题命令提交以后还没执行之前可能需要取消。比如窗口已经关闭角色已经销毁状态已经切换之前排队的操作已经不再需要所以 CommandSystem 需要支持中断命令。命令对象本身有分配 ID也就是AssignID。通过这个 ID可以找到还在等待中的命令。如果命令还没有进入执行阶段就可以直接移除并回收。如果命令已经进入本帧执行列表就不能随便在遍历过程中删除只能让它失效避免继续访问接收者。这类细节在小项目里不明显但在长期项目里很重要。因为延迟逻辑越多取消和失效处理就越多。如果这些逻辑全部靠业务自己判断很容易漏。八、接收者销毁后如何处理命令CommandSystem 里还有一个非常实际的问题命令的接收者销毁了命令怎么办比如一个 UI 窗口关闭时之前可能还有一些延迟命令没有执行。如果这些命令继续执行就可能访问已经销毁的窗口对象。所以接收者销毁时需要通知 CommandSystem。CommandSystem 会清理和这个接收者相关的未执行命令。可以理解为接收者销毁 ↓ 通知 CommandSystem ↓ 查找等待中的命令 ↓ 移除属于该接收者的命令 ↓ 已经进入执行列表的命令让其失效这样可以避免很多延迟调用导致的空引用问题。这也是 CommandSystem 比直接延迟调用函数更安全的地方。它知道命令属于谁也能在接收者销毁时统一处理。九、命令对象也走对象池Command 本身也是对象。如果每次执行命令都 new 一个对象用完就丢给 GC那高频命令下也会产生额外开销。所以 MyFramework 中的 Command 也会走对象池。这正好和上一篇 ClassPool 的设计接上。命令执行完以后会被回收到对象池中。如果是主线程命令就回收到主线程 ClassPool。如果是线程命令就回收到线程安全对象池。这意味着 Command 也必须正确实现resetProperty。因为命令对象下一次还会被复用。它需要清理接收者延迟时间回调执行状态日志等级线程命令标记延迟命令标记执行结果否则下次从对象池取出命令时就可能带着上一次的残留状态。这再次说明对象池复用的核心不是“对象回收了没有”而是“对象回收前有没有清干净”。十、CommandSystem 和其他系统的关系CommandSystem 并不是孤立模块。它和 MyFramework 里的很多系统都有关系。例如 GlobalTouchSystem 判断某个对象被点击以后后续可以通过命令去驱动 UI 或对象行为。比如点击按钮 ↓ GlobalTouchSystem 判断按钮是否能响应 ↓ 按钮逻辑提交命令 ↓ CommandSystem 统一执行ClassPool 则负责命令对象的创建和回收。所以这几个模块之间可以形成一个完整链路GlobalTouchSystem 负责判断谁响应输入 CommandSystem 负责统一执行操作 ClassPool 负责命令对象生命周期这也是框架设计里比较重要的一点。一个系统不应该把所有事情都做完。GlobalTouchSystem 不负责具体业务操作。CommandSystem 不负责判断点击命中。ClassPool 不关心命令语义。每个系统只负责自己的边界。十一、这套方案解决的具体问题CommandSystem 解决的不是“怎么把函数调用包装起来”。它主要解决的是操作执行过程中的生命周期问题。具体包括操作可以统一提交操作可以立即执行操作可以延迟执行延迟操作可以在统一 update 中推进命令可以记录执行状态命令可以绑定接收者接收者销毁后可以清理相关命令未执行的延迟命令可以被中断命令执行前后可以有统一回调命令执行日志可以统一控制命令对象可以通过对象池复用这些能力如果分散在各个业务系统里每个地方都要重复写一遍。而 CommandSystem 的价值就是把这些通用流程收敛到框架层。结语CommandSystem 的价值不是为了把简单函数调用变复杂。如果只是单纯调用一个函数直接调用当然更简单。但在长期游戏项目里很多操作都不只是“立刻执行一下”这么简单。它可能需要延迟。可能需要取消。可能需要等到主线程。可能需要知道接收者是否已经销毁。可能需要执行前后回调。可能需要统一日志。可能还需要对象池回收。所以 MyFramework 中的 CommandSystem本质上是给操作请求加上了一套生命周期。它把操作从一行函数调用变成一个可管理的命令对象。这样做的目的不是形式上套设计模式而是让跨系统操作、延迟操作和可取消操作都能被框架统一管理。这就是 CommandSystem 在 MyFramework 中的核心作用。