Files
erqiwang_youle/docs/client/development-guide/06-子游戏接入模式与Hooks外置.md
T
2026-07-06 17:16:05 +08:00

259 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 是平台配置项的**结构定义**文件,配置项的**集合与结构由平台/框架固定**。子游戏接入只做一件事:**把已有配置项的值改成本游戏需要的**。
- ✅ **允许(改值)**:给已有配置项赋值,如 `Game_Config.Max.PlayerCnt = 4;`、`Game_Config.Share.title = "进贤麻将";`、改 `Game_Config.Info.TextContent`(常用语)/`Game_Config.Chat.ChatLoc`(气泡坐标)等数组元素的【值】。
- ❌ **禁止(改定义 / 结构)**:新增配置项(`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` → 存在则委托子游戏实现并返回其结果;不存在则执行空操作(A)/平台默认返回值(B)/模板默认 UI 渲染(D)。
**兼容性原因**:平台只认全局接口名,不关心实现在哪。老游戏不定义 `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 返回平台默认值,D 类无 hook 由模板渲染可变人数默认 UI)。
4. **index.html 加载**:按既有顺序在原 `00_/01_/02_SubGame_*.js` 的位置加载新三文件,并在 `codes/` 相应位置加载 `SubGameHooks.js`(在其所依赖的 controllers/handlers 之后)。
### index.html 加载顺序要点
- **阶段5**:先加载 `SubGameHooks` 所依赖的控制器/管理器/网络等实现模块。
- **`SubGameHooks.js`**:在其依赖之后、三文件之前加载。
- **阶段6**:最后加载受限对接层三文件(转发壳,引用 `SubGameHooks`)。
即:**依赖模块 → `SubGameHooks` → 三文件转发壳**,顺序即依赖,不可颠倒。
---
## 4. 转发壳范式
转发壳按「无 hook 时的默认行为」分四类,必须**逐一覆盖全部平台接口**(见 §6)。
### A 类 — 纯子游戏行为(无 hook 即 no-op)
无 `SubGameHooks.X` 时什么都不做,语义是「模板未实现该功能」。
```js
// A 类:无 hook 则空操作
Game_Modify.StartWar = function (_msg) {
if (window.SubGameHooks && SubGameHooks.StartWar) {
return SubGameHooks.StartWar(_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;
};
```
`gameHallImport.*` 侧同理(如 `isInstalled` 无 hook 返回 `1`、`getLeaveLimit` 返回 `10`)。
### C 类 — 配置数据(不是 hook,留配置文件)
`Game_Config.*`(00 全部)与房型数据(`Game_Modify.Type_1/Type_2/CreateRoomData/game_config`、`Game_Modify.combat/roomDes`)属于**配置数据**而非行为接口,不进 `SubGameHooks`。
模板在 01 文件顶部设置「配置区」,由子游戏直接填值(与转发壳接口段物理分开):`Game_Modify.combat/roomDes/Type_1/Type_2/CreateRoomData/game_config`。
### D 类 — 模板默认 UI 渲染(可变人数)
少数 UI 接口无 hook 时,模板不止 no-op,而是提供一套**可变人数**的默认渲染(随房间 2/3/4 人自适应各玩家位):`updatePlayerInfoUI`(玩家头像/昵称/分数)、`ShowChat`(桌面文字聊天气泡)、`gameui_play_voice` / `gameui_stop_voice`(桌面语音气泡)。
- 渲染只用平台全局数据(`Desk`/`C_Player`/`Game_Config`),并一律经框架 `SpriteManager` 操作精灵,**不引用 codes、不直接调引擎原语**。
- 座位→显示位的映射(2/3 人时精灵槽 ≠ 物理布局位)由内部助手统一解析,保证头像面板与聊天/语音气泡落在同一玩家位。
- **布局配置挂在 `Game_Modify` 下**(01 配置区,与 C 类配置数据同处):玩家信息用 `Game_Modify.PLAYER_INFO_LAYOUT`、聊天/语音气泡用 `Game_Modify.BUBBLE_LAYOUT`;子游戏可调这些坐标,或用同名 hook 完全接管该 UI。
---
## 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` 前缀以区分。
---
## 6. 接口覆盖要求
转发壳必须**逐个覆盖全部平台接口**,遗漏会导致新游戏某平台调用落空(静默失败)。接口分组如下:
| 来源 | 数量 | 说明 |
|------|------|------|
| `gameHallImport.*` | 9 | 大厅相关;其中 `appStart` 对应 `SubGameHooks.hallAppStart`(加 `hall` 前缀) |
| `Game_Modify.*`(事件/交互,01 段) | 9 | 精灵事件类默认 no-op,改用框架 `SpriteEventController` |
| `Game_Modify.*`(生命周期/回调,02 段) | 45 | 多为 A 类 no-op,少量 B 类需给平台默认返回值 |
| `Game_Modify.*`(桌面聊天/语音气泡) | 3 | `ShowChat` / `gameui_play_voice` / `gameui_stop_voice`;D 类,模板提供可变人数默认渲染 |
前三组共 63 项以平台接入 spec §9 为权威;桌面聊天/语音 3 项由本模板补充转发并提供可变人数默认。
覆盖原则:**A 类无 hook 即 no-op;B 类无 hook 须返回有意义的平台默认值**(如玩家数、离开上限、房间文案),**D 类无 hook 由模板按可变人数渲染默认 UI**(玩家信息/聊天/语音气泡,布局配置在 `Game_Modify`),均不应静默返回 `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 项 + 桌面气泡 3 项) |
| 签名不变 | 检查平台调用处参数与转发壳参数列表一致 |
| 行为等价 | 逐接口黑盒测试(开局/重连/战绩/离开等主流程) |
| 配置值完整 | `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 项 + 桌面聊天/语音气泡 3 项),遗漏会导致平台调用落空(静默失败) |
| **严格 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) 查看导航。