Files
youle_cocos/docs/superpowers/specs/2026-09-05-platform-ui-wiring-design.md
T

10 KiB
Raw Blame History

公共 UI 接线设计:启动 → 登录 → 大厅 → 进房

日期:2026-09-05。基线:master@8eae325。

1. 目标与实施边界

这是总设计 2026-09-04-framework-subgame-zero-coupling-migration-design.md 第三阶段的第一个子项目:把已完成的 PlatformRuntime 接到可测试的公共 UI Presenter。先交付不依赖 Cocos 的接线核心,再进行编辑器接管。

本轮产物为设计与实施计划,不运行实施任务。下一实施批次称为 3A:UI 接线核心,包括 Runtime 只读观察、ViewModel 投影、命令入口、同步 ScenePort 适配与真实 Runtime 回放测试。

3B:Cocos 实际接管单独实施:需要编辑器确认节点、挂载与持久化,且须先取得修改指定序列化资源的明确授权。用户现有“不修改任何 .scene/.prefab/.anim/.meta”限制继续有效,不能因为有 MCP 就绕过。3A 不启动编辑器导入,不写这些文件,也不声称实际界面已经可用。

主题扩展、SemanticAssetKey/ExtensionSlot 完整实现、创房规则页、聊天/商城/设置、首个真实子游戏均不在 3A 内。现有主题模块不复制、不重构。

2. 已核对的现状

  • assets/framework/platform/runtime.ts 已拥有唯一 Store、WireClient、Router、GameSessionHost,公开 state、ready、登录/进房/准备/退出命令,但没有 UI 订阅入口。
  • Store 为不可变快照,订阅不立即回调;其通用监听异常处理只记录日志。UI 需要显式错误通道,不能让渲染故障只留日志。
  • ScenePort 是同步接口。当前房间时序为 module attach、room.entered、showRoom()、可选 restore(deskinfo)。不得将 showRoom() 实现为未等待的异步场景切换。
  • 当前 assets/scenes 只有 Login 场景;旧 LoginFlow.ts 跳转 MainMenu,且自行建立旧网络栈、内置调试账号。LaunchFlow.ts、SceneStart.ts、RoomEventProbe.ts 也包含演示流程,不是新 Runtime 入口。
  • 已迁移 Login_Layer、Layer4_MainMenu、Layer15_JoinRoom、Layer50_MainScene、Layer202_PlayerHeadScore 及 loading/reconnect/kick 等 Prefab。存在资源不等于已确认按钮语义或绑定。
  • 两个旧 PlayerInfoView 使用相同 ccclass 名,且含下游默认值。新核心不依赖它们;是否阻碍实际编辑器接管由 3B 预检决定,不能随手修改旧资源引用。
  • 旧 net/cocos-transport.ts 含可选发送、字符串强转和吞异常,不能不经契约测试就作为现代生产适配器。

以上路径均相对 cocoscreator_projects/YouleNexus。已废止的 2026-09-04-platform-vertical-slice.md 不得执行。

3. 方案选择

选择单场景常驻根、页面预加载、同步页面激活。3A 用严格 ViewPort 验证该语义,不创建 Cocos 节点;3B 用组件引用实现同一端口。

另外两个选项不采用:

  • 多场景异步导航需要修改 Runtime/Host 的进房恢复时序,扩大已验证生命周期契约的变更面。
  • 直接给旧 LoginFlow/RoomSceneStart 接新网络对象会保留双 Store/双网络栈及下游默认值,无法满足唯一来源。

3B 的所有基础页面、公共座位容器和必需绑定须在 loadResources() 完成前准备好。加载画面本身由启动根预先具备,不依赖这次异步加载。showRoom() 只投影最新快照、同步渲染并激活已就绪页面;不加载资源、不发包、不初始化第二个 GameSessionHost。

4. 核心接口与所有权

新增 framework/presentation/,允许依赖 platform 的只读类型、现有 selector 与 ScenePort;不导出到 SDK,不允许子游戏反向导入。View 不得到 Store、WireClient、Router 或协议包构造器。

Runtime 增加:

subscribeState(
  listener: (state: PlatformState) => void,
  onError: (error: unknown) => void,
): () => void;

订阅不立即通知;通知交付 Store 提交的原快照,不复制、修补或另建响应式状态。调用者订阅后同步读取 state 获取初值。取消幂等;stop/fatal 清理观察者;终止后新订阅显式拒绝。某监听器抛错时先取消该监听器,再将原错误交给必需 onError,其他监听器仍可接收。onError 也是可抛的外部回调:复用现有清理/错误聚合路径,原错误不可丢失;不得修改通用 Store 的订阅语义来满足 UI。

Presenter 接口:

type UiRuntime = Pick<PlatformRuntime,
  'state' | 'ready' | 'subscribeState' | 'login' | 'joinRoom' | 'prepare' | 'exitRoom'>;
interface FrameScheduler { request(callback: () => void): () => void; }
interface PlatformViewPort {
  render(model: PageModel): void;
  activate(page: PageId): void;
  showOverlay(model: OverlayModel): void;
}

FrameScheduler 必须异步、至多调用一次;返回取消函数。测试用手动帧,3B 才使用 Creator 更新周期。Presenter 拥有订阅与帧取消;外层应用拥有 Runtime.stop。清理与错误处理不得相互递归复活。

PlatformUiController 实现 ScenePort,构造参数为 ViewPort、FrameScheduler、必需 onFault(error: unknown): void,随后 connect(runtime: UiRuntime): void 一次。外层顺序:构造 controller → 用 controller 作为 ScenePort 构造 Runtime → connect → Runtime.start。controller 不调用 Runtime.start/stop、不选择 GameEntry/渠道/账号来源。

5. ViewModel 与页面状态

PageId = 'loading' | 'login' | 'lobby' | 'room'。PageModel 是对应判别联合:loading 无业务字段;login 携带 canLogin;lobby 携带必需 self;room 携带 roomcode、selfSeat、动态 seats、canPrepare、canExit。玩家公共显示字段仅 playerid/nickname/avatar/bean。

SeatModel 为 empty 或 occupied,都携带服务器 seat 索引,occupied 额外携带 player、ready、offline。数组长度严格等于权威 seatPlayerIds.length;不固定为 4/8,不压缩空位,不解析 roomtype 推测布局。2/4/10 座位都须验证。

已登录页缺少 self 或 occupied 缺实体必须报错;空座位是来源明确的 null 语义,不是伪造匿名玩家。头像 URL 原样交给 View,资源失败的视觉策略不在核心中猜测。

canPrepare 在 room.needprepare 为 1、自身未准备、stage 为 0 且连接 phase 为 logged-in 时为真;canExit 在 stage 为 0 或 infinite 为 1,且 phase 为 logged-in 时为真。这是本批 UI 的可用性策略,不是新增服务器规则。Runtime/Commands 仍为命令合法性的最终执行入口。

页面只由 ScenePort 决定,不能同时通过 app.phase 推断第二套导航。快照通知只触发当前页面更新。页面切换取消待处理旧帧、同步生成最新模型并 render,然后 activate;render 抛错不得激活半成品页面。room 首帧必须在 showRoom 返回前完成,保证既有 restore 时序。

同一帧多次状态变化只渲染最新值。dispose、fatal、kick 后旧帧/旧订阅无效。生命周期检查必须覆盖 View 回调重入 dispose,不能仅在入口检查一次。

6. 操作、拒绝与错误

controller 暴露 login(account: LoginAccountIdentity)、joinRoom(command: JoinRoomCommand)、prepare()、exitRoom()、dismissDenial() 与 dispose(),均返回 void。

  • 登录只在 login 页且 runtime.ready 时转发;大厅进房只在 lobby 页;prepare/exit 只在 room 且模型对应能力为真时转发。界面本地误用显式抛出,不偷偷缓存请求。
  • account/JoinRoomCommand 原引用转发;不填 mock_openid、ip、location、gameid,不裁剪/重写 roomcode,不擅加正则或长度协议。账号/设备/进房环境由外层权威适配器完整供给。
  • 本批不新增超时、重连策略或通用请求 pending 状态机;网络策略沿用 Runtime。按钮节流/账号异步获取另有来源契约后再实施。
  • OverlayModel 为 none/reconnect/denial/kicked/fatal 判别联合。denial 保存完整 ServerDenial 引用,包括原 data;不把合法非零结果升级 fatal,不修改 Store。dismissDenial 仅清除当前 denial;登录/进房成功页面切换清除非终止 overlay。
  • 重连保留当前页面。Store phase 从 reconnecting/slow 恢复 connected/logged-in 时只清 reconnect overlay,不清 denial,不自行跳大厅。
  • kicked 保存原 data,fatal 保存原 Error。两者终止交互并取消观察/帧,但保留底图用于展示;fatal 可覆盖 kicked,其他回调不得覆盖终止画面。
  • 同步 ScenePort/View 异常向 Runtime 原样传播;帧/订阅异步异常必须取消更新并调用 onFault。外层应展示 fatal 并停止 Runtime;两项操作均须尝试,清理异常聚合保留。3A 不把错误转换为默认模型。

7. 外部兼容与后续实际接管

服务器包、route/rpc、roomtype 与 deskinfo 全部沿用已验证 contracts。不得为 UI 新增字段。远程配置与原生读取/WVJB 不在本批修改;后续适配前须完整读 native-bridge-contract,并用原工程核实精确契约,不能根据摘要重新发明接口。已核定远程配置 POST 空 body、URL 拼接、gameconfig 等行为保持现状。

3A 测试夹具只留在 framework-tests,不导入 assets,不声明生产 GameEntry。3B 启动前须明确真实 GameEntry 与 config.identity.gameid 一致、账号来源及目标宿主。尚未选定真实游戏不能用夹具顶替生产入口。

3B 单独批准后需完成:MCP 枚举 Login 活跃组件及 Prefab 控件语义;形成唯一绑定清单;移除活跃路径上的旧模拟驱动;以一个应用根持有新 Runtime;严格 WebSocket adapter 契约测试;保存并重开确认挂载;浏览器回放截图与真实宿主验收分别记录。没有这些证据,3A 完成不能称为“UI 接管完成”。

8. 验收

3A 必须通过:观察生命周期、2/4/10 座位、同帧合并/切页首帧、命令原样转发、非零拒绝可重试、重连 overlay 恢复、kick/fatal 终止、回调重入销毁、真实 PlatformRuntime 登录/进房/deskinfo 恢复顺序回放、现有非 UI 回归、类型与架构检查。

代码边界测试禁止 presentation 导入 cc/net/protocol 实现、旧 PlatformSession/RoomRPCBus,以及 SDK 导出 presentation。允许 type-only 引用现有 LoginAccountIdentity、JoinRoomCommand、ServerDenial;协议 parser/builder 不进入 Presenter。

任何 .scene/.prefab/.anim/.meta 变更均使 3A 验收失败。本计划完成后交付测试证据与明确 3B 门槛,不自动启动编辑器、选择真实游戏或删除旧资源。