Files
youle_cocos/docs/superpowers/specs/UI-手工迁移规范.md
T
joywayerandClaude Opus 5 1e51daeb50 docs(spec): UI 手工迁移规范 (9 步 funplay MCP 流程 + 8 项验证清单 + 多帧 sprite 命名规则)
基于 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>
2026-09-03 17:40:33 +08:00

8.1 KiB
Raw Blame History

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 UUID
  • FrameStyle / 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 migration
  • VoiceFileID 音频 → deferred to audio migration
  • Event 字段(mousedown 等)→ 用 bind_button_click_event 工具 wire
  • GameTxtStyle i18n → deferred to i18n plan
  • FrameStyle + 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)