16 KiB
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 = [[35,517],[1100,315]/* ... */]; // 改聊天气泡坐标【值】
❌ 禁止(改定义 / 结构):
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,每个给空函数 + 用途注释 | 子游戏起点 |
接入步骤
- 复制四个模板文件到新游戏目录。
- 三个转发壳直接用,不改:
00/01/02_SubGame_*.template.js复制后重命名,原样放入01_SubGame/。 - 填充 SubGameHooks:将
SubGameHooks.template.js重命名为codes/SubGameHooks.js,在其中实现本游戏需要的 hook(不需要的 hook 留空函数或删除,A 类无 hook 默认 no-op,B 类无 hook 返回平台默认值)。 - 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 时什么都不做,语义是「模板未实现该功能」。
// 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。
// 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 文件顶部设置「配置区」,由子游戏直接填值:
// 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 前缀以区分。
// 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 等精灵事件方法)整体写在契约文件里,会导致契约文件膨胀。新游戏接入时,大块业务实现应按以下三步外置:
- 移到
codes/:把整块实现移到codes/下对应目录(一个或多个模块文件),声明为全局对象并在index.html按依赖顺序加载。 - 经
SubGameHooks接入平台接入点:相关平台接口在SubGameHooks对应 hook 里委托给该模块,契约文件不出现业务实现。 - 精灵交互改用
SpriteEventController:界面的精灵交互改用框架SpriteEventController按精灵 ID 注册回调,而非散落的utl*方法。
这样契约文件中只剩干净的配置区与转发壳,该块业务完全在 codes/ 内自治。
8. 老游戏迁移步骤与验证
迁移是单独立项的改动,不与其他功能混在一次提交里。
迁移步骤
-
替换接口段为转发壳:用模板
01/02_SubGame_*.template.js的转发壳替换三文件的接口实现段;保留Game_Config.*配置值和Game_Modify.combat/roomDes/Type_*配置数据不动。 -
逐接口搬到 SubGameHooks:为每一个原有接口实现在
codes/SubGameHooks.js里创建对应 hook,将内联实现原样移入,逐接口核对行为等价。 -
大块实现按 §7 外置:战绩等大块逻辑移到
codes/对应目录,通过 hook 接入平台接入点。 -
验证:
- 全局接口名与签名不变(平台调用路径不变)。
- 逐接口黑盒对比迁移前后行为,不能靠"看起来一样",须可观测地验证(页面操作/日志/单测)。
- 连跑多次无异常。
验证要点
| 验证项 | 方法 |
|---|---|
| 接口不遗漏 | 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-开发规范与红线,回到 README 查看导航。