系列目录

问题与结论

开发框架 Sample 时,很容易出现一种扩张路径。

最初,示例只需要展示战斗主循环:

  • 接收一次命中;

  • 解析语义信号;

  • 执行伤害;

  • 更新生命值;

  • 判断胜负。

随后,为了让示例更像一款真正的游戏,又不断加入:

  • 输入录制;

  • Command、Undo、Redo 与 Replay;

  • Event 与公告;

  • MVVM 和正式 UGUI;

  • 对象池、资源引用和音效;

  • Save、Preferences、RedDot 与 Localization。

最后,示例确实更完整了,但父包也可能被迫依赖十几个模块。任何只想使用战斗编排内核的项目,都要承担整套 UI、音频、存档和本地化依赖。

这时,Sample 不再只是展示能力,而开始反向改变产品边界。

先说结论:综合展示应该向上组合,而不是向下污染核心

条件组合层(后文简称“够包才升级”):只有当一组可选依赖形成完整闭包时,才装配对应展示能力;否则保持较低档位,而不扩大核心包的硬依赖。

“够包才升级”解决的不是某一个 #if。

它解决的是三个同时存在的问题:

  1. 核心包只承诺最小职责。

  2. 综合 Sample 可以展示真实的跨模块协作。

  3. 消费项目缺少可选包时,仍然可以编译、导入和运行基础路径。

近期的 Semantic Battle Final Showcase 采用了四档结构。

档位 条件能力 展示内容 缺失时的行为
Baseline Core、GAS、Hitbox 等父包基础闭包 命中语义、伤害、状态与基础操作 始终保留
P1 再满足 Bootstrap、Input、Command、Event、Config、Ticker、UI、MVVM 正式 UGUI、命令按钮、撤销重做、Replay、公告与诊断 任一条件不足时回到 Baseline
P2 在 P1 上再满足 Rendering、Pooling、Asset、Audio 反馈队列、池化命中闪光、资源引用计数、程序化音效 条件不足时保持 P1
P3 在 P2 上再满足 Preferences、Save、RedDot、Localization 语言、检查点、未读状态与双语操作面板 条件不足时保持 P2

这里的十六项指 P1—P3 在基础档位之上的条件能力条目,不是父包加上传递依赖后的安装包总数。

真正重要的是:

这组条件能力由 Sample 的展示档位负责,没有因此成为战斗编排父包的硬依赖合同。

证据与分析

Sample 应该展示组合关系,而不是制造第二套框架

综合 Sample 的价值,在于把已经存在的模块放进同一条真实链路。

flowchart LR
    A["Hitbox 命中事实"] --> B["Semantic Combat 语义处理"]
    B --> C["GAS 动作执行"]
    C --> D["Command 历史与 Replay"]
    C --> E["Durable Event 与 Ticker"]
    D --> F["MVVM / UGUI 投影"]
    E --> F
    C --> G["Rendering 反馈队列"]
    G --> H["Pooling / Asset / Audio"]
    D --> I["Save Replay Payload"]
    I --> J["Preferences / RedDot / Localization"]

这条链路应该尽量复用各模块的公开合同。

例如:

  • 战斗状态通过 Command 记录,而不是另写一套 Sample 专用历史系统;

  • 存档保存可重演的命令序列,而不是直接修改战斗对象内部字段;

  • 命中反馈先进入有界队列,再由表现消费者处理;

  • 资源通过引用计数管理,而不是把 Sample 的 Sprite 和 AudioClip 变成全局单例;

  • UI 按钮和键盘输入进入同一条命令管线,而不是各写一套行为。

一旦 Sample 为了方便而绕过正式合同,它展示的就不再是框架用法,而是维护者知道内部细节时才能成立的特殊路径。

为什么不能“有几个包就开几个功能”

看起来最灵活的方案,是逐项检测:

  • 有 Command 就开撤销;

  • 有 UI 就开界面;

  • 有 Audio 就播放声音;

  • 有 Save 就显示存档;

  • 有 Localization 就切语言。

在本 Sample 已选定的组合合同下,独立开关可能制造半成品状态。下列依赖是该示例的设计,不是 Save 必须依赖 Command、UI 必须依赖 MVVM 等普遍规律;其他实现可通过不同的完整闭包成立。

例如:

  • 有 Save,没有 Command Replay,存档到底保存什么;

  • 有 RedDot,没有 Save revision,未读依据从哪里来;

  • 有 Rendering,没有 Pooling,反馈对象由谁回收;

  • 有 UI,没有 MVVM,展示层从哪里读取稳定状态;

  • 有 Preferences,没有 Localization,语言偏好写入后由谁消费。

能力闭包(后文简称“整套才成立”):一组模块只有在其依赖、所有者、数据来源和清理责任全部存在时,才构成可以对外启用的能力。

因此,P1、P2、P3 都不应被理解为若干独立开关。

它们更像逐层建立的合同:

Baseline
  └─ P1:交互与可视化合同完整
       └─ P2:表现反馈合同完整
            └─ P3:跨会话连续性合同完整

任何一层不完整,都保持上一层,而不是进入无法解释的部分启用状态。

运行时生成资源,是一种边界保护

综合 Sample 常见的另一个问题,是把演示资源偷偷变成项目依赖。

例如,示例要求项目预先存在:

  • 指定 Canvas;

  • 特定 Prefab;

  • Addressables Group;

  • AudioMixer;

  • Input Actions;

  • 本地化表;

  • PlayerPrefs 键;

  • 固定存档目录。

这种示例在维护者工程里可以运行,导入干净项目后却会立即失效。

因此,近期展示层采用了更克制的所有权策略:

内容 Sample 的处理方式
UGUI 舞台 运行时创建,Host 销毁时释放
命中闪光 运行时生成并由对象池复用
Sprite / AudioClip Sample 自有瞬态资源,通过引用计数核销
Preferences 命名空间隔离的内存 Store
Save 唯一系统临时目录中的 Replay Payload
输入 Sample 自己的 ActionId 与 Recorder
清理 只删除自己拥有的对象、监听器、槽位和临时目录

这不是生产方案。

它是一种 Sample 边界声明:

我可以证明模块如何协作,但不会擅自替消费项目决定资源、输入、持久化和发布策略。

Dispose 不是收尾细节,而是组合层的反向证明

模块组合越多,清理顺序越重要。

创建路径通常很显眼:

创建 Command
→ 订阅 Event
→ 创建 View
→ 注册按钮
→ 创建反馈池
→ 持有资源
→ 打开存档与偏好

真正容易被忽略的是反向路径:

停止观察
→ 注销监听
→ 清空未读树
→ 释放扩展层
→ 回收池对象
→ 核销资源引用
→ 销毁 View
→ 关闭 UI 状态机
→ 从 Controller 脱离

对称生命周期(后文简称“怎么装上就怎么拆下”):每一项注册、持有、订阅和创建,都存在唯一且可重复执行的反向动作。

综合 Sample 必须验证:

  • Host 销毁后没有残留监听;

  • 重复 Dispose 不会二次释放;

  • 重复运行不会累积按钮回调;

  • 对象池 active 数归零;

  • AssetRef 引用归零;

  • 临时存档和偏好不泄漏到下一次运行;

  • 降级档位不会残留高阶对象。

如果一个组合层只能启动,不能完整退出,它证明的只是“Demo 曾经亮起来”,不是模块能够安全协作。

适用边界与实践

这套方式的代价

条件组合层并不是免费方案。

它会增加:

  • 条件程序集;

  • 最低版本约束;

  • 档位识别;

  • 组合测试;

  • 降级路径;

  • 文档说明;

  • 清理矩阵。

但这些复杂度本来就存在。

区别只在于,是把它们显式放在 Sample 私有组合层,还是隐式转嫁给每一个消费项目。

我的判断是:

对大型框架而言,Sample 的职责不是展示最多功能,而是展示最小核心如何在不改变产品边界的前提下,被逐层组合成完整体验。

设计检查表

准备为一个 Sample 继续增加模块前,可以检查:

  • 新能力属于核心合同,还是只属于展示;

  • 是否会扩大父包硬依赖;

  • 是否存在完整的条件闭包,而不是若干随意开关;

  • 缺少依赖时能否回到明确档位;

  • 是否依赖消费项目已有 Prefab、设置或持久化目录;

  • 输入、Command、Event 和 UI 是否走同一事实源;

  • 每项资源、监听和临时文件是否有明确 Owner;

  • Host 销毁与重复 Dispose 是否有回归测试;

  • Sample 通过是否被错误描述为正式产品成熟度提升。

术语对照

正式术语 通俗称呼 含义
条件组合层 够包才升级 完整依赖闭包满足后才装配对应能力
能力闭包 整套才成立 依赖、数据、Owner 与清理责任共同完整
对称生命周期 怎么装上就怎么拆下 创建和注册均有幂等反向动作
渐进降级 保住上一档 高阶条件不足时稳定回到已成立档位

资料说明:本文依据 2026-08-04 至 2026-08-05 的仓库实现与测试记录整理。文中档位描述是该时间点的工程快照,不等同于相关包已升级为 Supported 或已完成正式发布。

来源与验证状态

本文保留 2026-08-04 至 08-05 的 P1—P3 历史分层。2026-10-26 在 Framework checkout 07f712c04236488879ec44d281695ff0705710c2 读取 Packages/com.unitygame.framework.semantic-combat/Samples~/SemanticBattle/semantic-battle-{showcase,presentation-feedback,persistent-ux}-requirements.json,确认条件闭包与 extends 关系仍可定位;当前条目已含拆包后的 config-core / ui-binding,不能把旧包名当成今天的安装清单。本轮未导入或运行 Sample。历史背景见8 月 5 日审计(内部研究材料)。