Files
youle_framework/docs/client/development-guide/06-子游戏接入模式与Hooks外置.md
T
2026-07-01 23:27:46 +08:00

17 KiB
Raw Blame History

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 → 存在则委托子游戏实现并返回其结果;不存在则执行空操作(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 时什么都不做,语义是「模板未实现该功能」。

// 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 = {};       // 游戏配置

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 前缀以区分。

// SubGameHooks.js 中的写法
SubGameHooks.appStart = function () {
    // 对应 Game_Modify.appStart:委托子游戏的启动编排
};

SubGameHooks.hallAppStart = function () {
    // 对应 gameHallImport.appStart(大厅启动):委托子游戏的大厅初始化
};

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-开发规范与红线,回到 README 查看导航。