当前位置: 首页 > news >正文

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 中的核心作用。

http://www.cnnetsun.cn/news/2956248.html

相关文章:

  • 5个秘诀掌握游戏化编程学习:CodeCombat完整实战指南
  • Platinum-MD:终极跨平台MiniDisc音乐管理完整指南
  • 如何在Blender中实现专业级流体模拟?FLIP Fluids插件完全指南
  • 面向对象的三大特征
  • Playwright-MCP:跨浏览器自动化测试与工作流编排实战指南
  • Streamlit机器学习部署:零前端门槛的交互式模型交付方案
  • 供应链成本函数:用经济学思维重构机器学习损失函数
  • AI系统落地的核心不是技术极限,而是价值权衡
  • Go Web应用骨架构建:从Gin、GORM到Zap的现代化实践
  • 从零到一:用Godot卡牌游戏框架轻松打造你的第一款桌游
  • ImageGlass:超越传统图像查看器的终极解决方案,90+格式全支持
  • NXP eIQ Toolkit实战:从TensorFlow/PyTorch模型到嵌入式边缘AI的高效部署
  • OWASP ZAP进阶指南:从自动扫描到手动渗透测试实战
  • 2025-2026全国/一二线全屋定制售后、质保服务品牌测评,终身质保/长期售后/闭店跑路防范、时间陷阱与服务履约避坑指南
  • 非结构化数据连接查询的挑战与BaS算法解析
  • i.MX平台DM-Crypt磁盘加密实战:从DCP硬件加速到OP-TEE安全栈
  • UI-TARS Desktop:如何用AI视觉模型让你的电脑听懂指令的完整指南
  • Motorola Suite56 DSP仿真器:从零上手嵌入式信号处理调试
  • 抖音批量下载终极指南:3分钟学会免费无水印内容批量采集
  • 新手学网安踩无数坑?这份 2026 完整学习路线,零基础从入门到进阶,附带资源与避雷方案
  • QTTabBar终极指南:如何用免费标签页插件拯救你的Windows文件管理混乱
  • 从FLOPS到实际效能:揭秘CPU与GPU算力评估的深层逻辑
  • 从零到一:OpCore Simplify如何用智能自动化重塑黑苹果配置体验
  • 国产高边开关SCT44160:以精准电流感测与智能诊断,重塑多通道负载控制
  • 扣子 3.0 正式上线,但我更关心的是:Agent 做出来之后去哪卖?
  • 为什么你的Figma设计效率提升50%?3个中文界面快速切换秘诀
  • 3天快速上手:用Arduino-ESP32打造你的第一个物联网项目
  • 微生物菌种采购新趋势:如何科学选择优质供应商
  • Navicat Mac版无限试用重置方案:一键解决14天试用限制
  • 零成本搭建企业级营销自动化系统:Mautic完整部署与实战指南