Login_Layer 18 个节点经中间描述落地为 prefab, 结构比对通过。 实测耗时回填 spec §5, 作为「是否需要写编辑器扩展」的判断依据。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
22 KiB
旧 gameabc UI 数据 → Cocos 平台界面:迁移与分辨率适配 设计
状态:已与用户确认(brainstorming 通过),待转 writing-plans。 适用工程:
cocoscreator_projects/YouleNexus(框架真源)。 数据源:projects/Game_Surface_3/output/gameabc_*.json(旧 H5 工程的 UI 导出数据)。 关联:2026-08-27-ui-asset-and-skin-design.md(资源归属 §2 / 图集划分 / override 白名单 / theme §4);2026-06-28-cocos-framework-design.md§5(换肤)。 本 spec 只覆盖子系统 A,B(引擎机制映射)与 C(平台逻辑移植)另立,见 §0.1。
0. 目标与背景
0.1 范围:这是三个子系统中的第一个
把「旧平台 UI 搬到 Cocos」拆成三件互相独立的事,本 spec 只做 A:
| 子系统 | 产出 | 依赖 | |
|---|---|---|---|
| A | UI 数据迁移(本 spec) | 991 对象 / 55 界面 → 中间描述 → prefab + 1306 张散图 | 无 |
| B | 引擎机制映射 | ~10 个 ifast_* API + set_self/get_self 属性号对照表 → Cocos 等价物 |
需逆向 js/gameabc.min.js(186KB) |
| C | 平台逻辑移植 | 11_GameUI.js(9862 行)等 → TypeScript |
A 与 B 都要先有 |
A 与 B 互不依赖、可并行;C 必须等两者。
B 的紧迫性:平台层有
set_self1773 处 +get_self447 处调用,全部使用数字属性号(37/7/18/20/43…)。若属性号解不出来,C 就只能重写而非移植,工作量差一个量级。建议 A 开工后尽快并行启动 B。
0.2 决策依据(实测数据)
以下全部实测自 projects/Game_Surface_3,2026-08-28。记录在此,便于日后回看每个取舍的依据。
布局数据规模
gameabc_Object.json:991 个对象,ObjectType仅两种——2= 图片精灵(782)、4= 文字(209)gameabc_Layer.json:55 个 Layer = 55 个界面(Logo_Layer/Login_Layer/MainMenu_Layer/Player_Head_Score_Layer/inputPannel…)gameabc_GroupList.json:80 个组,覆盖 990/991 个对象(仅 1 个未分组)- 109 个对象使用九宫格(
L9/T9/R9/B9非零) - 52 个对象为满屏尺寸(≥1280×720),其中 40 个是所在层的首个对象
- 222 个对象坐标超出画布边界;
Left范围 −286 ~ 1501、Top范围 −318 ~ 1206
图片资源
gameabc_Image.json:439 张图,其中 139 张多帧(31.7%)- 多帧图全部为规整网格:
w × h = frame_all,139 张零例外 - 切分后总计 1006 帧 + 300 张单帧图 = 1306 张散图
- 78/139 张多帧图被多个对象共用,最多一张被 13 个对象共用
- 389 个对象引用多帧图,其中
FrameIndex = 0的仅 2 个
坐标系与适配(现状)
- 旧引擎原点左上、Y 向下、精灵锚点左上,
position = (Left, Top),设计分辨率 1280×720 OriginID/Parent全为 0,OriginPos/SelfPos全为 1 → 零相对锚定数据- 引擎(
js/gameabc.min.js)用innerWidth/innerHeight+scale+onresize做整画布等比缩放,靠 letterbox 适配
文字
- 209 个 Type4 对象全部没有
ImageFileID、TextFrames为空 → 纯动态文本占位 - 样式在代码里:
Utl.setFontColor(spid, value)(08_Utl_Output.js:824),引擎用 canvasfillText - → 转换只能得到几何信息,字体/字号/颜色须由
theme.fonts提供
运行时动态精灵(B 的线索,A 需为其保留信息)
ifast_addtospritefromspritecopy(父精灵ID, 模板精灵ID, x, y, tag)—— 88 处调用ifast_dllpritefromspritecopy—— 74 处调用- → 典型「克隆模板 → 用完删除」的列表/对象池模式;991 个对象中有一部分是模板、一部分是容器
0.3 帧序:1 基 + 行优先(实证结论)
列 = (FrameIndex − 1) % w
行 = ⌊(FrameIndex − 1) / w⌋
源矩形 = (列 × w1, 行 × h1, w1, h1)
验证方法(记录在此,因为这是最容易搞错且后果最广的一处):00014.png 为 450×280、网格 3×4,被 11 个按钮共用。直接读取该 PNG,其网格内容为:
我的 福利 任务
战绩 背包 仓库
吐槽 财富榜 邀请码
通知 设置 (空)
对照对象的 FrameIndex:通知=10、设置=11、战绩=4、背包=5、仓库=6、任务=3。按「1 基 + 行优先」换算,六项全部精确命中;列优先与 0 基行优先均对不上。用户随后独立确认了该结论。
FrameIndex = 0 的 2 个对象按帧 1 处理。
一处被推翻的统计:早期曾得出「1306 帧中 674 帧(51.6%)从未被引用」并险些据此裁剪资源。该数字错误且不得使用,两个原因:① 帧号 1 基,统计时把下标 0 当成了有效帧;② 平台层有
set_self1773 处调用,帧是运行时切换的,静态数据里的FrameIndex只是初始帧。按钮的按下态/选中态就在那些「未引用」帧里。任何帧都不得因静态未引用而删除。
0.4 已确认决策
- 设计分辨率保持 1280×720 +
fitHeight—— 991 个对象的 X/Y 全部 1:1 迁移,不做任何坐标缩放。现代宽屏由fitHeight天然获得(20:9 下水平可见区约等于 1600×720)。 - 多帧图切成散图(1306 张)—— 多帧网格是旧引擎缺图集能力的手工替代品,Cocos 的 Auto Atlas 在构建期自动打包;照搬它等于把引擎缺陷当成新框架的约定。
- 同组对象挂同一父节点 ——
GroupList覆盖 990/991,是旧引擎用来做整体操作的机制,语义等价于父节点。 - 锚点按中心点三区自动推断,仅水平方向(§2.3)。
- 背景四边拉伸,美术后续手动更换正确尺寸的图片。
- 全部 55 个界面转进框架(含牌桌内的平台部分)。
- 落地走「中间产物 + 编辑器批量」(§4.1),不直接生成
.prefab文件。
0.5 红线
- 绝不手写
.prefab/.meta/.scene(CLAUDE.md)。序列化资源只能由编辑器经官方 API 产生。这一条直接否决了「Node 脚本直接吐 prefab」这一最直觉的做法(§4.1)。 - 服务器零改动:本 spec 不触及协议。
- 数据源权威唯一、下游不兜底:中间描述是转换的唯一真相;转换失败必须显式报错,不得静默产出残缺节点。
- 不得因静态未引用而裁剪帧(§0.3)。
0.6 方法论教训(写给后续维护者)
本 spec 的调研过程中两次因看错字段而误判「数据不存在」:
- 查
OriginID/Parent全为 0 → 结论「没有层级信息」。错:层级语义在GroupList里。 - 统计
FrameIndex引用 → 结论「51.6% 帧是死重量」。错:帧号 1 基,且帧在运行时切换。
教训:在 gameabc 这类自研引擎的数据上,「某字段全为空」不等于「该语义不存在」——它可能编码在另一个字段、另一个文件,或根本在代码里。下结论前先把 output/ 下全部 JSON 与相关引擎 API 都看一遍。
1. 中间描述的数据模型
1.1 形态:一层一个 JSON + 一份 manifest
tools/ui-migration/intermediate/
├─ manifest.json # 55 界面索引 + ObjectID 全局映射 + 转换器版本
├─ 002-Login_Layer.json
├─ 004-MainMenu_Layer.json
└─ …(共 55 个)
一层一文件:局部重跑与人工校对的单位就是界面。改了锚点规则只想重转登录页,不该动其余 54 个。
1.2 节点模型
{
"id": 254,
"name": "微信登录按钮", // ObjectName 原样保留(中文名信息量大)
"kind": "sprite", // ObjectType 2→sprite, 4→label
"position": { "x": -245, "y": 125 }, // 已转为 Cocos 坐标系(§2.2)
"size": { "width": 150, "height": 70 },
"siblingIndex": 7, // 来自 IndexOfLayer,决定 z 序
"widget": { "horizontal": "center", "horizontalCenter": -245 }, // §2.3
"sprite": {
"frame": "atlas-login/00014_05.png", // 当前帧的逻辑路径(§3)
"frameSet": { // 供 FrameSet 组件(§3.4)
"prefix": "atlas-login/00014", // 帧文件名前缀
"count": 12, // 帧总数,索引 1..count
"pad": 2 // 补零位数,用于拼出 00014_01 … 00014_12
},
"type": "sliced", // L9/T9/R9/B9 任一非零 → sliced
"slice": { "left": 12, "right": 12, "top": 8, "bottom": 8 }
},
"flags": {
"stretch": false, // 背景类,四边拉伸(§2.4)
"offscreen": false, // 坐标超出画布(§2.5)
"animation": false // FrameStyle=1,整图动画(§3.5)
},
"legacy": { // 原始信息,不参与渲染,但**承重**(§1.3)
"objectId": 254,
"imageFileId": 14, "frameIndex": 5, "frameStyle": 0,
"groupId": 3, "voiceFileId": 0, "timerInterval": 0,
"left": 320, "top": 200, // 原始左上坐标,供回溯对照
"events": ["mousedown", "mouseup"]
// 校验:x = 320 + 150/2 − 640 = −245;y = 360 − 200 − 70/2 = 125
// cx = 320 + 75 = 395 ∈ [320, 960] → 水平居中,horizontalCenter = 395 − 640 = −245
}
}
kind: "label" 的节点只有 position / size / widget / legacy,没有样式字段——依据 §0.2,样式在旧代码里,数据中不存在。字体字号颜色由 theme.fonts 提供(见 2026-08-27-ui-asset-and-skin-design.md §4)。
1.3 legacy.objectId 是承重字段,不是纪念品
旧平台逻辑通过 set_self(精灵ID, 属性号, …) 操作 UI,全平台层 1773 处调用。子系统 C 移植这些逻辑时,必须能由 ObjectID 定位到 Cocos 节点——丢掉这个映射,那 1773 处调用就没有着落,C 会从「移植」退化成「重写」。
因此:
- 每个节点保留完整
legacy块 manifest.json额外维护一份全局索引:ObjectID → { 界面, 节点路径 }
同理保留 groupId(B 需要它理解容器语义)与 events(C 需要它接事件)。
1.4 层级信息
{
"layerId": 2,
"name": "Login_Layer",
"bucket": "login", // login | hall | room | common | unassigned(§3.3)
"backgroundNodeId": 2, // 「层内首个 + 满屏」识别(40/52 命中)
"groups": [ // 来自 GroupList,同组挂同一父节点(§2.1)
{ "groupId": 2, "nodeIds": [2, 405, 285, /* … */] }
],
"nodes": [ /* 按 siblingIndex 排序 */ ]
}
1.5 本节有意不做的事
- 不推断按钮。
Event含mousedown的对象包括背景图(对象 1 即是),据此判断交互性不可靠。事件原样存入legacy,留给 C。 - 不识别模板/容器。§0.2 表明存在运行时克隆的模板与容器,但要认出「哪个 ObjectID 是
ConstVal.myRoomList.bgSp」需要读平台层代码——那是 B/C 的范围。A 只需忠实保留 ObjectID,使后续可交叉引用。
2. 坐标与锚点转换规则
2.1 group → 父节点:父节点必须是满屏容器
子节点坐标相对父节点,而旧数据全是绝对坐标。若 group 父节点用包围盒或零尺寸,两件事会同时出问题:子坐标要重算,且 子节点的 Widget 会锚到组边界而非屏幕边界,适配立刻失效。
解法是让父节点不产生坐标偏移、且与屏幕同尺寸:
Layer 根节点 1280×720,Widget 四边拉伸
└─ Group 节点 1280×720,Widget 四边拉伸,position (0,0)
└─ 对象节点 §2.2 转换后的绝对坐标
于是子对象坐标 = 转换后的绝对坐标(无需再减父偏移),其 Widget 锚定的满屏父节点等价于锚定屏幕边界。
2.2 坐标系转换
节点锚点采用 Cocos 惯例 (0.5, 0.5):
x = Left + Width / 2 − 640
y = 360 − Top − Height / 2
为何不用 (0, 1)(左上):那样公式更简(x = Left − 640、y = 360 − Top)且与旧引擎语义一一对应,但用户明确会在编辑器里人工审核并修改节点树,而 Cocos 的编辑器、对齐工具、Widget 全部假定锚点居中。转换公式复杂一点是转换器一次性的成本,人工编辑的别扭是长期成本。
原始 Left/Top 存入 legacy,回溯对照随时可查。
2.3 锚点推断:只需要水平方向
fitHeight 下垂直方向永远精确撑满、不会溢出,因此不需要任何垂直适配——垂直用固定位置即可。这是保持 1280×720 设计分辨率带来的实质简化。
水平方向按对象中心点 cx = Left + Width / 2 三区推断:
| 条件 | 锚定 | Widget 值 |
|---|---|---|
cx < 320 |
靠左 | left = Left |
cx > 960 |
靠右 | right = 1280 − (Left + Width) |
| 其余 | 水平居中 | horizontalCenter = cx − 640 |
分界取 1280 的四等分点。这是机械规则,不追求完美——用户已确认转换后会逐屏人工审核修正,规则只需「大部分对、且错了容易看出」。
2.4 背景:四边拉伸
识别规则:层内首个 + 满屏尺寸(实测 40/52 命中)。此类节点 Widget 四边置 0 全拉伸,中间描述标 flags.stretch = true。
另外 12 个满屏但非层内首个的(如「创建房间遮罩」)同样四边拉伸——它们本就是覆盖全屏的遮罩,行为一致。
转换器不做任何图片处理:美术后续会手动更换为正确尺寸的图片。
2.5 屏外对象
222 个对象坐标超出画布(Left −286 ~ 1501、Top −318 ~ 1206),多为动画入场的暂存位。
处理:坐标原样转换,不裁剪、不钳制。三区规则照常适用(cx 为负自然落入「靠左」)。额外标 flags.offscreen = true,供人工审核时快速筛出——它们最可能需要手工确认锚点是否合理。
3. 多帧图切分与命名约定
3.1 切分规则
见 §0.3 的公式与实证。139 张多帧图全部满足 w × h = frame_all,切分是纯机械操作。
3.2 命名:保持 1 基,补零对齐字典序
00014.png (3×4) → 00014_01.png … 00014_12.png
00019.png (13×1) → 00019_01.png … 00019_13.png
用 1 基而非 0 基,使文件名与 FrameIndex 直接对齐:C 移植时代码里的 set_self(spid, 帧属性号, 7) 能一眼对上 _07,无需心算减一。这类 off-by-one 是最易反复踩的坑(§0.3 已经踩过一次统计口径)。
补零位数按 frame_all 决定,保证字典序 = 帧序(否则 _10 会排在 _2 前,图集打包与人工浏览都会乱)。
3.3 图集归属
每张图按使用它的对象所在层决定 bucket;跨多个 bucket 的图归 atlas-common。
层 → bucket 用实测出的 LayerID 段位规律 + 可覆盖的显式表:
| LayerID 段 | bucket | 典型层 |
|---|---|---|
| 1–29 | login / hall |
Logo/Login/Auth/Bind → login;MainMenu/Pay/Task/rankList/wareHouse → hall |
| 50 / 202 / 4xx | room |
MainScene / Player_Head_Score / Chat / Interact / btnPannel |
| 6xx | common |
inputPannel / Tips / Loading / Reconnect / Kick |
命名笼统的层(Layer6 / Layer13 / Layer403 / Layer411 / Layer601 / Layer603 / Layer618)标 bucket: "unassigned",不猜,由人工归位。转换器遇到 unassigned 应告警而非静默归入 common。
3.4 运行时切帧:FrameSet 组件
每个引用多帧图的节点挂一个 FrameSet 组件,持有该图全部帧的 SpriteFrame 数组,索引与 FrameIndex 同为 1 基:
frameSet.setFrame(7); // ← C 移植时对应 set_self(spid, 帧属性号, 7)
副作用是必要的:全部帧都被数组引用 → 都会进图集、不会被依赖树裁掉,符合 §0.3「任何帧都不得因静态未引用而删除」。
3.5 不切的情况
- 300 张单帧图原样保留,仅按 bucket 分目录
FrameStyle = 1的 54 个对象(整图动画)照常切帧,节点额外标flags.animation = true,供后续做逐帧动画识别
3.6 物理组织与命名保留
framework/ui/atlas-hall/00014_01.png … 00014_12.png
framework/ui/atlas-login/00006_01.png … 00006_05.png
沿用原编号 + 帧号,不重命名为语义名。两个理由:① 原编号是与 gameabc_Image.json 对照回溯的唯一线索;② 实测证明对象名与美术内容会脱节(对象叫「更多」,图上是「我的」;对象叫「分享」,图上是「福利」),语义命名会把错误信息固化进文件名。
4. 落地管线与验证
4.1 管线形态
gameabc_*.json ──[Node 转换器]──► 中间描述 55 份 + 散图 1306 张
│
▼
[编辑器侧批量落地]
│
▼
framework/ui/*.prefab(55 个)
Node 侧为纯函数:读 JSON、算坐标、推锚点、切帧、分 bucket。可 TDD,沿用既有工具链约定(node:test、零第三方依赖、显式路径提交)。
编辑器侧先用 MCP 跑通一个界面(建议 Login_Layer,18 个对象,规模合适且链路完整),确认节点树、Widget、SpriteFrame 引用无误,再决定是否值得写编辑器扩展批量跑完其余 54 个。不预先写扩展——先证明单条链路成立。
为何不直接用 Node 生成 .prefab:那要手写 UUID 引用的序列化格式,直接违反 §0.5 红线;991 个对象 × 55 个 prefab,格式错一点就是「资源导入失败」且只能人工恢复。
4.2 验证:分四层
991 个对象、1306 张图无法靠肉眼验证,故分层且尽量自动:
① 几何与规则层(自动,TDD)
坐标换算、三区锚点推断、bucket 归属——纯函数,用已知样例断言(如「通知」按钮的 Left/Top → 期望的 Cocos 坐标与 Widget 值写死在测试中)。
② 切图正确性(自动,且是完全验证) 切分是可逆操作:把切出的帧按原网格重组,必须与原图逐字节相同。
切分(原图) → N 帧 → 重组(N 帧) ≡ 原图(逐字节)
对全部 139 张多帧图跑重组校验,即对全部 1006 帧的完全验证,成本近零。行优先/1 基这类错误会被当场抓住——比靠读图判断可靠得多。
③ prefab 结构(自动) 落地后回读 prefab:节点数、层级归属(group → 父节点)、z 序、Widget 参数,与中间描述逐项比对,差异即报错。
④ 视觉验证(半自动,抽样)
用 MCP cocos_capture 截图,与原项目同界面截图并排比对。有 ①②③ 兜底后,这一层只需回答「规则本身定得对不对」(某按钮该锚右却锚了中),而非「转换器有没有 bug」。
4.3 与已交付工具链的衔接
切出的 1306 张图落进 framework/ui/atlas-*,正是 2026-08-27-ui-asset-and-skin-design.md §3.1 定义的可覆盖白名单那 8 个目录。因此:
check-skin的「框架可覆盖资源」将从当前 0 个变为 1306 个——换肤机制首次有真实对象- 该 spec §6 中三条一直 ⏸ 的承重假设(Auto Atlas 构建期打包、meta 尺寸行为、Spine)首次有真实素材可验证
A 落地时应顺手复测这些假设,而非任其继续悬挂。
4.4 幂等与可重跑
- 中间描述是纯函数产物,转换器可任意重跑
- prefab 落地以层为单位,支持局部重跑(改了某界面的规则只重落该层)
- 散图切分同样幂等;重跑前需清理旧产物,避免残留帧文件混入图集
5. 待验证项
| # | 待验证 | 验证方法 | 失败后的备选 |
|---|---|---|---|
| 1 | Auto Atlas 尺寸上限:1306 张图分 4 bucket,某 bucket 可能超单张图集 2048×2048 | 落地后构建,检查图集张数与尺寸 | 按加载时机进一步细分 bucket(如 atlas-hall-1/2) |
| 2 | 包体:1306 张图全部进每个子游戏包 | 第一次真实构建后量实际包体 | 评估是否将低频界面(如 Help/Protol)转为远程 bundle |
| 3 | MCP 批量建节点的可行性与耗时(991 节点) | 实测(2026-08-29):Login_Layer 18 节点落地共 76 次 MCP 调用、约 20 分钟。builder.build 1 次可建整棵树,但 cc.Widget「flag 使能即采纳当前 margin」会按瞬时尺寸污染 margin,需逐节点 set_property 修正(约 22 次、每次 ~3.5s)。按 991 节点外推仅 margin 修正即需 ~1000 次调用(≈58 分钟纯往返)。结论:逐节点 MCP 不可行 |
已确认:修 builder 的 Widget 赋值时序(margin 在最终尺寸后设)或写编辑器扩展批量修正,二者取一 |
| 4 | 三区锚点规则的实际命中率 | 抽样 5 个界面视觉比对 | 调整分界阈值,或改为逐元素人工标注 |
6. 非目标(YAGNI)
- 不做嵌套节点树的进一步细分。转换只产出「Layer → Group → 对象」两层结构;更细的节点树由用户在编辑器中人工组织。
- 不识别按钮/交互组件。事件信息保留在
legacy,由 C 处理。 - 不迁移任何平台逻辑。本 spec 只做静态 UI;逻辑属 C。
- 不处理图片本身(背景拉伸只设 Widget,不做缩放/重采样;美术后续手动更换)。
- 不重命名资源为语义名(§3.6)。
- 不做逐帧动画的时间轴。
flags.animation只做标记。
7. 与其它 spec 的接口
2026-08-27-ui-asset-and-skin-design.md:切出的散图落入其定义的 8 个可覆盖目录;bucket 划分沿用其 §2 规则 3 的目录表;label 样式取自其 §4 的theme.fonts。- 子系统 B(引擎机制映射,待写):需要 A 产出的
legacy.objectId/groupId/events;其产出的属性号对照表是 C 的前置。 - 子系统 C(平台逻辑移植,待写):依赖 A 的 ObjectID → 节点路径全局索引(§1.3)与 B 的 API 映射表。
- 组件替换 spec(待写):
SeatView/ 按人数布局。牌桌内的Player_Head_Score_Layer(60 个对象)由 A 转出静态版本后,该 spec 决定哪些改为按人数动态生成。