基于 Layer 8 (Notice 9 spirit) + Layer 15 (JoinRoom 26 spirit) 实际迁移经验. 含决策树 (脚本生成 vs funplay MCP 手工) + __prefab 数组索引约束 + SelfPos=1 top-left 锚点公式. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.1 KiB
UI 手工迁移规范 (Game_Surface_3 → YouleNexus Cocos)
Status: Draft v1 (2026-09-03) Purpose: 为后续手工 UI 迁移提供可复用规范(funplay MCP + 数据模型 + 验证流程) 基于:
docs/superpowers/specs/原工程数据模型.md+ Layer 8/15 实际迁移经验
决策树:何时用哪种迁移路径
迁移 Layer N 包含 K 个 spirit 且 K ≤ 4 ?
├── 是 (简单 Layer) → 脚本生成 (Python build-layer-N.py)
│ 已验证 OK: Layer 8 (9 spirit), Layer 15 (26 spirit)
│ ⚠️ 复杂 Layer 脚本生成已废弃 (Notice_Layer 9 spirit 经历多次修复)
│
└── 否 (>4 spirit 或需手工 UI 调优) → funplay MCP 手工复刻
单个 API call 效率低 (26 spirit 需 200+ calls)
建议 create_scene + execute_javascript 批量 + save + duplicate_prefab
实践结论:脚本生成和"funplay MCP 手工"在数据层完全等价(同一 JSON schema、同一 spriteFrame UUID 引用),区别仅在过程。复杂 Layer 建议 execute_javascript 一次性注入(既避免 200+ calls 又保证数据精确)。
9 步 funplay MCP 手工迁移流程
Step 1: 准备数据
# 从原工程读取 Layer XML
cat projects/Game_Surface_3/save/LayerXXXX.xml | head -100
# 从 JSON 读取 Object 详细字段
python -c "
import json
with open('projects/Game_Surface_3/output/gameabc_Object.json') as f:
data = json.load(f)
objs = {x['Property']['ObjectID']: x for x in data['ObjectList'] if x}
# 查找某个 spirit 的完整信息
sp = objs[465] # 例: ID 465
print(json.dumps(sp, indent=2, ensure_ascii=False))"
关键字段:
ObjectID/ObjectName/ObjectType(2=Sprite, 4=Text/EditBox, 5/6=运行时)Left/Top/Width/Height(绝对坐标, 左上角锚点)GroupID(0=根 layer, >0=某 group 容器)BelongLayerID/IndexOfLayer(用于排序)ImageFileID→ gameabc_Image.json 的 id 字段 → 找 spriteFrame UUIDFrameStyle/FrameIndex(多帧 sprite 切帧)OriginPos/SelfPos(实测全部 = 1 = top-left, 无复杂公式)L9/T9/R9/B9(9-slice border 像素)Event字段 (mousedown/mouseup 等 → callback ObjectID)Option字段 (canclick, vx/vy/vw/vh 动画物理 - deferred)
Step 2: 查 SpriteFrame UUID
# 找 ImgResID 对应的 atlas sub-meta
python -c "
import json, glob
img_id = 9 # 替换为目标 ImgResID
for path in glob.glob(f'cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-*/{img_id:05d}*.png.meta'):
with open(path, encoding='utf-8') as f:
data = json.load(f)
sub_metas = data.get('subMetas', {})
for sid, sm in sub_metas.items():
if sm.get('name') == 'spriteFrame':
print(f'{path}: {sm.get(\"uuid\")}')"
多帧 sprite 命名规则(实测):
- 0-padded:
00009_01.png~00009_12.png(12 帧) - 不 padded:
00028_1.png~00028_2.png(2 帧) - ⚠️ 迁移脚本需要支持两种命名 (playbook §关键迁移规则 应更新此发现)
Step 3: 创建空 scene (funplay MCP)
mcp__funplay_cocos__create_scene
target: db://assets/framework/ui/_playbook_demo/LayerXXXX_Layer_manual.scene
sceneName: LayerXXXX_Layer_manual
Step 4: 添加容器节点 (group-X)
# 对 GroupID > 0 的每个组, 创建一个 cc.Node
mcp__funplay_cocos__create_node
parentPath: Canvas
name: group-{GroupID}
position: '{"x": 0, "y": 0, "z": 0}'
# + 加 UITransform + Widget + CompPrefabInfo + PrefabInfo (6 对象每个节点)
对 26 spirit Layer 15:1 个 group-5 容器 + 26 sprite 节点 = 27 个 node × 6 对象 = 162 objects
Step 5: 添加 Sprite 节点 (per spirit)
# 26 次循环, 每次:
# 1. create_node (parentPath=Canvas/group-5, name=spirit.Name, position=lpos)
# 2. add_component cc.UITransform (contentSize=spirit.W/H)
# 3. add_component cc.Widget (alignFlags=45)
# 4. add_component cc.Sprite (spriteFrame.UUID, _color white, _type=0)
# 5. add_component cc.CompPrefabInfo
# 6. add_component cc.PrefabInfo (root=node_id)
每个 Node 在 Cocos prefab JSON 中需要 6 个对象:
- cc.Node (parent/children/components/prefab/lpos/lrot/lscale)
- cc.UITransform (node/contentSize/anchorPoint/prefab)
- cc.Widget (node/alignFlags/.../prefab)
- cc.Sprite (node/spriteFrame/color/.../prefab)
- cc.CompPrefabInfo (fileId)
- cc.PrefabInfo (root/asset/fileId/...)
关键:__prefab 字段是 数组索引(指向同一节点对应的 CompPrefabInfo 在 data 数组中的位置)。详见 5.x 数组索引约束。
Step 6: 数组索引约束 (⚠️ 关键)
所有 __prefab 字段必须是数组索引(不是 _id 字段值):
// 正确 (works):
{node: 'group-5', _prefab: {'__id__': N}}, // N = CompPrefabInfo 的数组索引
{sprite: '...', _prefab: {'__id__': N}},
// 错误 (Cocos 报 TypeError: undefined.__type__):
{node: 'group-5', _prefab: {'__id__': 100}}, // 100 是不存在的 object
正确做法:让每个 Node 紧跟其 CompPrefabInfo(按生成顺序 append),用 len(new) 计算数组索引:
node_offset = len(new)
cpi_idx = base_count + node_offset + 1 # cpi 紧跟 node
# 然后 append node, uit, sprite, widget, cpi, pi
new.append(node_obj)
# ↑ base_count + node_offset 才是 cpi 的实际位置
Step 7: 锚点公式 (实测验证)
# SelfPos = 1 (top-left) 实测对所有 991 object 成立
cocos_lpos_x = spirit.Left + spirit.Width / 2 - 640 # 原坐标系半宽 1280/2
cocos_lpos_y = 360 - (spirit.Top + spirit.Height / 2) # Y-up 翻转 + 半高 720/2
# 例: Spirit 27 (加入房间遮罩, X=0 Y=0 W=1280 H=720)
# -> lpos = (0 + 640 - 640, 360 - (0 + 360)) = (0, 0) ✓
多帧 sprite 公式(实测 Sprite 514 在 gameabc 显示第 10 帧 = ImgResID 9 的 spriteFrame 第 10 帧):
- 单帧 sprite (
FrameStyle: 0或FrameIndex: 0) →ImageFileID.bmp单张 PNG → cc.Sprite_spriteFrame.__uuid__用 frame 0 - 多帧 sprite (
FrameStyle: 1+FrameIndex: N) →ImageFileID.bmp是单张 PNG 含多帧 → Cocos 仍用 frame 0 的 spriteFrame,但_type=1(SLICED) + FrameIndex 决定 sub-region
⚠️ Cocos 与 gameabc 的 multi-frame 行为差异:gameabc 每个 spirit 引用 imageasset 同一 sub-region;Cocos 多帧 sprite 由 spriteFrame.uv 数组自动处理 sub-region。实际 UI 表现需要测试。
Step 8: 验证 (8 项)
| # | 项 | 验证方法 |
|---|---|---|
| 1 | node 数量与原 Layer XML spirit 数量一致 | list_prefabs + inspect_prefab |
| 2 | 每个 spirit ID 都在 prefab 中 | inspect_prefab JSON 全文搜 |
| 3 | 中文 Name 严格匹配 | JSON _name 字段 |
| 4 | _lpos 精度 < 0.001 | 对比 XML X/W + 计算公式 |
| 5 | _contentSize 一致 | 对比 XML W/H |
| 6 | SpriteFrame UUID 指向真实 atlas | validate_prefab_references 0 missing |
| 7 | group 层级正确 | _children 引用链验证 |
| 8 | 加载 0 报错 | open_asset + search_project_logs |
Step 9: 提交 + 推送
git add cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/_playbook_demo/
git commit -m "feat(scenes): Layer X JoinRoom_Layer_manual 1:1 迁移 (26 spirits)"
git push -u origin master
⚠️ 写迁移规范文件:docs/superpowers/specs/UI-手工迁移规范.md (本文件)
已知限制 / 后续子任务
Option.vx/vy/vw/vh动画物理 → deferred to game logic migrationVoiceFileID音频 → deferred to audio migrationEvent字段(mousedown 等)→ 用bind_button_click_event工具 wireGameTxtStylei18n → deferred to i18n planFrameStyle + FrameIndex多帧 sprite 在 Cocos 中实际显示 → 需要 test_scene 验证
关联文档
docs/superpowers/specs/原工程数据模型.md(完整 7 个 JSON schema + ObjectType 0-6 映射 + 锚点公式)docs/superpowers/specs/Layer-15-数据完整性报告.md(完整 1:1 验证 + 26 spirit 数据)docs/superpowers/specs/2026-09-03-interface-conversion-playbook.md(playbook §关键迁移规则 多帧 sprite)