在游戏项目里,系统之间互相调用是很常见的事情。
比如 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 中的核心作用。