migrate-sdk6-to-sdk7
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMigrate a Decentraland SDK6 Scene to SDK7
将 Decentraland SDK6 场景迁移到 SDK7
This is a porting skill, not a scaffolding skill. It assumes an existing SDK6 project on disk. For a brand-new SDK7 scene, use [[create-scene]].
这是移植 skill,不是脚手架 skill。 它假定磁盘上已有一个 SDK6 项目。对于全新的 SDK7 场景,请使用 [[create-scene]]。
RULE: Verify it really is SDK6 before doing anything
规则:开始操作前先确认它确实是 SDK6
Inspect these files in this order. ALL of the SDK6 signals must be present before treating the project as SDK6 — partial matches usually mean a half-migrated project that needs a different approach.
| Check | SDK6 signal | SDK7 signal |
|---|---|---|
| | |
| | |
| No | |
| Source code imports | No explicit | Most things imported from |
| Source code patterns | | |
按以下顺序检查这些文件。只有所有 SDK6 信号都存在时,才能将项目视为 SDK6——部分匹配通常意味着这是一个只迁移了一半的项目,需要采用不同的处理方式。
| 检查项 | SDK6 信号 | SDK7 信号 |
|---|---|---|
| | |
| | |
| 没有 | |
| 源代码 imports | 没有显式的 | 大多数内容从 |
| 源代码模式 | | |
RULE: Do NOT change scene.json
's main
field
scene.jsonmain规则:不要修改 scene.json
的 main
字段
scene.jsonmainKeep the value the SDK6 project already had (typically ). The SDK7 default for new scenes is , but writes the bundle to whatever path names (verified: sets the output file from ) — so an existing keeps working with no changes. Do NOT rename it to unless the user explicitly asks.
main"bin/game.js"bin/index.jssdk-commands buildscene.jsonmainsdk-commandsbundle.tssceneJson.mainbin/game.jsbin/index.jsYou SHOULD add to (and remove anything ECS6-specific). See [[create-scene]] for the full scene.json schema.
"runtimeVersion": "7"scene.json保留 SDK6 项目原有的 值(通常是 )。新场景在 SDK7 中的默认值为 ,但 会将 bundle 写入 中 指定的路径(已验证: 的 根据 设置输出文件)——因此现有的 无需修改即可继续工作。除非用户明确要求,否则不要将其重命名为 。
main"bin/game.js"bin/index.jssdk-commands buildscene.jsonmainsdk-commandsbundle.tssceneJson.mainbin/game.jsbin/index.js你应该向 中添加 (并移除所有 ECS6 专属内容)。完整的 schema 请参见 [[create-scene]]。
scene.json"runtimeVersion": "7"scene.jsonRULE: The composite-first rule applies to migrations too
规则:Composite 优先规则同样适用于迁移
After migration, all static scenery (models, lights, primitives, text placed at scene load) belongs in , NOT in TypeScript. SDK6 scenes have no composite — every entity is created in code. Migrating those into a composite is a separate, optional pass; the minimum-viable migration leaves them as in TypeScript so the scene runs first.
assets/scene/main.compositeengine.addEntity()Order of operations:
- Get the SDK7 TypeScript port running (entities still created in code).
- THEN, optionally, move static entities into . See [[create-scene]] for the composite-first rule and [[composites]] for the format.
assets/scene/main.composite
迁移完成后,所有静态场景内容(模型、灯光、图元以及在场景加载时放置的文本)都应放在 中,而不是放在 TypeScript 中。SDK6 场景没有 composite——每个 Entity 都是通过代码创建的。将这些内容迁移到 composite 属于单独的、可选步骤;最低可行的迁移会将它们保留为 TypeScript 中的 ,以确保场景首先能够正常运行。
assets/scene/main.compositeengine.addEntity()操作顺序:
- 首先让 SDK7 TypeScript 移植版本能够运行起来(Entity 仍由代码创建)。
- 然后,再选择性地将静态 Entity 移入 。有关 Composite 优先规则,请参见 [[create-scene]];有关格式,请参见 [[composites]]。
assets/scene/main.composite
The Conceptual Shift — read this before writing any code
概念转变——编写任何代码前请先阅读本节
SDK6 modeled entities as objects with components attached to them. SDK7 is a true ECS where entities are just opaque IDs and components are pure data.
| Concept | SDK6 | SDK7 |
|---|---|---|
| Entity creation | | |
| Entity type | | |
| Adding a component | | |
| Defining a component | | |
Component methods ( | Allowed (class methods) | Forbidden — components are data only. Move methods into free functions or systems |
| Reading component data | | |
| Modifying component data | Mutate the returned reference | MUST use |
| Parenting | | Set |
| Defining a system | | |
| Querying entities | | |
| Removing an entity | | |
| Event callbacks on components | | |
| Static "scene-level" objects | | |
Mutability is a performance contract, not just style. Always prefer (read) over (write). Using when you only read causes a CRDT delta to be sent every frame.
.get().getMutable().getMutable()SDK6 将 Entity 建模为附加了 component 的对象。SDK7 是真正的 ECS,其中 Entity 只是不透明的 ID,而 component 是纯粹的数据。
| 概念 | SDK6 | SDK7 |
|---|---|---|
| 创建 Entity | | |
| Entity 类型 | 带方法的 | |
| 添加 component | | |
| 定义 component | | |
component 方法( | 允许使用类方法 | 禁止使用——component 仅包含数据。将方法移至独立函数或 system 中 |
| 读取 component 数据 | | |
| 修改 component 数据 | 修改返回的引用 | 必须使用 |
| 设置父级 | | 设置子 Entity 的 Transform 的 |
| 定义 system | | |
| 查询 Entity | | |
| 移除 Entity | | |
| component 上的事件回调 | | |
| 静态“场景级”对象 | | |
可变性不仅是一种代码风格,也是性能约定。 读取时始终优先使用 ,仅在写入时使用 。如果只进行读取却使用 ,会导致每帧都发送一次 CRDT delta。
.get().getMutable().getMutable()API Mapping — quick reference
API 映射——快速参考
See for the full table. The most common conversions:
{baseDir}/references/api-mapping.md| SDK6 | SDK7 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
Custom per-frame | Delete it — use |
完整对照表请参见 。最常见的转换如下:
{baseDir}/references/api-mapping.md| SDK6 | SDK7 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
在 Entity 上使用 | |
| 在 system 内使用 |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
累加 | 删除它——改用 |
Migration Workflow
迁移工作流
Do this in order. Skipping steps leads to a broken scene that's hard to debug.
请严格按以下顺序执行。跳过步骤会生成难以调试的损坏场景。
1. Audit the SDK6 project
1. 审计 SDK6 项目
Read every source file. Build a list of:
- Every custom class — these become
@Componentwith a flat schema. Class methods (engine.defineComponent(...), etc.) must be lifted out into free functions or systems.reset() - Every — these become free functions
class X implements ISystem.(dt: number) => void - Every /
OnPointerDown/OnPointerUp— these becomeInput.instance.subscribecalls orpointerEventsSystemin a system.inputSystem.isTriggered() - Every ,
GLTFShape, etc. — see mapping table.BoxShape - Every ,
Camera.instance— these become static engine entities + component reads.Input.instance - All calls — these become
setParentfield assignments.Transform.parent
This list is the completion checklist for steps 5–10 — each of those steps is done only when every item in its category has been ported and its SDK6 symbols no longer appear in the source.
读取每一个源文件,并建立以下清单:
- 每个自定义 类——这些需要转换为带有扁平 schema 的
@Component。类方法(engine.defineComponent(...)等)必须提取为独立函数或 system。reset() - 每个 ——这些需要转换为独立函数
class X implements ISystem。(dt: number) => void - 每个 /
OnPointerDown/OnPointerUp——这些需要转换为Input.instance.subscribe调用,或在 system 中使用pointerEventsSystem。inputSystem.isTriggered() - 每个 、
GLTFShape等——参见映射表。BoxShape - 每个 、
Camera.instance——这些需要转换为静态 Engine Entity + component 读取。Input.instance - 所有 调用——这些需要转换为
setParent字段赋值。Transform.parent
此清单是步骤 5–10 的完成检查表——只有在每个类别中的所有项目都已移植,且其 SDK6 符号不再出现在源代码中时,才能认为相应步骤已完成。
2. Replace package.json
, tsconfig.json
, and update scene.json
package.jsontsconfig.jsonscene.json2. 替换 package.json
、tsconfig.json
并更新 scene.json
package.jsontsconfig.jsonscene.json- Replace with
decentraland-ecs(latest).@dcl/sdk - Replace /
build-ecsscripts withdcl start/sdk-commands start.sdk-commands build - Replace the old with the standard SDK7 one (
tsconfig.json,target: ES2020,module: ESNext,jsx: react-jsx).strict: true - Add to
"runtimeVersion": "7". Do NOT modifyscene.jsonunless the user asks.main
- 将 替换为最新版的
decentraland-ecs。@dcl/sdk - 将 /
build-ecs脚本替换为dcl start/sdk-commands start。sdk-commands build - 使用标准 SDK7 替换旧配置(
tsconfig.json、target: ES2020、module: ESNext、jsx: react-jsx)。strict: true - 向 中添加
scene.json。除非用户提出要求,否则不要修改"runtimeVersion": "7"。main
3. Reorganize asset folders under assets/
assets/3. 将资源文件夹重新组织到 assets/
下
assets/SDK6 scenes commonly kept assets in top-level folders at the project root: , , (or ), , . SDK7 + Creator Hub expects assets under a top-level folder so the visual editor can index them. Do this move as a discrete step BEFORE porting any code — that way the path rewrites in later steps can target the final locations directly.
models/images/audio/sounds/textures/videos/assets/- Identify every top-level asset folder in the SDK6 project. Common SDK6 names: ,
models/,images/,audio/,sounds/,textures/.videos/ - Create the new structure under using the capitalized category convention used elsewhere in these skills (see [[create-scene]], [[add-3d-models]], [[audio-video]]):
assets/- →
models/assets/Models/ - →
images/assets/Images/ - or
audio/→sounds/assets/Audio/ - →
videos/assets/Videos/ - →
textures/(textures are images; consolidate unless the user wants a separate folder)assets/Images/
- Move files into the new structure. Do NOT leave duplicate copies — delete the old top-level folders once the move is complete. Dual copies cause Creator Hub to index stale paths and roughly double scene size on deploy (both copies get bundled).
- Rewrite every code reference to the moved paths. Grep the entire source tree for the old folder names and update each hit:
- →
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' - Any string literal mentioning the old folder name anywhere in code, JSON, or files.
.composite
- Done when: grepping the project for the old folder names returns zero remaining references, and opening the scene in Creator Hub shows the asset tree populated from .
assets/
Exception — reuse existing layout if present. If the project already contains (the legacy Creator Hub layout) or / (Creator Hub adds these for asset-pack items and custom items), reuse those paths instead of creating a parallel . The rule is: one canonical location per asset type — don't fragment.
assets/scene/Models/assets/asset-packs/assets/custom/assets/Models/SDK6 场景通常将资源保存在项目根目录下的顶级文件夹中:、、(或 )、、。SDK7 + Creator Hub 期望资源位于顶级 文件夹下,以便可视化编辑器能够建立索引。应在移植任何代码之前将移动操作作为一个独立步骤完成,这样后续步骤中的路径重写可以直接使用最终位置。
models/images/audio/sounds/textures/videos/assets/- 识别 SDK6 项目中的每个顶级资源文件夹。 常见名称包括 、
models/、images/、audio/、sounds/、textures/。videos/ - 使用这些 skill 中采用的大写分类约定,在 下创建新结构(参见 [[create-scene]]、[[add-3d-models]]、[[audio-video]]):
assets/- →
models/assets/Models/ - →
images/assets/Images/ - 或
audio/→sounds/assets/Audio/ - →
videos/assets/Videos/ - →
textures/(纹理属于图片;除非用户希望单独保留文件夹,否则应合并)assets/Images/
- 将文件移入新结构。 不要保留重复副本——移动完成后应删除旧的顶级文件夹。重复副本会导致 Creator Hub 对过时路径建立索引,并使部署时的场景大小大约增加一倍(两份副本都会被打包)。
- 重写所有指向已移动路径的代码引用。 在整个源代码树中 grep 旧文件夹名称,并更新每一处匹配:
- →
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
- 完成标准: 在项目中 grep 旧文件夹名称应返回零个残留引用,并且在 Creator Hub 中打开场景时,资源树应正确显示来自 的内容。
assets/
例外——如果现有布局已经存在,则继续使用。 如果项目已经包含 (旧版 Creator Hub 布局),或 / (Creator Hub 为 asset-pack 项目和 custom 项目添加的目录),则应继续使用这些路径,而不是创建一套并列的 。原则是:每种资源类型只能有一个规范位置——不要造成目录碎片化。
assets/scene/Models/assets/asset-packs/assets/custom/assets/Models/4. Create src/index.ts
with an exported main()
src/index.tsmain()4. 创建包含导出式 main()
的 src/index.ts
main()src/index.tsSDK7 scenes start from a top-level exported function. The convention is to put most code in another file (e.g. ) and have just call into it:
main()src/game.tsindex.tstypescript
import { initializeGame } from "./game";
export function main() {
initializeGame();
}SDK7 场景从一个顶层导出的 函数开始。通常应将大部分代码放在另一个文件中(例如 ),并让 仅负责调用它:
main()src/game.tsindex.tstypescript
import { initializeGame } from "./game";
export function main() {
initializeGame();
}5. Port components first
5. 首先移植 component
Convert each class to . Flatten nested types — does not support arbitrary classes, but does support , , primitives, arrays, and nested . A field in SDK6 typically becomes two scalar fields (, ) in SDK7 — see the 2048 example in .
@Componentengine.defineComponent(name, schema, defaults)SchemasSchemas.Vector3Schemas.QuaternionSchemas.Map(...)Vector2posXposY{baseDir}/references/migration-example.mdDone when: returns zero hits and every component on the step-1 audit list has a corresponding call.
grep -rn "@Component(" src/engine.defineComponent将每个 类转换为 。扁平化嵌套类型—— 不支持任意类,但支持 、、基本类型、数组以及嵌套的 。SDK6 中的 字段通常需要转换为两个标量字段(、)——参见 中的 2048 示例。
@Componentengine.defineComponent(name, schema, defaults)SchemasSchemas.Vector3Schemas.QuaternionSchemas.Map(...)Vector2posXposY{baseDir}/references/migration-example.md完成标准: 返回零个匹配,并且步骤 1 审计清单中的每个 component 都有对应的 调用。
grep -rn "@Component\\(" src/engine.defineComponent6. Port systems
6. 移植 system
Each becomes a free function. Replace iteration with . Inside the loop, use only when writing.
class X implements ISystem { update(dt) {...} }engine.getComponentGroup(A, B).entitiesfor (const [entity, a, b] of engine.getEntitiesWith(A, B)).getMutable(entity)Done when: returns zero hits and every system on the step-1 audit list is registered via .
grep -rnE "implements ISystem|getComponentGroup" src/engine.addSystem每个 都应转换为一个独立函数。将 遍历替换为 。在循环内部,仅在写入时使用 。
class X implements ISystem { update(dt) {...} }engine.getComponentGroup(A, B).entitiesfor (const [entity, a, b] of engine.getEntitiesWith(A, B)).getMutable(entity)完成标准: 返回零个匹配,并且步骤 1 审计清单中的每个 system 都通过 注册。
grep -rnE "implements ISystem|getComponentGroup" src/engine.addSystem7. Port the entity/component setup (the bulk of game.ts
)
game.ts7. 移植 Entity/component 设置(game.ts
中的主体部分)
game.tsFor each block, replace with and a series of calls. Convert to a field on the Transform. Use the new , , paths established in step 3 for every / / .
new Entity()engine.addEntity()Component.create(entity, ...)setParentparentassets/Models/...assets/Audio/...assets/Images/...srcaudioClipUrltexture.srcDone when: returns zero hits, and no / / texture path still references a pre-step-3 folder.
grep -rnE "new Entity\(|\.addComponent\(|\.setParent\(" src/srcaudioClipUrl对于每个 代码块,替换为 ,并使用一系列 调用。将 转换为 Transform 的 字段。所有 / / 纹理路径都应使用步骤 3 中建立的 、、 路径。
new Entity()engine.addEntity()Component.create(entity, ...)setParentparentsrcaudioClipUrlassets/Models/...assets/Audio/...assets/Images/...完成标准: 返回零个匹配,并且没有任何 / / 纹理路径仍引用步骤 3 之前的文件夹。
grep -rnE "new Entity\\(|\\.addComponent\\(|\\.setParent\\(" src/srcaudioClipUrl8. Port input/pointer handlers
8. 移植输入和指针事件处理器
- Per-entity click handlers () →
new OnPointerDown(...). Add a collider if the entity doesn't already have one (pointerEventsSystem.onPointerDown({ entity, opts }, handler)orMeshCollider.setBox(entity)onvisibleMeshesCollisionMask: 1). See [[add-interactivity]].GltfContainer - Global key subscriptions () → use
Input.instance.subscribeinside a system. See [[advanced-input]].inputSystem.isTriggered(InputAction.IA_X, PointerEventType.PET_DOWN)
Done when: returns zero hits and every handler on the step-1 audit list has a or counterpart.
grep -rnE "OnPointerDown|OnPointerUp|Input\.instance|ActionButton\." src/pointerEventsSysteminputSystem- 每 Entity 的点击处理器()→
new OnPointerDown(...)。如果 Entity 尚无 collider,则添加一个(pointerEventsSystem.onPointerDown({ entity, opts }, handler),或在MeshCollider.setBox(entity)上设置GltfContainer)。参见 [[add-interactivity]]。visibleMeshesCollisionMask: 1 - 全局按键订阅()→ 在 system 内使用
Input.instance.subscribe。参见 [[advanced-input]]。inputSystem.isTriggered(InputAction.IA_X, PointerEventType.PET_DOWN)
完成标准: 返回零个匹配,并且步骤 1 审计清单中的每个处理器都有对应的 或 实现。
grep -rnE "OnPointerDown|OnPointerUp|Input\\.instance|ActionButton\\." src/pointerEventsSysteminputSystem9. Port animations
9. 移植动画
new Animator()new AnimationState('clip')animator.addClip(...)Animator.create(entity, { states: [{ clip, playing, loop }] })Animator.playSingleAnimation(entity, 'clip')Animator.stopAllAnimations(entity)Done when: returns zero hits, and every clip name that appears in an SDK6 / call is listed in some array — clips missing from fail silently (see the pitfall in Common Pitfalls).
grep -rnE "new Animator|AnimationState|addClip|getClip" src/playAnimationgetClipstates[]states[]playSingleAnimationnew Animator()new AnimationState('clip')animator.addClip(...)Animator.create(entity, { states: [{ clip, playing, loop }] })Animator.playSingleAnimation(entity, 'clip')Animator.stopAllAnimations(entity)完成标准: 返回零个匹配,并且 SDK6 / 调用中出现的每个 clip 名称都列在某个 数组中——遗漏在 中的 clip 会静默失败(参见“常见陷阱”中的 陷阱)。
grep -rnE "new Animator|AnimationState|addClip|getClip" src/playAnimationgetClipstates[]states[]playSingleAnimation10. Port sounds
10. 移植声音
new AudioClip(url)entity.addComponent(new AudioSource(clip))AudioSource.create(entity, { audioClipUrl: url, playing: false, ... })AudioSource.getMutable(entity).playing = trueDone when: returns zero hits.
grep -rnE "new AudioClip|new AudioSource\(" src/new AudioClip(url)entity.addComponent(new AudioSource(clip))AudioSource.create(entity, { audioClipUrl: url, playing: false, ... })AudioSource.getMutable(entity).playingtrue完成标准: 返回零个匹配。
grep -rnE "new AudioClip|new AudioSource\\(" src/11. Verify and test
11. 验证和测试
Run to fetch the new SDK, then (or ). The migration is complete only when ALL of these hold:
npm installsdk-commands startnpm start- exits 0.
npm run build - A final sweep returns zero hits — per-step greps get missed when work is interrupted and resumed; this is the end-to-end check.
grep -rnE "decentraland-ecs|@Component\(|implements ISystem|Input\.instance|Camera\.instance" src/ - The preview loads with no errors in the console.
- The visible layout (positions/rotations) matches the SDK6 scene.
If any check fails, return to the step that owns that symbol category — do NOT report the migration as done.
运行 获取新 SDK,然后运行 (或 )。只有满足以下所有条件,迁移才算完成:
npm installsdk-commands startnpm start- 以状态码 0 退出。
npm run build - 最终全面检查 返回零个匹配——工作中断后恢复时,分步骤 grep 很容易被遗漏,因此必须进行这次端到端检查。
grep -rnE "decentraland-ecs|@Component\\(|implements ISystem|Input\\.instance|Camera\\.instance" src/ - 预览加载时控制台中没有错误。
- 可见布局(位置/旋转)与 SDK6 场景一致。
如果任何检查失败,应返回到负责处理相应符号类别的步骤——不要报告迁移已经完成。
Common Pitfalls
常见陷阱
- Mutating a result:
.get()is silently a no-op. UseTransform.get(entity).position.x = 5.Transform.getMutable(entity).position.x = 5 - Passing explicit /
rotation: undefinedtoscale: undefinedcrashes the serializer: symptom isTransform.createat serialization. Why (verified —"Cannot read properties of undefined (reading 'x')"):@dcl/ecs/dist/components/manual/Transform.jsruns the value throughTransform.create, which spreadsTransformSchema.extendAFTER its defaults, so an explicit...valuefield overrides the default identity rotation / unit scale and is stored asundefined; the binaryundefinedthen readsserializeand throws. This bites when a port builds a Transform from a partial object that may carryvalue.rotation.xfields (e.g.undefined). Fix — omit the field entirely instead of passingTransform.create(e, { position, rotation: maybeRot, scale: maybeScale })so the schema applies its default:undefinedtypescriptconst out: Partial<TransformType> = { position } if (rotation) out.rotation = rotation if (scale) out.scale = scale if (parent !== undefined) out.parent = parent Transform.create(entity, out) - Forgetting the schema: REQUIRES a schema. SDK6 class fields with no type cannot be auto-inferred.
engine.defineComponent - Component methods: SDK6 components frequently had a or constructor logic. These MUST be moved to free functions in SDK7 — components are pure data.
reset() - Vector2 fields: does NOT have
Schemas. [UNVERIFIED — confirm by reading the Schemas object exports.] Flatten to twoSchemas.Vector2fields, or useSchemas.Number.Schemas.Map({ x: Schemas.Number, y: Schemas.Number }) - on the entity object: Entities are integers in SDK7 — there is no method. Set the
setParentfield on the child'sparent.Transform - Color3 vs Color4: Most SDK7 component fields take (with alpha). Replace
Color4calls withColor3.X()unless the field is documented as Color3.Color4.X() - Materials are not entities or objects: There is no instance you keep a reference to.
new Material()is the only way to set it — a material lives on one entity, not as a shared object.Material.setPbrMaterial(entity, props) - Shape entities (BoxShape, PlaneShape, etc.) need MeshRenderer AND MeshCollider: In SDK6 a single did both render and collide (with a flag). In SDK7 these are two separate components — add both if you need clicks/physics on a primitive.
BoxShape - GLTFShape had its visible mesh clickable by default with : In SDK7,
withCollisions: trueby default only has collisions on the invisible _collider mesh. Some models may not be ready for that. If an entity is no longer clickable from a lack of collider, it may need explicitGltfContainerset to a value includingvisibleMeshesCollisionMask(1) for clicks, orCL_POINTER(2) for player collision, orCL_PHYSICSfor both. See [[add-3d-models]].3 - callbacks no longer fire inside a
OnPointerDownover a component group: In SDK6 they were event-driven; in SDK7 you register the handler once withforEach— don't put that call inside a system's update loop or you'll register a new handler every frame.pointerEventsSystem.onPointerDown(...) - silently no-ops on clips not in
Animator.playSingleAnimation(porters who built the Animator in code): this applies specifically to the SDK6-port path where you author theAnimator.statesyourself in TypeScript and callAnimatorprogrammatically. In that case,Animator.playSingleAnimation(entity, clipName)returnsplaySingleAnimationand does nothing iffalseisn't already present in the entity'sclipNamearray (verified —stateslines 35-46). No warning. SDK6 hid this —@dcl/ecs/dist-cjs/components/extended/Animator.jsauto-created the clip on first use, so SDK6 scenes routinely callAnimator.getClip(name)without declaringplayAnimation('Appear')upfront. A naive port that maps'Appear'→entity.getComponent(Animator).getClip(name).play()ends up calling it against clip names the porter never added toAnimator.playSingleAnimation(entity, name), and those calls silently fail. Fix: walk everystates[]/playAnimationcall in the SDK6 source, collect the full set of clip names per entity, and list them all in thegetCliparray atstatestime. For a quick port-friendly shim that restores SDK6's "play any clip, no setup" ergonomics with a lazyAnimator.create, see the wrapper in [[animations-tweens]] (PITFALL section). This rule does not apply to scenes whose Animator was written by the Creator Hub Inspector (which pre-populatesstates.pushwith every GLB clip) or by asset packs / smart items (which ship withstates[]populated), nor to models with nostatesat all — those rely on the autoplay path in the next bullet.Animator - Unnecessary serialization overhead when porting to a hand-rolled SDK7 helper called every frame: applies when you replace SDK6's
anim.stop(); anim.play()with a custom SDK7 helper that directly mutatesanim.stop(); anim.play()(Animator.states,s.playing) AND that helper is invoked from a per-frame caller — a system update, or an input/raycast callback that fires every tick the input is held. The animation will NOT freeze (the CRDT unchanged-mutable suppression layer ins.shouldResetdetects identical writes and silently drops them), but calling@dcl/ecsevery tick incurs unnecessary serialization + byte-comparison overhead each frame. Fix: track the last-applied clip per entity and short-circuit the helper when unchanged; on agetMutableOrNull()re-call with the same clip, only refresh the revert timer instead of re-mutating states; clear the tracked clip before re-applying the idle clip from the revert callback. Does NOT apply when the helper is called only on a state transition (one-shotnoLoop + revertToIdle, edge-triggered state machine), nor when you useonPointerDownfrom a transition rather than a per-frame caller. See the "Short-circuit clip-switch helpers" best practice in [[animations-tweens]] for the full helper pattern.Animator.playSingleAnimation - autoplays one clip from a multi-animation .glb when no
GltfContaineris attached: SDK6 models stayed in their bind pose by default; SDK7 picks one clip from the .glb and plays it. This is the same mechanism that lets clip-less Inspector scenes work without registration — it's the complement to the previous bullet, not a contradiction. When the entity has noAnimator, the renderer autoplays; when the entity has anAnimatorand you callAnimator, the clip must be inplaySingleAnimation. Which clip the renderer autoplays is not stable across models — observed cases include a Halloween scene where small ghosts with nostates[]autoplayedAnimatorstraight from the GLB. Fix when you want a different default: attach andieto take control. Fix when you also want to switch clips at runtime viaAnimator.create(entity, { states: [{ clip: '<intended-default>', playing: true, loop: true }] }): list every clip you'll switch to inplaySingleAnimation(see previous bullet).states[] - with
Input.instance.subscribe/BUTTON_DOWN: there is no direct callback equivalent. UseBUTTON_UPinside a system for "just pressed this frame".inputSystem.isTriggered() - trigger areas (the "Utils library") AND hand-rolled per-frame AABB checks: do NOT port the custom per-frame position-checking system, and do NOT recreate it from scratch in SDK7. SDK6 had no native trigger area, so the community Utils library (and many scenes' bespoke code) polled
@dcl/ecs-scene-utilsagainst a box/sphere region every frame and firedCamera.instance.position/onCameraEnter. SDK7 has a nativeonCameraExitcomponent (TriggerArea) — replace the entire pattern with@dcl/sdk/ecs(orTriggerArea.setBox(entity)) +.setSphere/triggerAreaEventsSystem.onTriggerEnter(entity, cb)/.onTriggerExit. Size comes from.onTriggerStay, position fromTransform.scale, and the parent chain is respected — so triggers parented to a rotated/offset scene root Just Work without manual coordinate transforms. Behavior parity: the Utils library only ever fired for the local player; nativeTransform.positiondefaults toTriggerArea, which fires for ANY player on that layer. The correct guard for local-player-only behavior isColliderLayer.CL_PLAYER(NOTif (result.trigger?.entity !== engine.PlayerEntity) return— despite the confusing name,result.triggeredEntityis the trigger area's own entity, so comparing it to the player is always true and the guard never fires). See the "Trigger areas" section oftriggeredEntityand [[add-interactivity]] for the full component reference.{baseDir}/references/api-mapping.md - /
utils.Delayfromutils.ExpireIn(laterdecentraland-ecs-utils) AND hand-rolled per-frame timer systems: do NOT port the community@dcl-sdk/utilscomponent, do NOT recreate it in SDK7, and do NOT write a customDelayhelper backed by asetSceneTimeoutthat accumulatestimerSystem. Use thedtnamed export fromtimers(@dcl/sdk/ecs) — it providesimport { timers } from '@dcl/sdk/ecs'/setTimeout/clearTimeout/setIntervalbound to the scene engine. ReplaceclearIntervalwithentity.addComponent(new utils.Delay(2000, cb))and delete the "timer entity" the SDK6 scene created just to host the Delay component. Argument order istimers.setTimeout(cb, 2000), NOT(callback, ms)— the SDK6(ms, callback)order is the opposite of the JS standard, and custom helpers written with the SDK6 order are a known footgun that compounds the original mistake of writing the helper in the first place. Do NOT use the native JSDelay(ms, cb)/setTimeoutglobals — although QuickJS exposes them, they are not bound to the scene engine and can introduce subtle problems; always go throughsetInterval. See [[scene-runtime]] (Timers section) for the full reference.timers - SDK6 "intercepting tool" smart-items (keys, magnets, inventory items that act on a clicked target via + entity-hierarchy walking) have no direct SDK7 equivalent.
Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, true, …)is per-entity only — there is no global hit-result subscriber. The migration pattern is a small registry keyed by target entity name; the target's ownpointerEventsSystem.onPointerDownconsults the registry before falling back to its default behavior. SeepointerEventsSystem.onPointerDown.{baseDir}/references/api-mapping.md#click-interception-across-entities-keytool-acting-on-a-target - ,
Vector3.GetAngleBetweenVectorsetc.: many SDK6 Vector3 helpers existed as static methods. SDK7 keeps most underVector3.Up()but capitalization changed (e.g.@dcl/sdk/math→Vector3.Lerp). Grep forVector3.lerpafter migration and verify each call against the math module.Vector3. - is not exported in SDK7: replace with
log().console.log() - /
Attachable.FIRST_PERSON_CAMERAon held items (guns, aiming reticles, flashlights, fixed-position tools): SDK6 only offered two coarse follow modes —Attachable.AVATAR(follow the camera) andAttachable.FIRST_PERSON_CAMERA(follow the avatar root). In SDK7 you have a new choice that didn't exist before: bone-levelAttachable.AVATARwithAvatarAttach(e.g.anchorPointId). The bone anchor looks like the natural SDK7 equivalent of "put it in the avatar's hand", but bone anchors inherit avatar animation (idle bob, walk cycle, gestures) — a gun attached toAAPT_RIGHT_HANDjitters every frame and is unaimable. The correct SDK7 mapping for held gameplay items is to parent the entity viaAAPT_RIGHT_HAND, notTransform.parent. For any aim-sensitive item (gun, reticle, flashlight, anything the player should be able to point by looking around), the right SDK7 default isAvatarAttach— this mirrors SDK6'sTransform.parent = engine.CameraEntityand tracks both yaw AND pitch, so the muzzle aims where the camera looks.FIRST_PERSON_CAMERAis a different choice and a common subtle failure mode: it follows the player root (feet position + body yaw only, no pitch and no animation), so a gun parented toTransform.parent = engine.PlayerEntitystays flat when the player tilts the camera up to aim — the item ignores the look direction. UsePlayerEntityonly for body-fixed items that should NOT track aim (a held shield, a static torch, a non-aimed inventory item carried at hip-level). ReservePlayerEntityfor cosmetic items where riding the animation is desired (hats, halos, backpacks, name plates). Picking by intent —AvatarAttach(aim-sensitive, the recommended default for held weapons),engine.CameraEntity(yaw-only / no pitch, for body-fixed items),engine.PlayerEntity(cosmetic only, animates with the bone). See the "Held items vs cosmetic items" section of [[player-avatar]] for the comparison table and a worked gun example.AvatarAttach(boneId) - Billboarded models may appear facing AWAY from the camera after porting: observed in some SDK6 → SDK7 ports — a model that displayed its front face under SDK6's (Y-axis billboard) shows its back to the camera under SDK7's
new Billboard(false, true, false)with the same parent/child setup. The two SDKs appear to disagree on which model face (+Z vs -Z) the billboard points at the camera. The SDK7Billboard.create(e, { billboardMode: BillboardMode.BM_Y })interface has noPBBillboardflag — the only field isoppositeDirection(verified againstbillboardMode@dcl/ecs), so the fix is in the model's Transform, not the Billboard component. Rotate the displayed model 180° on Y (billboard.gen.d.ts). If the entity is a parent (Quaternion.fromEulerDegrees(0, 180, 0)'d) + a child model, apply the 180° rotation to the child'sBillboard— the parent entity's rotation is owned by the Billboard component and overwriting it on the billboarded entity has no effect. [UNVERIFIED whether this was an explicit SDK behavior change with a changelog entry, or a consequence of a separate convention change in the model importer — phrase any user-facing explanation as "the two SDKs disagree on billboard facing direction" rather than claiming a specific version introduced it.] See [[advanced-rendering]] (Billboard) for the SDK7 component reference.Transform.rotation - UI pixel sizes do NOT carry over 1:1: SDK6 UI (/
UICanvas/UIImage) was authored in raw pixels against a fixed screen size, so positions and sizes likeUITextorwidth = 200were absolute. SDK7 React-ECS UI lays out in raw screen pixels by default, but the actual screen size depends on the user's machine — so an SDK6 layout copied over verbatim will appear at a different relative size on every display. The fix is to set a virtual canvas onpositionX = -350, which makes the engine treat your pixel values as coordinates inside that virtual space and scale to the real screen. Before porting UI, check the SDK6 source (the legacy ECS lives at https://github.com/decentraland/ecs) to see what pixel grid the original UI was designed against, and pass that as the virtual size:ReactEcsRenderer.setUiRenderertsReactEcsRenderer.setUiRenderer(uiRoot, { virtualWidth: 1920, virtualHeight: 1080 })is a reasonable default to show, but the correct virtualWidth/virtualHeight depends on the resolution the SDK6 UI was authored against — if the SDK6 scene assumed a different reference resolution (e.g.1920x1080), use those numbers instead so existing pixel coordinates land in the same place. See [[build-ui]] for the full React-ECS rewrite.1280x720
- 修改 的结果:
.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时抛出异常。当移植代码从不完整对象构建 Transform,而该对象可能包含value.rotation.x字段时(例如undefined),就会遇到此问题。修复方法——完全省略该字段,不要传入Transform.create(e, { position, rotation: maybeRot, scale: maybeScale }),从而让 schema 应用默认值:undefinedtypescriptconst 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: 必须包含 schema。SDK6 类中无法推断类型的字段不能自动推断。
engine.defineComponent - Component 方法: SDK6 component 经常包含 或构造函数逻辑。这些逻辑必须在 SDK7 中移至独立函数——component 仅包含纯数据。
reset() - Vector2 字段: 没有
Schemas。[未验证——请通过读取 Schemas 对象导出来确认。] 应将其扁平化为两个Schemas.Vector2字段,或使用Schemas.Number。Schemas.Map({ x: Schemas.Number, y: Schemas.Number }) - 在 Entity 对象上调用 : SDK7 中的 Entity 是整数,因此不存在该方法。请设置子 Entity 的
setParent上的Transform字段。parent - Color3 与 Color4: 大多数 SDK7 component 字段接受 (包含 alpha)。除非字段明确记录为 Color3,否则应将
Color4替换为Color3.X()。Color4.X() - Material 既不是 Entity,也不是可保留引用的对象: 不存在需要保留引用的 实例。设置 Material 的唯一方式是
new Material()——Material 附着在一个 Entity 上,而不是共享对象。Material.setPbrMaterial(entity, props) - 形状 Entity(BoxShape、PlaneShape 等)需要同时使用 MeshRenderer 和 MeshCollider: SDK6 中单个 同时负责渲染和碰撞(带一个标志)。在 SDK7 中它们是两个独立的 component——如果图元需要支持点击或物理交互,应同时添加两者。
BoxShape - 在 SDK6 中,使用 的
withCollisions: true默认可以点击可见 mesh: SDK7 中,GLTFShape默认仅在不可见的GltfContainermesh 上启用碰撞,某些模型可能尚未针对此机制进行配置。如果 Entity 因缺少 collider 而无法点击,可能需要显式设置_collider,使其包含visibleMeshesCollisionMask(1)以支持点击、CL_POINTER(2)以支持玩家碰撞,或设为CL_PHYSICS同时支持两者。参见 [[add-3d-models]]。3 - 在遍历 component group 时,回调不再触发: SDK6 中这些回调由事件驱动;SDK7 中应一次性通过
OnPointerDown注册处理器——不要在 system 的 update 循环中调用该方法,否则每帧都会注册一个新处理器。pointerEventsSystem.onPointerDown(...) - 对于自行在代码中创建 Animator 的移植场景,对未包含在
Animator.playSingleAnimation中的 clip 会静默无效: 这 specifically 适用于 SDK6 移植路径,即你在 TypeScript 中自行创建Animator.states,并以编程方式调用Animator。此时,如果Animator.playSingleAnimation(entity, clipName)尚未存在于 Entity 的clipName数组中,states会返回playSingleAnimation且不执行任何操作(已验证——false第 35–46 行)。系统不会发出警告。SDK6 中,@dcl/ecs/dist-cjs/components/extended/Animator.js会在首次使用时自动创建 clip,因此 SDK6 场景经常在未预先声明Animator.getClip(name)的情况下直接调用'Appear'。如果简单地移植playAnimation('Appear')→entity.getComponent(Animator).getClip(name).play(),最终会针对移植者从未加入Animator.playSingleAnimation(entity, name)的 clip 名称调用该方法,从而静默失败。修复方法: 遍历 SDK6 源代码中的每个states[]/playAnimation调用,为每个 Entity 收集完整的 clip 名称集合,并在创建getClip时将它们全部列入Animator数组。如需快速迁移的兼容层,以恢复 SDK6“无需配置即可播放任意 clip”的使用体验并支持延迟添加states,请参见 [[animations-tweens]] 的 PITFALL 部分。此规则不适用于由 Creator Hub Inspector 创建 Animator 的场景(Inspector 会预先将每个 GLB clip 填入states.push),也不适用于随附已填充states[]的 asset pack / smart item,或完全不存在 Animator 的模型——后两者依赖下一条所述的自动播放机制。states[] - 将 移植为每帧调用的自定义 SDK7 helper 会产生不必要的序列化开销: 当使用自定义 SDK7 helper 直接修改
anim.stop(); anim.play()(Animator.states、s.playing),并且该 helper 由逐帧调用方——system update,或按键持续按下时触发的输入/射线检测回调——调用时,就会出现此问题。动画不会冻结(s.shouldReset中针对可变 CRDT 的抑制层会检测到相同的写入并静默丢弃),但每帧调用@dcl/ecs都会产生不必要的序列化和字节比较开销。修复方法: 按 Entity 跟踪最后应用的 clip,如果没有变化则提前返回;对于使用相同 clip 的getMutableOrNull()重新调用,只刷新回退计时器,而不是再次修改 states;在重新应用 idle clip 前清除已跟踪的 clip。不适用于仅在状态切换时调用一次 helper 的情况(一次性noLoop + revertToIdle、边沿触发的状态机),也不适用于从状态切换调用onPointerDown而非逐帧调用 helper 的情况。完整 helper 模式请参见 [[animations-tweens]] 中的“Short-circuit clip-switch helpers”最佳实践。Animator.playSingleAnimation - 当未附加 时,
Animator会自动播放多动画GltfContainer中的一个 clip: SDK6 模型默认保持绑定姿势;SDK7 会从.glb中选取一个 clip 并播放。该机制使无 clip 的 Inspector 场景无需注册即可工作,因此它与上一条并不矛盾。当 Entity 没有.glb时,渲染器会自动播放;当 Entity 有Animator且调用Animator时,该 clip 必须位于playSingleAnimation中。渲染器自动播放哪个 clip 并不会因模型不同而保持稳定——曾观察到一个万圣节场景,其中没有states[]的小幽灵会从 GLB 中自动播放Animator。如需指定其他默认 clip: 附加die来接管控制。如果还希望在运行时通过Animator.create(entity, { states: [{ clip: '<intended-default>', playing: true, loop: true }] })切换 clip: 请将所有要切换的 clip 列入playSingleAnimation(参见上一条)。states[] - 带有 /
BUTTON_DOWN的BUTTON_UP: 不存在直接等价的回调。请在 system 内使用Input.instance.subscribe检测“本帧刚刚按下”。inputSystem.isTriggered() - 触发区域(“Utils library”)以及手动实现的逐帧 AABB 检测: 不要移植自定义逐帧位置检测 system,也不要在 SDK7 中从头重新实现。SDK6 没有原生触发区域,因此社区 Utils 库和许多场景的自定义代码会每帧将
@dcl/ecs-scene-utils与盒状或球状区域进行比较,并触发Camera.instance.position/onCameraEnter。SDK7 提供原生onCameraExitcomponent(TriggerArea)——应将整个模式替换为@dcl/sdk/ecs(或TriggerArea.setBox(entity))+.setSphere/triggerAreaEventsSystem.onTriggerEnter(entity, cb)/.onTriggerExit。尺寸来自.onTriggerStay,位置来自Transform.scale,并会遵循父级链——因此,挂在旋转或偏移的场景根节点下的触发区域无需手动坐标转换即可正常工作。行为差异: Utils 库只为本地玩家触发;原生Transform.position默认使用TriggerArea,会为该层上的任何玩家触发。若要保持仅限本地玩家的行为,正确的保护条件是ColliderLayer.CL_PLAYER(不要使用if (result.trigger?.entity !== engine.PlayerEntity) return——尽管名称容易引起混淆,它实际指向触发区域自身的 Entity,因此与玩家比较始终为真,保护逻辑永远不会生效)。完整 component 参考请参见result.triggeredEntity中的“Trigger areas”部分和 [[add-interactivity]]。{baseDir}/references/api-mapping.md - 来自 (后来为
decentraland-ecs-utils)的@dcl-sdk/utils/utils.Delay以及手动实现的逐帧计时器 system: 不要移植社区utils.ExpireIncomponent,不要在 SDK7 中重新创建它,也不要编写依赖累加Delay的自定义dthelper。请使用setSceneTimeout的@dcl/sdk/ecs命名导出(timers)——它提供绑定到场景 Engine 的import { timers } from '@dcl/sdk/ecs'/setTimeout/clearTimeout/setInterval。将clearInterval替换为entity.addComponent(new utils.Delay(2000, cb)),并删除 SDK6 场景中为承载 Delay component 而创建的“计时器 Entity”。参数顺序为timers.setTimeout(cb, 2000),不是(callback, ms)——SDK6(ms, callback)的顺序与 JavaScript 标准相反,自定义 helper 沿用 SDK6 顺序是一个已知陷阱,并且会进一步放大最初以错误顺序编写 helper 所造成的问题。不要使用原生 JavaScriptDelay(ms, cb)/setTimeout全局函数——尽管 QuickJS 提供了这些函数,但它们未绑定到场景 Engine,可能引入难以察觉的问题;应始终通过setInterval调用。完整参考请参见 [[scene-runtime]] 的 Timers 部分。timers - SDK6“拦截工具”smart item(钥匙、磁铁,以及通过 + Entity 层级遍历来作用于点击目标的物品)没有直接对应的 SDK7 API。
Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, true, …)仅适用于单个 Entity——不存在全局命中结果订阅机制。迁移模式是创建一个按目标 Entity 名称索引的注册表;目标自身的pointerEventsSystem.onPointerDown会先查询该注册表,再回退到默认行为。参见pointerEventsSystem.onPointerDown。{baseDir}/references/api-mapping.md#click-interception-across-entities-keytool-acting-on-a-target - 、
Vector3.GetAngleBetweenVectors等: SDK6 中的许多 Vector3 辅助方法是静态方法。SDK7 会将其中大多数保留在Vector3.Up()中,但大小写发生了变化,例如@dcl/sdk/math→Vector3.Lerp。迁移后应 grepVector3.lerp,并将每个调用与 math 模块进行核对。Vector3. - SDK7 未导出 : 请替换为
log()。console.log() - 手持物品上的 /
Attachable.FIRST_PERSON_CAMERA(枪械、瞄准准星、手电筒、固定位置工具):SDK6 仅提供两种粗粒度跟随模式——Attachable.AVATAR(跟随相机)和Attachable.FIRST_PERSON_CAMERA(跟随 Avatar 根节点)。SDK7 提供了新的选择:支持骨骼级Attachable.AVATAR,可使用AvatarAttach(例如anchorPointId)。骨骼锚点看起来是 SDK7 中“在 Avatar 手中”的自然等价方案,但它会继承 Avatar 动画(待机起伏、行走循环、手势),导致跟随骨骼的枪械每帧抖动且无法准确瞄准。手持游戏物品的正确 SDK7 映射是通过AAPT_RIGHT_HAND设置父 Entity,而不是使用Transform.parent。对于需要精确瞄准的物品(枪械、准星、手电筒,以及玩家应能通过观察方向来指向的任何物品),SDK7 默认应使用AvatarAttach——这与 SDK6 的Transform.parent = engine.CameraEntity一致,并同时跟踪偏航角和俯仰角,因此枪口会朝向相机所指的方向。FIRST_PERSON_CAMERA是另一种选择,也是一种常见且隐蔽的失败模式:它跟随玩家根节点(脚部位置 + 身体仅偏航角,没有俯仰角和动画),因此当玩家向上抬头瞄准时,挂在Transform.parent = engine.PlayerEntity下的枪械会保持水平,无法跟随视线方向。只有对于不应跟随瞄准方向的身体固定物品,才应使用PlayerEntity(例如手持盾牌、固定手电筒、非瞄准型腰挂物品)。PlayerEntity(骨骼 ID)仅适用于希望跟随动画的装饰性物品(帽子、光环、背包、名牌)。应根据意图进行选择——AvatarAttach(需要精确瞄准,推荐用于手持武器)、engine.CameraEntity(仅偏航角、无俯仰角,用于身体固定物品)、engine.PlayerEntity(骨骼 ID,仅用于装饰性物品并跟随动画)。比较表和枪械示例请参见 [[player-avatar]] 中的“Held items vs cosmetic items”部分。AvatarAttach - 移植后 Billboard 模型可能朝向相机的反面: 在一些 SDK6 → SDK7 移植中,使用 (Y 轴 Billboard)时正面朝向相机的模型,在 SDK7 中使用
new Billboard(false, true, false)并采用相同父子关系后,会显示背面。两个 SDK 对 Billboard 指向相机的模型正面(+Z 还是 -Z)似乎存在差异。SDK7 的Billboard.create(e, { billboardMode: BillboardMode.BM_Y })interface 没有PBBillboard标志——唯一字段是oppositeDirection(已根据billboardMode的@dcl/ecs验证),因此应在模型 Transform 上修复,而不是修改 Billboard component。将显示模型在 Y 轴上旋转 180°(billboard.gen.d.ts)。如果 Entity 是父 Entity(挂载Quaternion.fromEulerDegrees(0, 180, 0))并包含一个子模型,则应在子Entity 的Billboard上应用 180° 旋转——父 Entity 的旋转由 Billboard component 管理,覆盖 billboard Entity 自身的旋转不会生效。[未验证这究竟是 SDK 行为明确变更并附带 changelog,还是模型导入器采用其他约定所导致的结果——面向用户的说明应表述为“两个 SDK 对 Billboard 朝向存在差异”,不要声称是某个特定版本引入了该变化。] 参见 [[advanced-rendering]] 的 Billboard 部分以了解 SDK7 component 参考。Transform.rotation - UI 像素尺寸无法按 1:1 迁移: SDK6 UI(/
UICanvas/UIImage)使用原始像素并针对固定屏幕尺寸设计,因此UIText或width = 200等位置和尺寸是绝对值。SDK7 React-ECS UI 默认也使用原始屏幕像素进行布局,但实际屏幕尺寸取决于用户设备——因此,原样复制的 SDK6 布局在不同显示器上会呈现不同的相对大小。修复方法是使用positionX = -350设置虚拟画布,使 Engine 将像素值视为虚拟空间内的坐标,并缩放到真实屏幕。移植 UI 前,应检查 SDK6 源代码(旧版 ECS 位于 https://github.com/decentraland/ecs),确定原始 UI 面向的像素网格,并将其作为虚拟尺寸传入:ReactEcsRenderer.setUiRenderertsReactEcsRenderer.setUiRenderer(uiRoot, { virtualWidth: 1920, virtualHeight: 1080 })是较为合理的默认值,但正确的1920x1080/virtualWidth取决于 SDK6 UI 最初设计时采用的分辨率——例如,如果原 SDK6 场景基于virtualHeight,就应使用这些数值,以确保现有像素坐标落在相同位置。完整 React-ECS 重写请参见 [[build-ui]]。1280x720
What is genuinely hard to migrate
真正难以迁移的内容
These don't have a clean 1:1 mapping and need redesign:
- Components with methods or constructors that do work (e.g. ): split the data into the new component, move the logic into a free function that takes
GemData.reset(val, x, y)and calls(entity: Entity, val: number, ...).Component.getMutable(entity) - Component groups that the user holds a reference to and iterates many times: SDK7 is recomputed each call. For hot loops over the same set, cache it inside the system or accept the cost.
engine.getEntitiesWith(...) - Object pools that rely on : SDK7 entities don't have an
entity.aliveflag. Pools must track liveness with a custom component or by checkingalive. The Migrated-2048 example simplified this to "always add a new entity" — that's acceptable for low-throughput cases, but a real pool needs explicit state.Component.has(entity) - UI built with the SDK6 /
UICanvas/UIImage/UIText: not API-compatible with SDK7. SDK7 UI is React-ECS (JSX). See [[build-ui]] — this often requires a from-scratch rewrite. When porting, also set a virtual canvas size onUIClickableso the SDK6 pixel coordinates scale predictably across screens — see the UI pixel sizing pitfall in Common Pitfalls above.ReactEcsRenderer.setUiRenderer - decorator metadata: SDK7 component IDs are strings supplied to
@Component('name'). Use the same name as the SDK6 decorator if other tools (composites, multiplayer sync) reference it by name. Otherwise pick a consistent naming scheme.engine.defineComponent('name', ...)
以下内容没有清晰的 1:1 映射,需要重新设计:
- 包含实际逻辑的方法或构造函数 component(例如 ):将数据拆到新 component 中,并把逻辑移入一个接受
GemData.reset(val, x, y)参数并调用(entity: Entity, val: number, ...)的独立函数。Component.getMutable(entity) - 用户持有引用并多次遍历的 component group: SDK7 的 会在每次调用时重新计算。对于遍历同一集合的热循环,应在 system 内缓存结果,或接受这项开销。
engine.getEntitiesWith(...) - 依赖 的对象池: SDK7 Entity 没有
entity.alive标志。对象池必须使用自定义 component 跟踪存活状态,或通过检查alive判断。迁移后的 2048 示例将其简化为“始终添加新 Entity”——这适用于低吞吐量场景,但真正的对象池需要显式状态管理。Component.has(entity) - 使用 SDK6 /
UICanvas/UIImage/UIText构建的 UI: 与 SDK7 API 不兼容。SDK7 UI 使用 React-ECS(JSX)。参见 [[build-ui]]——这通常需要从头重写。移植时,还应在UIClickable上设置虚拟画布尺寸,使 SDK6 像素坐标能够在不同屏幕上按预期缩放——参见上文“常见陷阱”中的 UI 像素尺寸问题。ReactEcsRenderer.setUiRenderer - decorator metadata: SDK7 component ID 是传给
@Component('name')的字符串。如果其他工具(composite、多人同步等)通过名称引用该 component,请使用与 SDK6 decorator 相同的名称;否则应选择一致的命名方案。engine.defineComponent('name', ...)
Cross-References
交叉引用
- [[create-scene]] — scene.json schema, composite-first rule, scaffolding a fresh SDK7 project
- [[composites]] — file format for the optional second pass that moves static entities out of TypeScript
.composite - [[add-3d-models]] — , collider masks for clickability
GltfContainer - [[add-interactivity]] — (replaces SDK6
pointerEventsSystem)OnPointerDown - [[advanced-input]] — polling (replaces SDK6
inputSystem)Input.instance.subscribe - [[scene-runtime]] — /
timers.setTimeoutfromtimers.setInterval(replaces SDK6 community@dcl/sdk/ecsdecentraland-ecs-utils/Delay),Interval,executeTask, realm/scene infofetch - [[animations-tweens]] — and
Animator(replaces SDK6Tween/Animator)AnimationState - [[audio-video]] — and
AudioSourceVideoPlayer - [[build-ui]] — React-ECS UI (replaces SDK6 UI system)
UICanvas - [[advanced-rendering]] — PBR materials, TextShape, Billboard
- [[multiplayer-sync]] — for multiplayer (replaces SDK6
syncEntity/ sync libraries)MessageBus
- [[create-scene]]——schema、Composite 优先规则、新建 SDK7 项目脚手架
scene.json - [[composites]]——文件格式,用于将静态 Entity 移出 TypeScript 的可选第二阶段
.composite - [[add-3d-models]]——、用于点击检测的 collider mask
GltfContainer - [[add-interactivity]]——(替代 SDK6
pointerEventsSystem)OnPointerDown - [[advanced-input]]——轮询(替代 SDK6
inputSystem)Input.instance.subscribe - [[scene-runtime]]——/
timers.setTimeout(替代 SDK6 社区timers.setInterval的decentraland-ecs-utils/Delay)、Interval、executeTask、realm/scene 信息fetch - [[animations-tweens]]——和
Animator(替代 SDK6Tween/Animator)AnimationState - [[audio-video]]——和
AudioSourceVideoPlayer - [[build-ui]]——React-ECS UI(替代 SDK6 UI 系统)
UICanvas - [[advanced-rendering]]——PBR 材质、、Billboard
TextShape - [[multiplayer-sync]]——用于多人同步的 (替代 SDK6
syncEntity/ 同步库)MessageBus
References
参考资料
- — full table of SDK6 → SDK7 symbol conversions
{baseDir}/references/api-mapping.md - — annotated before/after using the 2048 game scene (entity setup, components, systems, pointer events, animations)
{baseDir}/references/migration-example.md
- ——完整的 SDK6 → SDK7 符号转换表
{baseDir}/references/api-mapping.md - ——使用 2048 游戏场景制作的带注释迁移前后对照(Entity 设置、component、system、指针事件、动画)
{baseDir}/references/migration-example.md