关于 migrate-sdk6-to-sdk7
供编码 Agent 将现有 Decentraland SDK6 场景迁移到 SDK7 的技能。它先核实项目版本,并指导 API 转换,同时保留原有场景入口路径。
主要功能
- 01核实 SDK6 项目版本
- 02SDK6 到 SDK7 API 映射
- 03按序执行迁移流程
- 04实体与系统转换指引
- 05可选迁移静态场景内容
适用场景
- 01迁移现有 SDK6 场景
- 02检查实体与组件转换
- 03安全更新场景配置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill migrate-sdk6-to-sdk7安装后在 Claude Code 中输入/migrate-sdk6-to-sdk7确认已加载。
附带文件references/api-mapping.mdreferences/migration-example.md
详细介绍
将 Decentraland SDK6 场景迁移到 SDK7
在 Creator Hub Agent 中已禁用。 Creator Hub 随附此技能时不包含其
SKILL.md,因此它不会触发应用内嵌的 AI 助手——Creator Hub 通过自有界面管理项目设置,并且仅支持 SDK7。技能的references/文件仍会随附,因此其他技能仍可解析指向这些文件的相对路径。如果你是 Creator Hub 助手,且用户提出此需求,请引导他们使用应用自身的流程。在 Creator Hub 之外(Claude Code、Cursor、SDK Agent),此技能可正常使用。
这是一项移植技能,而非脚手架技能。 它假设磁盘上已有 SDK6 项目。要创建全新的 SDK7 场景,请使用 [[create-scene]]。
规则:开始操作前,先确认项目确实是 SDK6
按以下顺序检查这些文件。只有所有 SDK6 特征都存在时,才能将项目视为 SDK6——部分匹配通常意味着项目只完成了一半迁移,需要采用不同的方法。
| 检查项 | SDK6 特征 | SDK7 特征 |
|---|---|---|
package.json dependencies | decentraland-ecs | @dcl/sdk |
package.json scripts | build-ecs、dcl start | sdk-commands start、sdk-commands build |
scene.json | 没有 runtimeVersion,或缺少 ecs7 | "runtimeVersion": "7" 和/或 "ecs7": true |
| 源代码导入 | 没有显式导入 @dcl/sdk/ecs——符号(Entity、Transform、Vector3、engine)为全局变量 | 大多数内容从 @dcl/sdk/ecs 和 @dcl/sdk/math 导入 |
| 源代码模式 | new Entity()、entity.addComponent(...)、@Component('name')、class X implements ISystem、OnPointerDown、GLTFShape、Input.instance | engine.addEntity()、Transform.create(entity, ...)、engine.defineComponent(...)、pointerEventsSystem.onPointerDown、GltfContainer |
规则:不要更改 scene.json 的 main 字段
保留 SDK6 项目原有的 main 值(通常为 "bin/game.js")。虽然新场景的 SDK7 默认值是 bin/index.js,但 sdk-commands build 会将构建包写入 scene.json 的 main 指定的路径(已验证:sdk-commands 的 bundle.ts 根据 sceneJson.main 设置输出文件),因此现有的 bin/game.js 无需更改即可继续使用。除非用户明确要求,否则不要将其重命名为 bin/index.js。
你应该在 scene.json 中添加 "runtimeVersion": "7"(并移除任何 ECS6 专属内容)。完整的 scene.json 架构请参阅 [[create-scene]]。
规则:迁移同样适用组合文件优先原则
迁移后,所有静态场景内容(模型、灯光、基元、场景加载时放置的文本)都应放在 assets/scene/main.composite 中,而不是 TypeScript 中。SDK6 场景没有组合文件——每个实体都是通过代码创建的。将这些实体迁移到组合文件中是一个单独的可选步骤;_最小可行_迁移会将它们保留为 TypeScript 中的 engine.addEntity(),以便场景先运行起来。
操作顺序:
- 先让 SDK7 TypeScript 移植版运行起来(实体仍通过代码创建)。
- 然后,可选地将静态实体移至
assets/scene/main.composite。有关组合文件优先原则,请参阅 [[create-scene]];有关格式,请参阅 [[composites]]。
概念转变——编写任何代码前请先阅读
SDK6 将实体建模为附加了组件的对象。SDK7 是真正的 ECS,实体只是不透明 ID,组件则是纯数据。
| 概念 | SDK6 | SDK7 |
|---|---|---|
| 创建实体 | const e = new Entity() then engine.addEntity(e) | const e = engine.addEntity()(返回 ID) |
| 实体类型 | IEntity / Entity 类,带有方法(addComponent、getComponent、setParent、alive) | Entity = 不透明整数 ID,无方法 |
| 添加组件 | entity.addComponent(new Transform({...})) — 入口是实体 | Transform.create(entity, {...}) — 入口是组件类型 |
| 定义组件 | @Component('name') class GemData { val: number; constructor(...) {...} } | export const GemData = engine.defineComponent('gemData', { val: Schemas.Number }, { val: 2 }) — 架构 + 默认值,无方法 |
组件方法(reset() 等) | 允许(类方法) | 禁止 — 组件仅存储数据。将方法移到普通函数或系统中 |
| 读取组件数据 | entity.getComponent(GemData).val(始终可变) | GemData.get(entity).val(不可变,速度快),或 GemData.getMutable(entity).val = X(仅在写入时使用) |
| 修改组件数据 | 修改返回的引用 | 必须使用 getMutable() — .get() 返回只读引用。使用 .get() 并修改它会静默失败 |
| 设置父级 | child.setParent(parent) | 设置子实体 Transform 的 parent 字段:Transform.create(child, { parent: parentEntity }) 或 Transform.getMutable(child).parent = parentEntity |
| 定义系统 | class MoveGems implements ISystem { update(dt) {...} } then engine.addSystem(new MoveGems()) | export function moveGemsSystem(dt: number) { ... } then engine.addSystem(moveGemsSystem) |
| 查询实体 | const group = engine.getComponentGroup(Transform, GemData) then group.entities | for (const [entity, gemData, transform] of engine.getEntitiesWith(GemData, Transform)) { ... } |
| 移除实体 | engine.removeEntity(entity) | engine.removeEntity(entity)(相同 API) |
| 组件上的事件回调 | entity.addComponent(new OnPointerDown((e) => {...})) | pointerEventsSystem.onPointerDown({entity, opts}, () => {...}) — 与组件分离 |
| 静态“场景级”对象 | Camera.instance、Input.instance(单例) | engine.PlayerEntity、engine.CameraEntity、engine.RootEntity + 读取组件(Transform.get(engine.PlayerEntity)) |
可变性是一项性能约定,而不只是代码风格。始终优先使用 .get()(读取),而不是 .getMutable()(写入)。如果只读取却使用 .getMutable(),每一帧都会发送一个 CRDT 增量。 |
API 映射 — 快速参考
完整表格请参阅 {baseDir}/references/api-mapping.md。最常见的转换如下:
| SDK6 | SDK7 |
|---|---|
import { ... } from 'decentraland-ecs'(通常为隐式导入) | import { engine, Transform, GltfContainer, Vector3, Quaternion, Color4 } from '@dcl/sdk/ecs' + '@dcl/sdk/math' |
new Entity() + engine.addEntity(entity) | const entity = engine.addEntity() |
entity.addComponent(new Transform({position, rotation, scale})) | Transform.create(entity, { position, rotation, scale }) |
entity.getComponent(Transform)(读取/写入) | Transform.get(entity)(读取)/ Transform.getMutable(entity)(写入) |
entity.getComponentOrCreate(Transform) | Transform.getOrCreateMutable(entity) |
entity.addComponentOrReplace(...) | Component.createOrReplace(entity, {...}) |
entity.hasComponent(Transform) | Transform.has(entity) |
entity.removeComponent(Transform) | Transform.deleteFrom(entity) |
child.setParent(parent) | Transform.getMutable(child).parent = parent(或在 create 中传入 parent) |
new GLTFShape('models/x.glb') + entity.addComponent(shape) | GltfContainer.create(entity, { src: 'models/x.glb' }) |
new BoxShape() / new PlaneShape() / new SphereShape() / new CylinderShape() | MeshRenderer.setBox(entity) / setPlane(entity) / setSphere(entity) / setCylinder(entity)(如需碰撞检测,请添加 MeshCollider.setBox(entity) 等) |
new Material() + m.albedoColor = Color3.Blue() + entity.addComponent(m) | Material.setPbrMaterial(entity, { albedoColor: Color4.Blue() })(注意:SDK7 中使用 Color4) |
new BasicMaterial() + m.texture = new Texture(...) | Material.setBasicMaterial(entity, { texture: Material.Texture.Common({ src }) }) |
new TextShape(text) + t.fontSize = 1 | TextShape.create(entity, { text, fontSize: 1 }) |
new Animator() + new AnimationState('Clip') + animator.addClip(state) | Animator.create(entity, { states: [{ clip: 'Clip', playing: false, loop: false }] });使用 Animator.playSingleAnimation(entity, 'Clip') 播放;使用 Animator.stopAllAnimations(entity) 停止 |
在实体上使用 new OnPointerDown(handler, { button, hoverText }) | pointerEventsSystem.onPointerDown({ entity, opts: { button: InputAction.IA_POINTER, hoverText } }, handler) |
Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, false, handler) | 在系统中使用 inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_DOWN),或对特定实体使用 pointerEventsSystem.onPointerDown |
ActionButton.POINTER / .PRIMARY / .SECONDARY | InputAction.IA_POINTER / .IA_PRIMARY / .IA_SECONDARY |
Camera.instance.position / .rotation | Transform.get(engine.CameraEntity).position / .rotation |
new Vector3(x, y, z) | Vector3.create(x, y, z) |
Quaternion.Euler(x, y, z) | Quaternion.fromEulerDegrees(x, y, z) |
Color3.Blue() | Color4.Blue()(SDK7 的大多数组件字段使用 Color4) |
Vector3.Lerp(a, b, t) | Vector3.lerp(a, b, t)(小写 l) |
Scalar.Lerp(a, b, t) | Scalar.lerp(a, b, t) |
vector.setAll(0.5) | Vector3.create(0.5, 0.5, 0.5)(没有可变更对象的辅助方法——Vector3 被视为普通数据) |
class X implements ISystem { update(dt) {...} } + engine.addSystem(new X()) | function xSystem(dt: number) { ... } + engine.addSystem(xSystem) |
engine.getComponentGroup(A, B) 返回 .entities | engine.getEntitiesWith(A, B) 返回由 [entity, a, b] 元组组成的可迭代对象 |
log(...) | console.log(...) |
entity.addComponent(new utils.Delay(ms, cb))(来自 decentraland-ecs-utils / @dcl-sdk/utils) | timers.setTimeout(cb, ms)(import { timers } from '@dcl/sdk/ecs',参数顺序为 (callback, ms)) |
entity.addComponent(new utils.Interval(ms, cb)) | timers.setInterval(cb, ms)(import { timers } from '@dcl/sdk/ecs') |
自定义逐帧 timerSystem 累积 dt | 删除它——改用 timers.setTimeout / timers.setInterval |
迁移工作流
请按顺序执行。跳过步骤会导致场景损坏,且难以调试。
1. 审查 SDK6 项目
阅读每个源文件。列出以下内容:
- 每个自定义
@Component类——这些类将转换为具有扁平架构的engine.defineComponent(...)。类方法(reset()等)必须移到自由函数或系统中。 - 每个
class X implements ISystem——这些类将转换为自由函数(dt: number) => void。 - 每个
OnPointerDown/OnPointerUp/Input.instance.subscribe——这些将转换为pointerEventsSystem调用,或系统中的inputSystem.isTriggered()。 - 每个
GLTFShape、BoxShape等——请参阅映射表。 - 每个
Camera.instance、Input.instance——这些将转换为静态引擎实体和组件读取。 - 所有
setParent调用——这些将转换为Transform.parent字段赋值。
此列表是步骤 5–10 的完成检查清单——只有当所属类别中的每一项都已迁移,且源代码中不再出现 SDK6 符号时,对应步骤才算完成。
2. 替换 package.json、tsconfig.json 并更新 scene.json
- 将
decentraland-ecs替换为@dcl/sdk(最新版本)。 - 将
build-ecs/dcl start脚本替换为sdk-commands start/sdk-commands build。 - 将旧版
tsconfig.json替换为标准 SDK7 配置(target: ES2020、module: ESNext、jsx: react-jsx、strict: true)。 - 在
scene.json中添加"runtimeVersion": "7"。除非用户提出要求,否则不要修改main。
3. 将资源文件夹整理到 assets/ 下
SDK6 场景通常会将资源放在项目根目录下的顶层文件夹中:models/、images/、audio/(或 sounds/)、textures/、videos/。SDK7 和 Creator Hub 要求资源放在顶层的 assets/ 文件夹下,以便可视化编辑器为其编制索引。请将此操作作为代码迁移前的独立步骤完成——这样后续步骤重写路径时就能直接指向最终位置。
- 识别 SDK6 项目中的所有顶层资源文件夹。常见的 SDK6 文件夹名称包括:
models/、images/、audio/、sounds/、textures/、videos/。 - 创建
assets/下的新目录结构,并采用其他技能中使用的类别名称首字母大写的惯例(请参阅 [[create-scene]]、[[add-3d-models]]、[[audio-video]]):models/→assets/Models/images/→assets/Images/audio/或sounds/→assets/Audio/videos/→assets/Videos/textures/→assets/Images/(纹理属于图片;除非用户想要单独的文件夹,否则请合并)
- 移动文件到新目录结构中。不要保留重复副本——移动完成后删除旧的顶层文件夹。双份文件会导致 Creator Hub 为过期路径编制索引,并使部署时的场景体积大约翻倍(两个副本都会被打包)。
- 重写所有代码引用,使其指向移动后的路径。在整个源代码树中搜索旧文件夹名称,并更新每个匹配项:
GltfContainer.create(e, { src: 'models/chair.glb' })→... { src: 'assets/Models/chair.glb' }AudioSource.create(e, { audioClipUrl: 'sounds/click.mp3' })→... 'assets/Audio/click.mp3'Material.Texture.Common({ src: 'textures/logo.png' })→... 'assets/Images/logo.png'- React-ECS
<UiEntity uiBackground={{ texture: { src: 'images/icon.png' } }} />→... 'assets/Images/icon.png' - 代码、JSON 或
.composite文件中任何提及旧文件夹名称的字符串字面量。
- **完成条件:**在项目中搜索旧文件夹名称时,不再有任何匹配项;在 Creator Hub 中打开场景时,资源树会从
assets/中载入资源。
例外——如果已有目录布局,则沿用现有布局。 如果项目中已包含 assets/scene/Models/(旧版 Creator Hub 布局)或 assets/asset-packs/ / assets/custom/(Creator Hub 会为资源包项目和自定义项目添加这些目录),请沿用这些路径,而不要另建平行的 assets/Models/。原则是:每种资源只使用一个规范位置,不要拆分存放。
4. 创建带有导出 main() 的 src/index.ts
SDK7 场景从顶层导出的 main() 函数开始。通常会将大部分代码放在另一个文件中(例如 src/game.ts),而 index.ts 只负责调用它:
import { initializeGame } from "./game";
export function main() {
initializeGame();
}
5. 优先迁移组件
将每个 @Component 类转换为 engine.defineComponent(name, schema, defaults)。扁平化嵌套类型——Schemas 不支持任意类,但支持 Schemas.Vector3、Schemas.Quaternion、基本类型、数组和嵌套的 Schemas.Map(...)。SDK6 中的 Vector2 字段在 SDK7 中通常会变成两个标量字段(posX、posY)——请参阅 {baseDir}/references/migration-example.md 中的 2048 示例。
完成标准:grep -rn "@Component(" src/ 返回零条匹配结果,并且第 1 步审查清单上的每个组件都有对应的 engine.defineComponent 调用。
6. 迁移系统
每个 class X implements ISystem { update(dt) {...} } 都要转换为独立函数。将 engine.getComponentGroup(A, B).entities 迭代替换为 for (const [entity, a, b] of engine.getEntitiesWith(A, B))。在循环中,仅在需要写入时使用 .getMutable(entity)。
完成标准:grep -rnE "implements ISystem|getComponentGroup" src/ 返回零条匹配结果,并且第 1 步审查清单上的每个系统都通过 engine.addSystem 注册。
7. 迁移实体/组件设置(game.ts 的主要部分)
对于每个 new Entity() 代码块,都要将其替换为 engine.addEntity() 和一系列 Component.create(entity, ...) 调用。将 setParent 转换为 Transform 上的 parent 字段。对每个 src/audioClipUrl/texture.src,使用第 3 步中确定的新 assets/Models/...、assets/Audio/...、assets/Images/... 路径。
完成标准:grep -rnE "new Entity\(|\.addComponent\(|\.setParent\(" src/ 返回零条匹配结果,并且没有任何 src/audioClipUrl/纹理路径仍引用第 3 步之前的文件夹。
8. 迁移输入/指针处理程序
- 每个实体的点击处理程序(
new OnPointerDown(...))→pointerEventsSystem.onPointerDown({ entity, opts }, handler)。如果实体还没有碰撞体,请添加一个(MeshCollider.setBox(entity)或在GltfContainer上设置visibleMeshesCollisionMask: 1)。请参阅 [[add-interactivity]]。 - 全局按键订阅(
Input.instance.subscribe)→ 在系统中使用inputSystem.isTriggered(InputAction.IA_X, PointerEventType.PET_DOWN)。请参阅 [[advanced-input]]。
完成标准:grep -rnE "OnPointerDown|OnPointerUp|Input\.instance|ActionButton\." src/ 返回零条匹配结果,并且第 1 步审查清单上的每个处理程序都有对应的 pointerEventsSystem 或 inputSystem 实现。
9. 迁移动画
new Animator() + new AnimationState('clip') + animator.addClip(...) 将变成单个 Animator.create(entity, { states: [{ clip, playing, loop }] })。使用 Animator.playSingleAnimation(entity, 'clip') 播放。使用 Animator.stopAllAnimations(entity) 停止所有动画。请参阅 [[animations-tweens]]。
完成标准:grep -rnE "new Animator|AnimationState|addClip|getClip" src/ 返回零条匹配结果,并且 SDK6 的 playAnimation/getClip 调用中出现的每个片段名称都列在某个 states[] 数组中——未列入 states[] 的片段会静默失败(请参阅 Common Pitfalls 中有关 playSingleAnimation 的问题)。
10. 迁移声音
new AudioClip(url) + entity.addComponent(new AudioSource(clip)) 将变成 AudioSource.create(entity, { audioClipUrl: url, playing: false, ... })。将 AudioSource.getMutable(entity).playing = true 设为 true 即可播放。请参阅 [[audio-video]]。
完成标准:grep -rnE "new AudioClip|new AudioSource\(" src/ 返回零条匹配结果。
11. 验证和测试
运行 npm install 获取新的 SDK,然后运行 sdk-commands start(或 npm start)。只有满足以下所有条件,迁移才算完成:
npm run build的退出码为 0。- 最后执行一次
grep -rnE "decentraland-ecs|@Component\(|implements ISystem|Input\.instance|Camera\.instance" src/检查,结果为零条命中——工作中断并恢复时容易漏掉逐步执行的 grep;这是端到端检查。 - 预览加载时,控制台没有错误。
- 可见布局(位置/旋转)与 SDK6 场景一致。
如果任一检查未通过,请返回负责该符号类别的步骤——不要声称迁移已完成。
常见陷阱
- 修改
.get()的结果:Transform.get(entity).position.x = 5会静默地不执行任何操作。请使用Transform.getMutable(entity).position.x = 5。 - 向
Transform.create显式传入rotation: undefined/scale: undefined会导致序列化器崩溃:表现为序列化时出现"Cannot read properties of undefined (reading 'x')"。原因(已验证 —@dcl/ecs/dist/components/manual/Transform.js):Transform.create会通过TransformSchema.extend处理该值,而该方法在默认值之后展开...value,因此显式传入的undefined字段会覆盖默认的单位旋转 / 单位缩放,并以undefined存储;随后二进制serialize读取value.rotation.x时就会抛出异常。如果迁移代码根据可能包含undefined字段的部分对象创建 Transform(例如Transform.create(e, { position, rotation: maybeRot, scale: maybeScale })),就会触发此问题。修复方法 — 完全省略该字段,不要传入undefined,这样 schema 才会应用默认值:const out: Partial<TransformType> = { position } if (rotation) out.rotation = rotation if (scale) out.scale = scale if (parent !== undefined) out.parent = parent Transform.create(entity, out) - 忘记 schema:
engine.defineComponent必须提供 schema。SDK6 中没有类型的类字段无法自动推断。 - 组件方法:SDK6 组件通常带有
reset()或构造函数逻辑。在 SDK7 中,必须将这些逻辑移到自由函数中 — 组件是纯数据。 - Vector2 字段:
Schemas没有Schemas.Vector2。[尚未验证 — 请通过读取 Schemas 对象的导出项确认。] 将字段拆成两个Schemas.Number,或使用Schemas.Map({ x: Schemas.Number, y: Schemas.Number })。 - 在实体对象上调用
setParent:SDK7 中的实体是整数 — 没有此方法。请在子级的Transform上设置parent字段。 - Color3 与 Color4:SDK7 中大多数组件字段都使用
Color4(带 alpha)。除非字段文档明确说明是 Color3,否则请将Color3.X()调用替换为Color4.X()。 - 材质不是实体或对象:不存在需要保留引用的
new Material()实例。设置材质只能使用Material.setPbrMaterial(entity, props)— 材质属于单个实体,而不是可共享对象。 - 形状实体(BoxShape、PlaneShape 等)需要 MeshRenderer 和 MeshCollider:SDK6 中,单个
BoxShape同时负责渲染和碰撞(通过标志控制)。SDK7 中这两个组件彼此独立 — 如果需要原始形状支持点击 / 物理效果,请同时添加二者。 GLTFShape默认允许点击其可见网格(withCollisions: true):SDK7 中,GltfContainer默认只在不可见的_collider网格上设置碰撞。有些模型可能不适合这种方式。如果实体因缺少碰撞体而无法点击,可能需要显式将visibleMeshesCollisionMask设置为包含点击所需的CL_POINTER(1)、玩家碰撞所需的CL_PHYSICS(2),或两者都需要时设置为3。请参阅 [[add-3d-models]]。- 在组件组上使用
forEach时,OnPointerDown回调不再触发:SDK6 中它们由事件驱动;SDK7 中应使用pointerEventsSystem.onPointerDown(...)注册一次处理程序 — 不要在系统的更新循环中调用,否则每帧都会注册一个新处理程序。 - 对不在
Animator.states中的动画片段调用Animator.playSingleAnimation会静默地不执行任何操作(适用于在代码中构建 Animator 的迁移者):这具体适用于 SDK6 迁移路径:你在 TypeScript 中自行编写Animator,并以编程方式调用Animator.playSingleAnimation(entity, clipName)。如果clipName不在实体的states数组中,playSingleAnimation就会返回false并且不执行任何操作(已验证 —@dcl/ecs/dist-cjs/components/extended/Animator.js第 35-46 行)。不会发出警告。SDK6 隐藏了这个问题 —Animator.getClip(name)会在首次使用时自动创建动画片段,因此 SDK6 场景通常会在没有预先声明'Appear'的情况下调用playAnimation('Appear')。如果直接将entity.getComponent(Animator).getClip(name).play()映射为Animator.playSingleAnimation(entity, name),而迁移者从未将这些动画片段名称添加到states[],这些调用就会静默失败。修复方法:遍历 SDK6 源码中的每个playAnimation/getClip调用,收集每个实体的全部动画片段名称,并在调用Animator.create时将它们全部列入states数组。若想快速添加适合迁移的垫片,通过惰性执行states.push恢复 SDK6“无需设置即可播放任意动画片段”的便捷方式,请参阅 [[animations-tweens]] 中的包装器(PITFALL 部分)。此规则不适用于 Animator 由 Creator Hub Inspector 编写的场景(它会将 GLB 中的每个动画片段预先填入states[]),也不适用于自带已填充states的资源包 / 智能物品,或完全没有Animator的模型 — 后者依赖下一条中的自动播放机制。 - 将
anim.stop(); anim.play()迁移为每帧调用的手写 SDK7 辅助函数会产生不必要的序列化开销:当你用自定义 SDK7 辅助函数替代 SDK6 的anim.stop(); anim.play(),直接修改Animator.states(s.playing、s.shouldReset),且该辅助函数由每帧执行的调用方触发时,就会遇到此问题 — 例如系统更新,或每个输入保持的时刻都会触发的输入 / 射线检测回调。动画不会冻结(@dcl/ecs中的 CRDT 未变化可变值抑制层会检测相同写入并将其静默丢弃),但每个时刻都调用getMutableOrNull()会带来不必要的序列化和逐字节比较开销。修复方法:跟踪每个实体上最后应用的动画片段,并在其未变化时提前返回;对于使用同一动画片段再次调用noLoop + revertToIdle的情况,只刷新恢复计时器,不要再次修改状态;在恢复回调中重新应用 idle 动画片段前,清除跟踪的动画片段。以下情况不适用:辅助函数仅在状态转换时调用(一次性的onPointerDown、边沿触发的状态机),或通过状态转换而非每帧调用方使用Animator.playSingleAnimation。有关完整辅助函数模式,请参阅 [[animations-tweens]] 中“缩短动画片段切换辅助函数执行路径”的最佳实践。 - 未附加
Animator时,GltfContainer会自动播放多动画.glb中的一个动画片段:SDK6 模型默认保持绑定姿势;SDK7 会从.glb中选取一个动画片段并播放。这与使没有动画片段注册的 Inspector 场景也能正常工作的机制相同 — 它是上一条的补充,而不是矛盾。实体没有Animator时,渲染器会自动播放;实体带有Animator且你调用playSingleAnimation时,动画片段必须在states[]中。渲染器自动播放哪个动画片段因模型而异,无法保证稳定 — 已观察到的情况包括一个万圣节场景中没有Animator的小幽灵直接从 GLB 自动播放die。若要使用不同的默认动画,修复方法是:附加Animator.create(entity, { states: [{ clip: '<intended-default>', playing: true, loop: true }] })以接管控制。若还要在运行时通过playSingleAnimation切换动画片段,修复方法是:将所有要切换到的动画片段列入states[](参阅上一条)。 - 使用
BUTTON_DOWN/BUTTON_UP调用Input.instance.subscribe:没有可直接对应的回调。请在系统中使用inputSystem.isTriggered()检查“本帧刚按下”。 @dcl/ecs-scene-utils触发区域(“Utils 库”)以及手写的逐帧 AABB 检查:不要迁移自定义的逐帧位置检查系统,也不要在 SDK7 中从头重建。SDK6 没有原生触发区域,因此社区 Utils 库(以及许多场景中的自定义代码)会在每帧轮询Camera.instance.position,检查其是否位于盒形 / 球形区域内,并触发onCameraEnter/onCameraExit。SDK7 提供了原生TriggerArea组件(@dcl/sdk/ecs)— 请将整套模式替换为TriggerArea.setBox(entity)(或.setSphere)以及triggerAreaEventsSystem.onTriggerEnter(entity, cb)/.onTriggerExit/.onTriggerStay。尺寸取自Transform.scale,位置取自Transform.position,并且会考虑父级链 — 因此,将触发区域设为已旋转 / 偏移的场景根节点的子级后,无需手动转换坐标即可正常工作。行为一致性:Utils 库只会对本地玩家触发;原生TriggerArea默认使用ColliderLayer.CL_PLAYER,会对该层上的任何玩家触发。若要仅针对本地玩家,正确的检查条件是if (result.trigger?.entity !== engine.PlayerEntity) return(而不是result.triggeredEntity— 尽管名称容易造成混淆,triggeredEntity实际上是触发区域自身的实体,因此将它与玩家比较始终为真,检查条件永远不会触发)。完整组件参考请参阅{baseDir}/references/api-mapping.md的“触发区域”部分和 [[add-interactivity]]。decentraland-ecs-utils中的utils.Delay/utils.ExpireIn(后来改为@dcl-sdk/utils)以及手写的逐帧计时器系统:不要迁移社区的Delay组件,不要在 SDK7 中重建它,也不要编写由累加dt的timerSystem支持的自定义setSceneTimeout辅助函数。请使用@dcl/sdk/ecs的具名导出timers(import { timers } from '@dcl/sdk/ecs')— 它提供绑定到场景引擎的setTimeout/clearTimeout/setInterval/clearInterval。将entity.addComponent(new utils.Delay(2000, cb))替换为timers.setTimeout(cb, 2000),并删除 SDK6 场景中仅用于承载 Delay 组件而创建的“计时器实体”。参数顺序为(callback, ms),而不是(ms, callback)— SDK6 的Delay(ms, cb)顺序与 JS 标准相反;按 SDK6 顺序编写的自定义辅助函数是一个已知的陷阱,只会让一开始不该编写辅助函数的问题更加严重。不要使用原生 JS 的全局setTimeout/setInterval— 尽管 QuickJS 提供了这些函数,它们没有绑定到场景引擎,可能导致难以察觉的问题;请始终使用timers。完整参考请参阅 [[scene-runtime]](计时器部分)。- SDK6 的“拦截工具”智能物品(钥匙、磁铁、库存物品等通过
Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, true, …)和遍历实体层级,对点击的目标执行操作的物品)在 SDK7 中没有直接对应的实现。pointerEventsSystem.onPointerDown仅针对单个实体 — 没有全局点击结果订阅器。迁移模式是建立一个以目标实体名称为键的小型注册表;目标自身的pointerEventsSystem.onPointerDown会先查询该注册表,再回退到默认行为。请参阅{baseDir}/references/api-mapping.md#click-interception-across-entities-keytool-acting-on-a-target。 Vector3.GetAngleBetweenVectors、Vector3.Up()等:SDK6 中有许多 Vector3 静态辅助方法。SDK7 仍在@dcl/sdk/math中提供大部分方法,但大小写已更改(例如Vector3.Lerp→Vector3.lerp)。迁移后请搜索Vector3.,并对照 math 模块核实每个调用。- SDK7 不导出
log():请替换为console.log()。 - 持握物品(枪、瞄准标、手电筒、固定位置工具)上的
Attachable.FIRST_PERSON_CAMERA/Attachable.AVATAR:SDK6 只提供两种粗略的跟随模式 —Attachable.FIRST_PERSON_CAMERA(跟随相机)和Attachable.AVATAR(跟随玩家根节点)。SDK7 提供了此前没有的新选择:使用anchorPointId(例如AAPT_RIGHT_HAND)的骨骼级AvatarAttach。骨骼锚点看起来像是 SDK7 中“将物品放到玩家手中”的自然对应方式,但骨骼锚点会继承玩家动画(待机摆动、行走循环、手势)— 附加到AAPT_RIGHT_HAND的枪会每帧抖动,无法瞄准。持握类游戏物品在 SDK7 中正确的映射方式是通过Transform.parent将实体设为子级,而不是使用AvatarAttach。对于任何需要瞄准的物品(枪、瞄准标、手电筒,以及任何玩家应能通过环视来指向的物品),SDK7 中正确的默认值是Transform.parent = engine.CameraEntity— 这对应 SDK6 的FIRST_PERSON_CAMERA,同时跟踪水平和垂直视角,因此枪口会指向相机所看的方向。Transform.parent = engine.PlayerEntity是另一种选择,也是一种常见的隐蔽故障模式:它跟随玩家根节点(脚部位置和仅水平旋转,不跟随垂直视角或动画),因此当玩家抬头瞄准时,附加到PlayerEntity的枪仍保持水平 — 物品不会跟随视线方向。只有对于不应跟随瞄准的身体固定物品(手持盾牌、静态火炬、挂在腰间且不用于瞄准的库存物品),才使用PlayerEntity。AvatarAttach仅用于希望随动画运动的装饰物品(帽子、光环、背包、姓名牌)。请根据用途选择:engine.CameraEntity(需要瞄准,持握武器的推荐默认值)、engine.PlayerEntity(仅水平旋转 / 不跟随垂直视角,用于身体固定物品)、AvatarAttach(boneId)(仅用于装饰,随骨骼动画)。对比表和完整的枪支示例,请参阅 [[player-avatar]] 中的“持握物品与装饰物品”部分。 - 迁移后,带 Billboard 的模型可能会背对相机:在一些 SDK6 → SDK7 迁移案例中观察到此现象 — 在 SDK6 的
new Billboard(false, true, false)(Y 轴 Billboard)下正面朝外的模型,在 SDK7 的Billboard.create(e, { billboardMode: BillboardMode.BM_Y })下,即使父子级设置相同,也会背对相机。这两个 SDK 似乎对 Billboard 朝向相机的模型面(+Z 或 -Z)有不同约定。SDK7 的PBBillboard接口没有oppositeDirection标志 — 唯一字段是billboardMode(已根据@dcl/ecs的billboard.gen.d.ts验证),因此应修改模型的 Transform,而不是 Billboard 组件。将显示模型绕 Y 轴旋转 180°(Quaternion.fromEulerDegrees(0, 180, 0))。如果实体结构是一个设有 Billboard 的父级和一个子级模型,请将 180° 旋转应用到子级的Transform.rotation— 父实体的旋转由 Billboard 组件控制,在带 Billboard 的实体上覆盖该旋转不会产生效果。[尚未验证这是有变更日志记录的 SDK 显式行为变更,还是模型导入器中另一项约定变更所导致 — 面向用户解释时,请表述为“两个 SDK 对 Billboard 的朝向约定不同”,不要声称是某个特定版本引入了此变化。] SDK7 组件参考请参阅 [[advanced-rendering]](Billboard)。 - UI 像素尺寸不会按 1:1 沿用:SDK6 UI(
UICanvas/UIImage/UIText)是以固定屏幕尺寸为基准,使用原始像素编写的,因此像width = 200或positionX = -350这样的尺寸和位置都是绝对值。SDK7 React-ECS 会根据虚拟画布缩放像素值;在 SDK 7.26.0 及更高版本中,如果不指定虚拟画布,它会按平台应用默认值(1920x1080,移动端为1600x720)— 这很可能与编写 SDK6 UI 时使用的网格不同。在低于 7.26.0 的版本中没有默认值,不指定虚拟画布就完全不会缩放,因此布局会随显示设备变化而偏移。请在ReactEcsRenderer.setUiRenderer中显式设置虚拟画布,让引擎将像素值视为编写 UI 时所用空间中的坐标。迁移 UI 前,请查看 SDK6 源码(旧版 ECS 位于 https://github.com/decentraland/ecs),确认原始 UI 的像素网格,并将其作为虚拟尺寸传入:ReactEcsRenderer.setUiRenderer(uiRoot, { virtualWidth: 1920, virtualHeight: 1080 })1920x1080是一个合理的默认展示值,但正确的 virtualWidth/virtualHeight 取决于编写 SDK6 UI 时所依据的分辨率 — 如果 SDK6 场景使用了不同的参考分辨率(例如1280x720),请改用对应数值,确保现有像素坐标仍落在相同位置。在 7.26.0 及更高版本中,有两种行为可能会导致网格偏移:场景在移动端运行时,16:9 尺寸会被覆盖为1600x720;任何<= 0的值都会禁用缩放。完整的 React-ECS 重写说明及 SDK 版本限制,请参阅 [[build-ui]]。
难以真正迁移的内容
这些内容无法直接一一对应,需要重新设计:
- 包含会执行操作的方法或构造函数的组件(例如
GemData.reset(val, x, y)):将数据拆分到新组件中,把逻辑移至自由函数,该函数接收(entity: Entity, val: number, ...),并调用Component.getMutable(entity)。 - 用户会持有引用并多次迭代的组件组:SDK7 每次调用都会重新计算
engine.getEntitiesWith(...)。对于针对同一集合的热点循环,可以在系统中缓存结果,或接受这项开销。 - 依赖
entity.alive的对象池:SDK7 的实体没有alive标志。对象池必须使用自定义组件跟踪实体是否存活,或通过检查Component.has(entity)来判断。Migrated-2048 示例将其简化为“始终添加新实体”——对于低吞吐量场景,这样做可以接受,但真正的对象池需要显式状态。 - 使用 SDK6
UICanvas/UIImage/UIText/UIClickable构建的 UI:与 SDK7 API 不兼容。SDK7 UI 使用 React-ECS(JSX)。请参阅 [[build-ui]]——这通常需要从头重写。移植时,还要在ReactEcsRenderer.setUiRenderer上设置虚拟画布大小,这样 SDK6 的像素坐标才能在不同屏幕上按预期缩放——请参阅上文 常见陷阱 中的 UI 像素尺寸问题。 @Component('name')装饰器元数据:SDK7 组件 ID 是传递给engine.defineComponent('name', ...)的字符串。如果其他工具(composites、多人游戏同步)按名称引用组件,请使用与 SDK6 装饰器相同的名称。否则,请选择一致的命名方案。
交叉引用
- [[create-scene]] — scene.json 架构、优先使用 composite 的规则,以及如何为全新的 SDK7 项目搭建脚手架
- [[composites]] —
.composite文件格式,用于第二个可选阶段,将静态实体从 TypeScript 移出 - [[add-3d-models]] —
GltfContainer、用于实现可点击性的碰撞体掩码 - [[add-interactivity]] —
pointerEventsSystem(替代 SDK6OnPointerDown) - [[advanced-input]] —
inputSystem轮询(替代 SDK6Input.instance.subscribe) - [[scene-runtime]] —
@dcl/sdk/ecs中的timers.setTimeout/timers.setInterval(替代社区库 SDK6decentraland-ecs-utils的Delay/Interval)、executeTask、fetch、realm/场景信息 - [[animations-tweens]] —
Animator和Tween(替代 SDK6Animator/AnimationState) - [[audio-video]] —
AudioSource和VideoPlayer - [[build-ui]] — React-ECS UI(替代 SDK6
UICanvasUI 系统) - [[advanced-rendering]] — PBR 材质、TextShape、Billboard
- [[multiplayer-sync]] — 用于多人游戏的
syncEntity(替代 SDK6MessageBus/ 同步库)
参考资料
{baseDir}/references/api-mapping.md— SDK6 → SDK7 符号转换完整表格{baseDir}/references/migration-example.md— 使用 2048 游戏场景标注的迁移前后对照(实体设置、组件、系统、指针事件、动画)