# 06 · 子游戏接入模式与 Hooks 外置 > 权威设计来源:`docs/superpowers/specs/2026-06-30-子游戏接入Hooks外置模式-design.md`。 > 本篇面向"要接入新游戏或将老游戏迁移到 Hooks 外置模式"的开发者。 --- ## 1. 两种接入模式 平台通过三个**契约文件**调用子游戏: - `client/js/01_SubGame/00_SubGame_Config.js` —— `Game_Config.*` 配置项 - `client/js/01_SubGame/01_SubGame_modify.js` —— `Game_Modify.*` 部分接口与战绩系统 - `client/js/01_SubGame/02_SubGame_Input.js` —— `gameHallImport.*` 与 `Game_Modify.*` 大部分接口 平台运行时**只按固定全局名调用**这些接口,对实现在哪里、怎么组织毫不关心。子游戏有两种方式实现这些接口: | 维度 | 内联模式(老) | Hooks 外置模式(新) | |------|---------------|---------------------| | 三文件内容 | 配置值 + 业务逻辑全部写在三文件内 | 三文件是纯转发壳,仅查 hook 并委托 | | 业务逻辑位置 | 直接在 `01_/02_SubGame_*.js` 里 | `codes/SubGameHooks.js` + 子游戏实现层 | | `SubGameHooks` | 不定义 | 子游戏填充,与平台接口一一对应 | | 模板可复用性 | 低(新游戏要删改前任代码) | 高(三文件原样复用,只填 hook) | | 平台视角 | 调全局接口直接得到实现 | 调全局接口 → 转发壳 → hook | | 适用时机 | 已有存量代码,维持现状 | 新游戏接入、或老游戏迁移 | **二选一,不混用**:一个游戏的代码库要么是「实心三文件」,要么是「转发壳三文件 + SubGameHooks」。同一接口不允许两处实现。这是**工程级选择**,平台运行期无需任何开关——转发壳的 `if (SubGameHooks.X)` 仅是实现存在性探测,不是模式判断。 > 现有麻将游戏使用内联模式,**本次不迁移**,三文件原样不动。 ### 三文件可改性总表(硬规则) 这三个文件是**平台契约**,可改性严格受限: | 文件 | 子游戏可否改 | 允许 | 禁止 | |------|-------------|------|------| | `00_SubGame_Config.js` | ✅ **仅可改值** | 给**已有** `Game_Config.*` 配置项赋本游戏需要的值 | 新增 / 删除 / 改名配置项;改变项的结构或类型;删除平台定义 | | `01_SubGame_modify.js` | ⚠️ **仅配置区填值,转发壳不碰** | 仅在顶部「配置区」给 `Type_1/Type_2/CreateRoomData/combat/game_config/roomDes` 赋值 | 改任何转发壳;新增接口;写业务逻辑 | | `02_SubGame_Input.js` | ❌ **完全不碰** | —— | 改任何转发壳;新增接口;写任何代码 | 子游戏的**一切业务实现**都在 `codes/SubGameHooks.js` + `codes/` 实现层,**不进这三个文件**;新增前后端交互走 `SubGameHooks` + 服务端 `mod.js` 的 rpc,**不在三文件加接口**。 ### `00_SubGame_Config.js` —— 只改值,不改定义 `00` 是平台配置项的**结构定义**文件,配置项的**集合与结构由平台/框架固定**。子游戏接入只做一件事:**把已有配置项的值改成本游戏需要的**。 ✅ **允许(改值)**: ```js Game_Config.Max.PlayerCnt = 4; // 改人数 Game_Config.Share.title = "进贤麻将"; // 改分享标题 Game_Config.Info.TextContent = ["你好","谢谢","快点","不要","加油","收到","稍等"]; // 改常用语的【值】 Game_Config.Chat.ChatLoc = [[35,517],[1100,315]/* ... */]; // 改聊天气泡坐标【值】 ``` ❌ **禁止(改定义 / 结构)**: ```js Game_Config.Info.MyNewField = 1; // ✗ 新增配置项(增定义) delete Game_Config.Voice; // ✗ 删除配置项(删定义) Game_Config.Max = [4]; // ✗ 改变项的类型/结构 // ✗ 重命名平台已定义的字段(如把 PlayerCnt 改成 playerCount) ``` > **为什么**:平台代码(`00_Surface/*`)按**固定字段名**读取 `Game_Config.*`。增 / 删 / 改名定义会让平台读到 `undefined` 或破坏约定,引发线上故障。**配置项的集合与结构是平台契约,只有「值」属于子游戏**。 > > 对数组类配置项(如 `TextContent`、`ChatLoc`、`isLeft`),改其中元素值属「改值」(允许);但若平台对该数组**长度/索引语义有约定**(如固定 5 个聊天气泡位置、按座位索引),改变长度需以平台读取约定为准,不可随意增删。 --- ## 2. 三层架构 Hooks 外置模式下,职责分为三层,单向向下: ``` 平台契约层(框架维护,子游戏不碰) 00_SubGame_Config.js 平台配置项结构 + 子游戏填【配置值】 01_SubGame_modify.js 平台接口骨架,每个接口是固定【转发壳】 02_SubGame_Input.js 平台接口骨架,每个接口是固定【转发壳】 | | 转发壳查 SubGameHooks.X,存在则委托,不存在则平台默认(B类)/空操作(A类) v 子游戏入口层(codes/,Hooks 外置新方案核心) SubGameHooks{} 全局对象,接口名与平台接口一一对应;子游戏只填需要的 | | SubGameHooks.StartWar = function(_msg){ /* 委托子游戏的开局处理 */ }; v 子游戏实现层(codes/,已有) 各消息处理器 / 收发包 / 战绩等业务模块 / UIManager 扩展 ... ``` **数据流**:平台调 `Game_Modify.X(args)` → 转发壳查 `SubGameHooks.X` → 存在则委托子游戏实现并返回其结果;不存在则执行平台默认(B 类)或空操作(A 类)。 **兼容性原因**:平台只认全局接口名,不关心实现在哪。老游戏不定义 `SubGameHooks`、三文件实心实现 → 照跑;新游戏用转发壳三文件 + `SubGameHooks` → 也跑。无需任何运行期判断。 --- ## 3. 新游戏:使用模板接入 模板文件位于 `gameabc-framework/templates/subgame-entry/`,共四个文件: | 文件 | 内容 | 谁维护 | |------|------|--------| | `00_SubGame_Config.template.js` | `Game_Config.*` 全部配置项结构 + 占位/默认值 | 框架维护 | | `01_SubGame_modify.template.js` | 01 段接口的转发壳 + 配置区占位 | 框架维护 | | `02_SubGame_Input.template.js` | `gameHallImport.*` + 02 段 `Game_Modify.*` 的转发壳 | 框架维护 | | `SubGameHooks.template.js` | 子游戏实现骨架,列出全部可填 hook,每个给空函数 + 用途注释 | 子游戏起点 | ### 接入步骤 1. **复制四个模板文件**到新游戏目录。 2. **三个转发壳直接用,不改**:`00/01/02_SubGame_*.template.js` 复制后重命名,原样放入 `01_SubGame/`。 3. **填充 SubGameHooks**:将 `SubGameHooks.template.js` 重命名为 `codes/SubGameHooks.js`,在其中实现本游戏需要的 hook(不需要的 hook 留空函数或删除,A 类无 hook 默认 no-op,B 类无 hook 返回平台默认值)。 4. **index.html 加载**:按既有顺序在原 `00_/01_/02_SubGame_*.js` 的位置加载新三文件,并在 `codes/` 相应位置加载 `SubGameHooks.js`(在其所依赖的 controllers/handlers 之后)。 ### index.html 加载顺序要点 - **阶段5**:先加载 `SubGameHooks` 所依赖的控制器/管理器/网络等实现模块。 - **`SubGameHooks.js`**:在其依赖之后、三文件之前加载。 - **阶段6**:最后加载受限对接层三文件(转发壳,引用 `SubGameHooks`)。 即:**依赖模块 → `SubGameHooks` → 三文件转发壳**,顺序即依赖,不可颠倒。 --- ## 4. 转发壳范式 转发壳按「无 hook 时的默认行为」分三类,必须**逐一覆盖全部 63 个接口**(见 §6)。 ### A 类 — 纯子游戏行为(无 hook 即 no-op) 无 `SubGameHooks.X` 时什么都不做,语义是「模板未实现该功能」。 ```js // A 类:无 hook 则空操作 Game_Modify.StartWar = function (_msg) { if (window.SubGameHooks && SubGameHooks.StartWar) { return SubGameHooks.StartWar(_msg); } }; Game_Modify.Reconnect = function (_msg) { if (window.SubGameHooks && SubGameHooks.Reconnect) { return SubGameHooks.Reconnect(_msg); } }; ``` ### B 类 — 有平台默认返回值(无 hook 即返回默认) 平台某些接口需要有意义的返回值,无 hook 时应给出合理默认,而非静默返回 `undefined`。 ```js // B 类:无 hook 返回平台默认值 Game_Modify.getMaxPlayerCount = function (roomtype) { if (window.SubGameHooks && SubGameHooks.getMaxPlayerCount) { return SubGameHooks.getMaxPlayerCount(roomtype); } return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4; }; Game_Modify.getLeaveLimit = function (roomtype) { if (window.SubGameHooks && SubGameHooks.getLeaveLimit) { return SubGameHooks.getLeaveLimit(roomtype); } return 10; }; gameHallImport.isInstalled = function () { if (window.SubGameHooks && SubGameHooks.isInstalled) { return SubGameHooks.isInstalled(); } return 1; }; ``` ### C 类 — 配置数据(不是 hook,留配置文件) `Game_Config.*`(00 全部)与房型数据(`Game_Modify.Type_1/Type_2/CreateRoomData/game_config`、`Game_Modify.combat/roomDes`)属于**配置数据**而非行为接口,不进 `SubGameHooks`。 模板在 01 文件顶部设置「配置区」,由子游戏直接填值: ```js // 01_SubGame_modify.js 顶部:配置区(子游戏填值,与转发壳接口段物理分开) Game_Modify.combat = null; // 战绩配置,子游戏按需赋值 Game_Modify.roomDes = ''; // 房间描述 Game_Modify.Type_1 = []; // 房型一级选项 Game_Modify.Type_2 = []; // 房型二级选项 Game_Modify.CreateRoomData = []; // 创建房间数据 Game_Modify.game_config = {}; // 游戏配置 ``` --- ## 5. Hook 命名约定 `SubGameHooks` 的 hook 名**默认与平台接口名严格一致**。 **跨对象同名冲突**:`gameHallImport.*` 与 `Game_Modify.*` 各有一个 `appStart` 接口,两者同名。为避免 `SubGameHooks` 上的 key 冲突,规则如下: | 平台接口 | SubGameHooks key | 说明 | |---------|-----------------|------| | `Game_Modify.appStart` | `SubGameHooks.appStart` | 保持同名 | | `gameHallImport.appStart` | `SubGameHooks.hallAppStart` | 加 `hall` 前缀驼峰 | 目前仅 `appStart` 存在此冲突。`gameHallImport` 侧所有同名接口均加 `hall` 前缀以区分。 ```js // SubGameHooks.js 中的写法 SubGameHooks.appStart = function () { // 对应 Game_Modify.appStart:委托子游戏的启动编排 }; SubGameHooks.hallAppStart = function () { // 对应 gameHallImport.appStart(大厅启动):委托子游戏的大厅初始化 }; ``` --- ## 6. 接口覆盖要求 转发壳必须**逐个覆盖全部平台接口**,遗漏会导致新游戏某平台调用落空(静默失败)。接口分三组,总计 **63 个**: | 来源 | 数量 | 说明 | |------|------|------| | `gameHallImport.*` | 9 | 大厅相关;其中 `appStart` 对应 `SubGameHooks.hallAppStart`(加 `hall` 前缀) | | `Game_Modify.*`(事件/交互,01 段) | 9 | 精灵事件类默认 no-op,改用框架 `SpriteEventController` | | `Game_Modify.*`(生命周期/回调,02 段) | 45 | 多为 A 类 no-op,少量 B 类需给平台默认返回值 | 覆盖原则:**A 类无 hook 即 no-op;B 类无 hook 须返回有意义的平台默认值**(如玩家数、离开上限、房间文案等),而非静默返回 `undefined`。各接口的具体分类与默认值以平台接入 spec 为权威,接入时逐一核对。 --- ## 7. 大块实现外置范式 > 本节为**范式说明**,供新游戏接入时参考外置思路。 内联模式下,一段大块业务实现(如战绩列表/滚动/回放,往往数百行、还自带 `utlmousedown/move/up/drawbegin` 等精灵事件方法)整体写在契约文件里,会导致契约文件膨胀。新游戏接入时,大块业务实现应按以下三步外置: 1. **移到 `codes/`**:把整块实现移到 `codes/` 下对应目录(一个或多个模块文件),声明为全局对象并在 `index.html` 按依赖顺序加载。 2. **经 `SubGameHooks` 接入平台接入点**:相关平台接口在 `SubGameHooks` 对应 hook 里委托给该模块,契约文件不出现业务实现。 3. **精灵交互改用 `SpriteEventController`**:界面的精灵交互改用框架 `SpriteEventController` 按精灵 ID 注册回调,而非散落的 `utl*` 方法。 这样契约文件中只剩干净的配置区与转发壳,该块业务完全在 `codes/` 内自治。 --- ## 8. 老游戏迁移步骤与验证 > 迁移是**单独立项**的改动,不与其他功能混在一次提交里。 ### 迁移步骤 1. **替换接口段为转发壳**:用模板 `01/02_SubGame_*.template.js` 的转发壳替换三文件的接口实现段;保留 `Game_Config.*` 配置值和 `Game_Modify.combat/roomDes/Type_*` 配置数据不动。 2. **逐接口搬到 SubGameHooks**:为每一个原有接口实现在 `codes/SubGameHooks.js` 里创建对应 hook,将内联实现原样移入,逐接口**核对行为等价**。 3. **大块实现按 §7 外置**:战绩等大块逻辑移到 `codes/` 对应目录,通过 hook 接入平台接入点。 4. **验证**: - 全局接口名与签名不变(平台调用路径不变)。 - 逐接口**黑盒对比**迁移前后行为,不能靠"看起来一样",须可观测地验证(页面操作/日志/单测)。 - 连跑多次无异常。 ### 验证要点 | 验证项 | 方法 | |--------|------| | 接口不遗漏 | grep 对比三文件原定义与转发壳一一对应(63 个) | | 签名不变 | 检查平台调用处参数与转发壳参数列表一致 | | 行为等价 | 逐接口黑盒测试(开局/重连/战绩/离开等主流程) | | 配置值完整 | `Game_Config.*` 与配置区数据均已保留 | --- ## 9. 红线 使用 Hooks 外置模式时,必须严格遵守以下约束: | 红线 | 说明 | |------|------| | **转发壳框架维护,子游戏不碰** | `01/02_SubGame_*.js` 转发壳由框架统一维护,子游戏不得直接修改转发壳内容 | | **00 只改值,不改定义** | `00_SubGame_Config.js` 只允许给**已有** `Game_Config.*` 配置项改值;**禁止**新增/删除/改名/改结构定义(平台按固定字段名读取,改定义会引发线上故障)。三文件中仅此文件子游戏可碰,且仅限改值 | | **01 仅配置区填值** | `01_SubGame_modify.js` 只允许在顶部「配置区」给 `Type_*/CreateRoomData/combat/game_config/roomDes` 赋值;转发壳一律不碰 | | **配置值留配置文件** | `Game_Config.*` 和配置区数据(`Type_1/2/CreateRoomData` 等)不得塞进 `SubGameHooks`,保留在对应配置位置 | | **hook 签名必须与平台接口一致** | 平台按位置传参,转发壳以相同参数透传给 hook,hook 签名不得偏移 | | **二选一,不混用** | 同一接口不允许在三文件内联实现与 `SubGameHooks` 中同时存在 | | **转发壳必须全覆盖** | 转发壳须覆盖全部 63 个接口,遗漏会导致平台调用落空(静默失败) | | **严格 ES5** | `SubGameHooks.js` 与所有 codes 文件一律 ES5,禁 `let`/`const`/箭头函数等 | | **加载顺序正确** | `SubGameHooks.js` 须在其依赖的 controllers/handlers 之后、三文件之前加载 | --- ## 10. 小结 - Hooks 外置模式将子游戏逻辑从平台契约文件中**彻底剥离**,三文件退化为纯转发壳,子游戏只需填充 `SubGameHooks`。 - 平台天然兼容两种模式——全局接口名不变,平台无感知。 - 新游戏接入:复制四个模板文件,三转发壳不改,只填 `SubGameHooks.js`,按顺序加入 `index.html`。 - 老游戏迁移:替换接口段为转发壳,逐接口搬实现到 `SubGameHooks`,黑盒验证行为等价。 - 转发壳由框架维护,子游戏不碰;配置值留配置文件;hook 命名默认同接口名,`gameHallImport.appStart` 例外用 `hallAppStart`。 上一篇 [05-开发规范与红线](./05-开发规范与红线.md),回到 [README](./README.md) 查看导航。