Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
34 KiB
房间流程审查与架构重构建议
日期:2026-09-08。状态:源码审查与设计草案,尚未实施重构。
后续详细文档:架构设计、原接口迁移清单、实施计划。本文件保留审查依据和方案评估;具体目标契约以详细设计文档为准。
1. 结论与边界
建议采用“纯 TypeScript 房间核心 + 子游戏接入契约 + Cocos 展示适配 + 应用组合入口”,沿现有实现分阶段迁移。保留已有网络、会话隔离和不可变状态的有效部分,不整体推倒重写。
“零耦合”应定义为:平台不引用具体子游戏,子游戏不引用平台内部实现,核心不引用 Cocos/浏览器/原生全局对象;双方依赖共享契约。组件之间仍有显式、单向的契约依赖,不能承诺完全没有依赖。
硬约束:服务器零修改;现有 route/rpc/字段与缺省语义不变;roomtype/deskinfo/deskfree 的业务解释只属于子游戏;不通过前端猜值、补位、强制类型转换掩盖错误。平台可以验证信封与 JSON 可传输性,但不能限定子游戏配置为数组或特定位串。
范围:原工程房间平台流程、当前 Cocos 对应代码、接入契约、生命周期、公共界面与性能策略。二七王的牌型、叫分、出牌、计分、结算画面仍是独立玩法任务,本草案不声称这些行为已实现。
2. 审查依据
- 原工程:07_Desk.js、09_Net.js、11_GameUI.js、12_Logic.js、08_Utl_Output.js。
- 协议:框架架构、游戏内协议与桥接、子游戏开发模式。文档与源码不一致时必须记录差异,不把旧建议当成已实现行为。
- 新工程:
assets/framework/platform/{runtime,runtime-session,game-host-adapter}.ts、platform/stores/、protocol/、sdk/、assets/scripts/platform-login/,路径均相对于cocoscreator_projects/YouleNexus/。 - 服务端只读核对:
server/youle/server_room/class.room.js的get_playerlist按完整座位列表返回玩家/空位,空位为 null;不能从当前人数推断座位容量。
3. 原工程实际流程
3.1 消息入口和登录屏障
12_Logic.js 约 238–263 行:等待登录回包期间,普通消息被登录屏障挡住,踢出消息保留处理;平台、代理商、房间 route 交给 Net[rpc],其余交给 Game_Modify._ReceiveData。网络打开只表示连接成功,不代表登录成功或可以处理房间命令。
新架构应保留连接、认证、房间、对局的不同阶段。当前游戏 route 可以显式注册,但遇到非预期 route 要有诊断,不能擅自把任意未知包发送给游戏。
3.2 创建房间
Net.create_room(09_Net.js:111)先调用 Desk.create_room,再结束加载并调用子游戏 createRoom(roomtype,infinite)、可选的 onCreateRoom(data)。
Desk.create_room(07_Desk.js:456)成功路径:子游戏 onCreateDesk → 根据子游戏人数初始化牌桌 → 初始化房间状态 → 写入房主、自身座位和房间配置 → 保存上次配置 → 打开主场景 → 房间描述与准备/开始界面 → 原生房间能力 → mainSceneLoaded。
注意原 Net 包装层的创建钩子写在成功判断之外。迁移时应明确区分“收到结果”与“创建成功”,不能把失败响应当成功打开房间;如某子游戏依赖失败结果通知,应以明确结果事件保留,而非产生半初始化房间。
本次已定位的错误来自资源装配:二七王创建 prefab 同时挂载模板和二七王两个 CreateRoomPage 实现,基类查询取到模板。仅验证 roomtype 编码器的单元测试无法覆盖这种错误。
3.3 登录恢复已有房间
Desk.login(07_Desk.js:282–425):
- 按 roomtype 调用子游戏
onCreateDesk,再由子游戏规则或明确的游戏人数配置创建完整座位。 - 写入房间模式、局数、开战条件、玩家列表、自己的座位和准备/在线状态;
isbattle用于房间阶段。 - 打开主场景,恢复描述、准备/退出状态和房主留言。
- 存在
agreefree时恢复投票状态和剩余倒计时。 mainSceneLoaded()后,有 truthydeskinfo执行Reconnect(deskinfo),否则执行ReconnectNoMakewar()。
没有房间则关闭游戏场景并进入大厅。恢复对局的判断条件不是 isbattle === 1。
文档勘误:05-游戏内协议与桥接.md 第 6 节将 07_Desk.js:419–423 标为 self_join_room,实际属于 Desk.login。设计必须以这里的真实分支为准。
3.4 主动加入房间
Desk.self_join_room(07_Desk.js:531,关键分支 629–685):写入房间和玩家 → 打开主界面 → myJoinRoom(整包) → 三分支:
- 有
deskwar:更新阶段和退出权限,调用StartWar(整包)。 - 无
deskwar但有deskinfo:调用DeskInfo(deskinfo)。 - 两者均无:进入等待/准备界面。
再处理准备按钮、视频等,最后 mainSceneLoaded。这与登录恢复的钩子、传参、历史调用时点都不同,不能统一为不带来源的 restore(snapshot)。
新实现可以用新的生命周期命名,但适配层必须明确保留以上语义;“界面可用”和“原 mainSceneLoaded 通知”不能在迁移时想当然地视为同一个钩子。
3.5 成员与座位
other_join_room 更新玩家后分别通知平台 UI 和子游戏 playerJoinRoom,且可能携带 deskwar;other_exit_room 更新座位后通知 playerLeaveRoom。上下线也各自更新状态并通知子游戏。换座属于明确状态变化,不能只移动 UI。
内部以服务器座位为唯一索引,视角换算在展示边界进行。保留自己的服务器座位;聊天、投票、头像、语音效果不得各自复制视角公式。空位、已占用人数、总座位数是不同概念。
3.6 准备与开战
GameUI.showStartButton(11_GameUI.js:5304)组合准备人数、makewar、stage、自身开始权限和房间模式;showReady 看 needprepare 与自身准备状态。这些是来源驱动的公共规则,不应由各个按钮自己判断。
开战入口至少包括:self_makewar、makewar、self_join_room.deskwar、other_join_room.deskwar、player_prepare.deskwar。player_prepare 先更新准备并调用 onReady(seat),随后可调用 StartWar(整包)(07_Desk.js:1128)。
平台负责准备/阶段/公共操作权限,游戏负责解释开战载荷及牌局状态。不能丢失触发开战的原始包,也不能把它等同于登录 deskinfo。
3.7 房间公共功能
- 聊天:普通文本、常用语索引、表情、语音、公告的路径不同。常用语与声音资源由游戏配置提供,历史消息由平台管理,具体气泡位置通过座位视角映射。
- 广播:即时提示与排队滚动公告不同,不应全部变成聊天记录。
- 设置/设备:音效、语音、摇一摇、定位、电量、网络等通过能力适配器接入;浏览器默认行为应在浏览器适配器定义,不能散落在每个 UI 组件。
- 数字输入/倍率选择:平台负责展示和输入结果,数值的玩法含义、允许范围、选择后发什么包由子游戏负责。
- UI:菜单布局与条目数量由已确定的展示模型驱动,预制体与 ScrollView 只负责展示,不读取网络包。
3.8 解散、结算与退出
self/other_apply_free_room 恢复投票数组、申请者和服务端倒计时;同意/拒绝分别更新,最终是否解散以服务端结果为准,客户端不根据本地票数自行宣布成功。
free_room(07_Desk.js:866)分支:freeNow 立即交给子游戏 Free(deskfree);普通通过先展示投票结果。CloseApplyResult(11_GameUI.js:5029)确认后,有 deskfree 转给子游戏结算,没有则清空房间返回大厅。结算可能仍需要房间/玩家上下文,不能收到 free_room 就销毁全部对象。
退出、房主直接解散、他人解散、开战后申请解散、无限局退出不是同一个操作。命令选择交给统一策略,按钮不复制条件。服务端确认后才提交权威离房结果;超时不能自行推断已经退出。
3.9 重连、换服与资源释放
换服涉及携带原指令重发,不能一概变成再次登录;解散过程中换回代理商服仍可能需要保留结算模块。踢出属于终止路径,不能自动重连。
离房需要释放房间订阅、计时器、声音/语音、原生回调、弹窗、头像资源、异步加载回调和游戏实例。原工程靠多个全局函数共同完成;新工程应交给一个房间生命周期作用域统一管理。
4. 当前 Cocos 的具体问题与可保留部分
4.1 高优先级:流程和边界
- 公共启动类依赖具体游戏。 PlatformStartup.ts:208–209、439 直接按 gameKey 选择工厂和 View,新增游戏要修改公共启动脚本。应迁移到应用组合入口,平台只接收装配好的契约对象。
- 资源装配缺乏唯一性验证。 最近的双创建脚本已修复,但仍需构建检查:每个创建 prefab 恰好一个 CreateRoomPage 实现,且与游戏注册项匹配;不能依赖 getComponent 返回第一个。
- 异步就绪契约缺口。 runtime.ts 的 SceneOrderedGameSessionHost 调用 showRoom 后立即 restore;PlatformStartup.ts:243 将异步 showRoom 以 void 调用。调用顺序不等于场景加载完成顺序。实际影响取决于游戏是否立即访问 View,但该契约本身不能保证就绪。
- 入口语义被压缩。 RuntimeSession.handleSelfJoin 将 parsed.reconnect 交给与登录相同的 openCommittedRoom/restore;GameModule 只有 restore(unknown),PlatformToGameEvent 的 room.entered 无来源。需要保留登录恢复、加入快照、加入开战、创建结果之间的区别。
- 开战桥接不完整。 当前 FirstSliceRpc 未包含 self_makewar/makewar;player_prepare、other_join_room 处理只发布 seat 事件,没有传递 deskwar 启动语义。补全前必须逐项对照协议与原钩子。
- 配置边界还可进一步收紧职责。 公共 Roomtype 类型目前是 string | readonly unknown[];应表达不透明、可传输 JSON 数据,业务格式由游戏拥有。room-chat-config.ts 还按具体游戏键定义游戏内容,应由游戏装配注入。
4.2 性能问题:源码可确认的工作量
- RoomChatPanel.ts:61 起,每次历史引用变化都会销毁所有历史节点,重新创建全部行并请求头像;即便只追加一条消息也如此。先做稳定消息标识和增量追加,消息较多时再接可变行高虚拟列表。
- RoomUi.avatar 每次创建 Texture2D/SpriteFrame,没有共享的 URL 资源租约。应在头像资源适配器内请求去重、引用计数与容量上限,释放由房间/行作用域负责。
- 聊天每次强制滚到底,影响用户阅读历史。只有原先已靠近底部或用户明确发送消息时自动滚动,否则显示未读提示。
- ApplyVotePanel 已按 seat 复用行,selectors 已缓存快照,GameHost 已有会话失效控制。这些应保留,不应为了统一架构而退化成每次重绘全部 UI。
- 房间对象通常只有少量座位,没有证据支持直接引入 ECS、Worker、多线程或全局事件总线。当前没有采样数据,不能承诺具体帧率或“提升多少倍”。
4.3 现有基础值得保留
WireClient 连接代际与重连机制、Router、平台命令入口、不可变状态、选择器缓存、GameHost 租约、销毁幂等处理,以及已有协议与流程测试,均可成为迁移底座。
边界扫描目前主要遍历 framework 与 games,scripts/platform-login 中的具体游戏依赖没有被同样覆盖。重构后应单独允许 app 组合根依赖两侧,其余 UI/核心一律按规则检查。
5. 推荐架构
5.1 模块职责与依赖方向
应用组合入口负责将 GameDefinition、资源、平台服务连接起来,是唯一知道具体游戏的位置。
共享契约定义不透明载荷、公共房间快照、命令、游戏入口、生命周期与能力端口,不引入 Cocos。
房间应用层负责登录/创建/加入/恢复/开战/离开等流程编排;房间状态层只处理平台拥有的状态;协议适配层负责公共字段解析、服务端差异与出站信封;游戏模块负责玩法载荷解释和自己的状态。
Cocos 适配层拥有节点、prefab、资源加载、动画、列表和输入;公共 UI 消费展示模型并发命令,不访问 Store 内部和 WebSocket。原生/浏览器实现使用同一能力端口。
建议先在仓库内保持模块化单体,边界稳定后再决定是否提取 npm 包。不要在此次重构同时引入微服务式消息架构。
5.2 GameDefinition 统一装配
一个游戏注册项统一提供:key/协议 route/身份配置关联、模块工厂、创建页面、房间展示工厂、资源清单、座位规则、聊天内容和平台功能策略。
编译期检查 TypeScript 契约,构建期检查资源引用与页面唯一性,启动时验证实际装配。平台不写 if(gameKey==='erqiwang'),也不通过节点名猜测游戏种类。
创建流程:游戏页面选项 → 游戏编码器生成 roomtype → 公共命令封装原样传输。上次配置由游戏解释;跨版本迁移必须由游戏定义版本与迁移规则,不能平台发现错误数组就补成字符串。
5.3 统一生命周期,保留不同业务入口
连接状态、房间生命周期、对局阶段、投票/结算状态分开建模,避免一个枚举混合所有笛卡尔组合。
房间生命周期建议:outside → entering → active,重连时进入 recovering,离开时进入 leaving;无法恢复的失败进入明确 failed 状态。平台 active 不代表牌局已开战。
每次进入/替换房间生成本地 session generation。异步场景/资源/原生回调必须验证其仍属于当前 generation;旧结果只释放自身资源,不得挂到新房间。
进入流程:解析来源 → 生成带来源的进入描述 → 提交公共状态 → 创建房间作用域 → 初始化游戏模型和挂载界面 → await 明确的就绪契约 → 按 create/login/join 语义调用游戏入口 → 按序处理等待消息。具体 model/view 初始化顺序由统一契约定义,不要求子游戏猜测何时有节点。
等待加载期间,只对房间业务消息设置有界 FIFO;踢出、断线、取消等控制信号不能被慢加载堵住。服务端没有序列号时不能凭 rpc 名去重或丢弃重复牌局事件。超过队列限额应显式报错并按既定恢复流程重新获取权威状态,不编造数据。
子游戏入口至少区分:创建成功、登录恢复有快照、登录恢复无快照、加入普通房间、加入已有快照、加入即开战、准备/成员变更触发开战、解散结算。传参保留原整包或原快照的区别,不能丢字段后让游戏反查全局变量。
5.4 状态、命令和事件
平台拥有成员、座位、在线/准备、公共模式与权限、聊天/公告、解散投票;子游戏拥有手牌、轮次、行动者、分数规则和玩法结算。roomtype/deskinfo 是来源载荷,不建立第二份平台解析结果。
同一入站消息的顺序固定为:解析公共契约 → 原子更新平台状态 → 按明确顺序通知子游戏事件 → 发布展示变更。展示层允许同帧合并绘制;业务事件不能合并或丢弃。
命令走具名端口;状态订阅用于持续事实;瞬时事件用于一次性效果。不要用全局字符串 EventBus 代替所有方法调用。错误包括操作阶段与原始原因,不能只有外层 RuntimeSessionFault。
5.5 公共能力与生命周期作用域
根据需求拆成 RoomCommands、RoomSnapshotReader、GameMessagePort、NumericInputPort、AudioPort、DeviceStatusPort 等稳定小接口,不将整个 PlatformRuntime 或 Cocos Node 注入游戏逻辑。
作用域统一登记订阅、计时器、异步任务、资源租约和面板;进入失败回滚与正常离开调用同一释放逻辑。依赖顺序明确:游戏停止使用能力后再释放能力。结算上下文存活到子游戏明确结束展示。
纯展示失败与协议/状态损坏分级处理:头像失败不应停止整条 WebSocket;房间状态不一致必须显式失败,不能吞掉。哪些失败可局部恢复由能力契约规定,不靠任意 try/catch。
5.6 性能目标与测量
先记录基线,再确定绝对耗时目标:房间进入耗时、重连到可交互耗时、主线程 p95、长任务、活跃节点数、资源数量、历史追加成本、重复进退后的订阅/计时器计数。
可先确立结构性验收:追加一条聊天不重建已有行;重复头像不重复建纹理;同帧同一展示模型最多刷新一次;隐藏面板不做无效布局;100 次进入/退出后作用域资源计数回到基线。真实渲染性能必须在目标设备与 Cocos 预览/构建验证。
倒计时基于服务端剩余时间建立本地截止时间,显示按差值计算;暂停/恢复后重新计算,而非定时器每跳一次就减一。动画只属于展示,不作为权威业务阶段推进条件。
6. 方案比较与迁移顺序
方案 A:只拆 PlatformStartup 与 UI 文件。风险小,但生命周期和入口语义问题仍在,不推荐作为最终方案。
方案 B:在现有基础上提取房间编排器、明确 GameDefinition 和能力端口,逐条迁移原流程。可以用现有测试与包回放验证,推荐。
方案 C:整体改成 ECS/全局响应流/通用插件系统。改动面大,当前没有负载证据证明收益,不推荐。
推荐分期,每一期都应能独立运行并回归:
- P0 协议与流程基线:建立原函数 → 入站包 → 公共状态变化 → 游戏钩子 → UI → 清理的映射;补齐创建/加入/登录恢复/开战分支夹具;记录文档勘误;自动检查资源唯一性。
- P1 装配边界:将游戏工厂、View、聊天/策略配置移到游戏注册项;收紧 import 检查;确保创建页面到真实 create_room 发包全链验证,不只测试编码器。
- P2 生命周期与事件:引入可等待的界面就绪、房间 generation 和有限消息队列;保留进入来源;补齐开战事件;验证断线/换服/取消与释放。
- P3 公共能力与增量展示:菜单、聊天、投票、HUD、广播、数字工具和原生能力按作用域接入;优化聊天与头像,使用测量决定是否虚拟化。
- P4 游戏接入验收:用模板测试模块和二七王接入验证新增游戏无需修改平台核心;玩法未实现部分单列,不能以空 restore 通过代替真实牌局恢复。
7. 必须覆盖的回归场景
- 创建页面 32 种二七王选项到按钮提交、平台命令、WebSocket 文本;模板仍发送自己的数组。原始值和类型保持一致。
- 登录无房间、有房间无快照、有快照、投票中、连接后登录被拒绝。
- 加入无快照、有 deskinfo、有 deskwar;other_join/player_prepare 携带 deskwar;self_makewar/makewar。
- 准备确认前后、重复点击、人数不足、无限局、换座、上线/离线。
- 加载中断线/退出/踢出、连续换房、旧异步回调晚到、加载失败回滚;订阅与计时器不泄漏。
- 解散拒绝、通过后确认、freeNow、有/无 deskfree、换服与 free_room 不同到达顺序;结算完成前上下文可用。
- 聊天动态追加、历史滚动位置、头像请求去重、后台恢复倒计时、浏览器原生替身。
- 房间玩法包保持 FIFO;故障日志保留操作阶段与原始原因;敏感完整包日志只在受控调试配置下开启。
8. 尚待确认
第一阶段优先平台框架与契约,还是同时进入二七王玩法接入;目标设备、希望覆盖的最大聊天历史量与性能基线;旧缓存/已经创建的错误 roomtype 房间如何由用户显式处理。
本轮已完成上述重点路径的源码追踪;尚未逐一穷尽原工程所有菜单分支、支付/VIP/百人场细节,也未做真机性能采样。进入实施前,应在 P0 将本项目支持的房间模式与 RPC 范围冻结,避免把未审查支线默认为已兼容。
9. 第二轮评估:是否高效、现代、低耦合、容易接入
评估结论:方案方向正确,但第一版还不是可直接冻结的 SDK 设计。它有良好的分层基础,却仍缺少游戏事实回传、接口可选性、同步/异步边界、回放隔离和开发者接入流程的精确定义。不能仅凭使用 TypeScript、接口和状态机就称为高性能、易扩展。
9.1 优点
- 组合入口替换具体游戏分支,可以消除公共启动类随游戏数量增长而持续修改的问题。
- roomtype 和玩法快照不透明,保留服务器契约,支持不同游戏独立演进编码方式。
- 领域逻辑与 Cocos 分开,协议回放、状态迁移、错误路径可以在无编辑器条件下测试。
- 房间作用域、就绪约束与代际保护能够统一处理旧回调晚到、离房泄漏、半挂载状态。
- 逐步迁移可以复用现有测试、选择器缓存与会话租约,风险小于整体重写。
9.2 成本与潜在缺点
- 抽象数量可能过多。 为每个 Utl 函数单独建 Service/Port,会使接入比原工程更繁琐。按职责聚合为少量开发者可理解的能力组,内部实现可以再拆分。
- 状态和事件可能重复触发。 如果游戏同时订阅公共快照并在每个成员事件里全量 render,就会双重更新。快照负责显示,事件负责必须保序的一次性业务动作;约定一次提交一次可观察版本。
- 异步化可能阻塞业务。 场景加载、资源、用户输入需要异步;普通玩法消息处理不应等待动画结束。游戏状态立即推进,展示动画可以排队/取消,慢动画不能挡住服务器包。
- 复制和冻结有成本。 不能每个 listener 都深复制整个房间或 deskinfo;入口进行必要的所有权隔离,后续通过不可变引用、结构共享和选择器订阅传播。数据真实性优先,是否改变冻结策略以采样为依据。
- 装配清单可能成为另一份重复配置。 若 GameDefinition 和 SubgameAssets 都手写 gameKey、route、prefab,就会形成双源。应只有一个权威定义,编辑器组件仅引用该定义或由它生成,并做资源校验。
- 过早插件化会增加复杂度。 第一阶段不做热插拔远程插件、通用依赖注入容器或运行时反射发现;显式工厂和模块引用足够。
- 服务器协议没有 requestId 时,不能伪造请求关联。 prepare/exit 等冲突命令按房间串行限制;本地 pending 标识只管理 UI,不得加到网络包。超时不代表服务器未执行,也不自动重发非幂等动作。
9.3 对四个目标的明确判断
高效:能够降低全量刷新和资源重复创建,但实际性能尚未测量。架构主要解决正确性与扩展成本,性能收益来自具体的数据更新与资源策略,不来自类的数量。
现代化:具备显式生命周期、依赖反转、类型契约、状态驱动和可测试性,是适合当前项目的现代化方向;不需要以 ECS、全局响应流、Worker 或微内核作为现代化的证明。
零耦合:无法实现“没有任何耦合”;可以严格实现零具体游戏依赖、零跨层内部访问、零全局状态写入、零引擎依赖的纯逻辑核心。对业务契约的依赖是必要且应稳定的。
接入方便:取决于最终是否做到“声明一个游戏、实现少数必需入口、提供页面和资源,平台不改”。如果最终要求补齐几十个空钩子或理解平台全部状态机,则未达到目标。
10. 两个原接口文件的专项审查
审查对象:projects/Game_Surface_3/js/01_SubGame/02_SubGame_Input.js 与 projects/Game_Surface_3/js/00_Surface/08_Utl_Output.js。以下为实际代码行为,不仅依据注释。
10.1 Input 不是单纯输入接口,而是混合的回调表
其中包含大厅宿主 gameHallImport、网络包接收、创建/加入/重连、成员变化、语音/电话、设备摇动、界面进入/退出、游戏配置查询、输入结果、房间描述、VIP 和回放。文件名无法表达这些职责。
存在三类接口混在同一个全局 Game_Modify 对象:
- 命令式通知:StartWar、Reconnect、Free、myJoinRoom、onReady。
- 同步查询:getRoomInfo、getFullRoomInfo、getRoomMode、getMult、getLeaveLimit。
- 有副作用的事件处理:shakeEvent 读取 GameData/Desk/C_Player,手工拼身份和房号,发送 Net.Send_self_makewar,再停止原生摇动。
不少查询桩返回固定示例值:getStarLimit 返回 2001000,getMult 返回 10000,getLeaveLimit 返回 10,getRoomTopDescAry 返回具体游戏描述。将这些默认值当成通用框架契约,会使未完成接入呈现为“看起来能运行”。新接口必需项必须明确提供;可选能力只有声明的缺省语义,不能返回虚构玩法值。
钩子传参不统一:StartWar/myJoinRoom 收整包,Reconnect/DeskInfo 收快照,createRoom 收两个标量,onCreateRoom 收 data。应保留语义和原始载荷,但由游戏适配器规范到它自己的内部事件,不要求每个游戏到处记忆历史签名。
10.2 Utl 同时是查询器、状态写入器、网络代理和 UI 控制器
已验证的具体问题:
getMyInfo用 clone 返回副本,而getPlayerInfoBySeat(923)与getPlayerList(942)直接返回 Desk 中对象/数组。调用者很难知道自己是否可以修改结果,隐藏写入绕过任何状态通知。clone(154)对所有对象使用 new Object,并遍历 for-in;数组不会被保留为数组,也没有循环处理。不适合作为新系统 JSON/状态所有权机制。setGrade(170)同时做座位换算、写分数标签与基于精灵尺寸/字体宽度的布局。游戏业务调用一个“设置分数”函数却绑定了具体 UI 编号。changeBean(453)按 initialBean 加变化值直接写玩家 bean,并更新资料弹窗。玩法分数展示与账户资产更新来源混杂。setPlayerPrepare(757)写 Desk、C_Player,再更新 UI;setDeskStage(794)又根据无限局修改状态和按钮。这些 setter 暴露内部结构,让调用顺序成为隐含契约。getOnState(342)从 GameUI.onstate 读取,isMainScene从精灵可见性读取。展示状态反过来成为业务判断来源。Exit(261)直接清空本地房间并跳大厅,sendExitRoom(998)才是发退出请求;gameOver与mainScene又操作另一组页面流程。名字接近,语义差异大。sendData(403)暴露 app/route/rpc/data 全部底层细节;大量具体命令重复读取全局身份与房号。子游戏容易误发平台 route 或构造不一致身份。openVideo(495)覆盖 Desk.roomtype、C_Player.seat、Desk.roomcode,填玩家、改设备标识和 UI;closeVideo(604)清空同一 Desk。回放与在线会话没有数据隔离。onGameFinished(668)并不只是“游戏结束通知”,还拼分享内容、更新头像精灵与隐藏 spidList;重绘时再次调用可能重复产生副作用。typeForActivity的注释也明确强调不能在重画时调用。getMultipleResult(369)看似计算函数,实际还修改 GameData.OrgArr、读取和修正本地存储。计算、用户偏好和状态写入应分开。openInputPanel、onCheckInput、calResult组合使用全局回调入口;需要进一步核对重叠调用和关闭语义。新接口不应让 A 房间的输入结果回到 B 房间。sendText(969)捕获异常但不报告,调用方无法区分未发送与成功。
因此不建议复刻 Utl 类或创建一个包含所有旧方法的 PlatformSDK 类;这会保留原有集成困难。
10.3 建议的迁移归属
生命周期和包接收:StartWar/DeskInfo/Reconnect/myJoinRoom/Free 由房间编排器产生带来源的输入,由游戏模块解释;updateScene 属于 View 从当前状态恢复展示,不重复发送命令或结算事件。
玩家查询:getPlayerList/getMyInfo/getMySeat 等收敛到只读 RoomSnapshot 与 seat mapper;同一快照版本内查询一致。Cocos 节点/精灵资源编号不出现在这些数据中。
房间命令:prepare、start、exit、applyDissolution 等具名方法,由平台统一填身份、房号和 route。游戏消息端口只允许当前游戏 route,游戏内部可用 rpc→payload 类型映射,平台仍不解析玩法字段。
游戏规则查询:getRoomInfo/getRoomMode/getStarLimit/getMult/getLeaveLimit/getRoomTopDescAry 移到游戏提供的纯配置/描述能力。描述返回结构化文本项,换行、布局与字体属于展示。已进房的服务端公共 roommode 等字段继续以回包为准,不能用游戏预览规则覆盖。
音频、提示、输入、剪贴板:按能力提供带作用域的方法。输入结果包含 confirmed/cancelled,离房必定完成取消或拒绝,不能悬空 Promise。单例面板重叠请求是排队、拒绝还是替换必须由能力定义,禁止静默替换回调。
分数与 HUD:setGrade/changeBean 不直接写平台资产。游戏拥有玩法分数与展示投影,平台账户余额仍以权威资产消息为准;必要的公共玩家栏展示通过命名字段区分玩法分数与账户资产。
结算与分享:游戏先解析结算事实、更新自己的状态;单独产生一次性结算展示/分享请求;重绘只读取状态。结束一局、结束一房和关闭结果页面是三个不同语义。
回放:独立 ReplaySession/ReplayContext,有自己的玩家视角、时钟与消息源,复用游戏逻辑/展示但不复用在线 RoomStore。回放上下文不提供真实网络发送能力。
大厅与安装/图片回调:gameHallImport 属于宿主应用集成,不塞进每个 GameModule 的必需接口。
11. 对第一版方案的必要补强
11.1 增加受控的游戏事实投影
“子游戏不能写平台 Store”是正确边界,但不能因此丢掉 setPlayerPrepare/setDeskStage 所承载的业务:一些下一局、结算、无限局参与状态只在玩法包中表达,公共平台包并不总能独自更新全部展示。
建议由游戏提供只读、可订阅的 GameRoomProjection,表达自己已解析的玩法阶段、自身参与状态、玩家玩法分数,以及对公共操作的限制。平台组合展示模型时读取投影,不能反向调用游戏内部 setter;每个字段定义唯一来源与会话版本。
不能保留通用 patchRoom 或任意 setPlayerState。确需更新公共协议状态时,逐条指定对应玩法输入和映射入口,通过协调器提交;审查时必须给出来源包,不能由按钮或动画猜测。投影中的游戏限制也不能扩大服务端/平台已明确禁止的权限。
11.2 少量必需入口,可选能力显式声明
开发者表面建议收敛到:一个 GameDefinition、一个每房间独立的游戏模型、一个创建页面、一个房间 View,以及游戏内部协议/规则模块。必需流程约定初始化、输入处理、释放;平台生命周期输入采用有类型的 discriminated union,保留来源和载荷。
游戏不使用的视频、活动、VIP、回放等能力无需实现几十个空方法。必需能力缺失时在装配阶段报错;声明为可选且有明确“未提供即不展示”语义的能力才允许省略。现有 GameHost.execute 回调联合不要无限扩张成新的 Utl。
RoomContext 可以提供少量分组能力(room、messages、ui、audio、scope);它是类型明确、房间绑定的门面,不是 getService(name) 服务定位器,也不暴露全部平台内部对象。
11.3 同步消息、异步展示、显式结果
同步:游戏规则解析、快照查询、消息到状态迁移、能力校验。
异步:场景就绪、资源加载、用户输入、必要的设备调用。开战动画不会延迟处理下一条协议消息,重连直接重建当前状态并取消过时动画。
发送接口区分“已写入传输层”和“服务器已确认”;没有回复契约的命令不能伪造成功 Promise。输入取消、离房、断线和服务端拒绝都有明确结果,不能靠空回调或 UI 消失推断。
11.4 子游戏接入的验收方式
接入者完成以下事情即可运行一款游戏的基础房间:
- 注册游戏身份关联、route、资源与声明的能力。
- 实现自己的 roomtype 编码/读取、座位规则和房间描述。
- 实现玩法消息和各进入来源的映射;缺少玩法实现必须显式标记,不能用空函数假装支持。
- 提供创建页与房间 View,平台负责外围房间 UI 和资源作用域。
- 用 SDK 接入测试验证创建按钮→实际出站包、创建/加入/重连、结算/退出、资源释放。
同时以第二款配置格式不同的测试游戏验证:不用修改 framework、公共 Cocos UI 或平台 route 分发器。测试游戏不需要重做大厅、聊天、解散和原生桥。编辑器资源装配也纳入检查,不能只以 TS 编译成功作为接入完成。
这些补强完成后,才适合称为“平台无具体游戏耦合、游戏接入步骤少、可独立测试”。本次仍是设计评估,没有实施接口重构,也没有以未经测量的耗时数据证明性能。