migrate-sdk6-to-sdk7

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrate 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.
CheckSDK6 signalSDK7 signal
package.json
dependencies
decentraland-ecs
@dcl/sdk
package.json
scripts
build-ecs
,
dcl start
sdk-commands start
,
sdk-commands build
scene.json
No
runtimeVersion
, or
ecs7
missing
"runtimeVersion": "7"
and/or
"ecs7": true
Source code importsNo explicit
@dcl/sdk/ecs
import — symbols (
Entity
,
Transform
,
Vector3
,
engine
) are globals
Most things imported from
@dcl/sdk/ecs
and
@dcl/sdk/math
Source code patterns
new Entity()
,
entity.addComponent(...)
,
@Component('name')
,
class X implements ISystem
,
OnPointerDown
,
GLTFShape
,
Input.instance
engine.addEntity()
,
Transform.create(entity, ...)
,
engine.defineComponent(...)
,
pointerEventsSystem.onPointerDown
,
GltfContainer
按以下顺序检查这些文件。只有所有 SDK6 信号都存在时,才能将项目视为 SDK6——部分匹配通常意味着这是一个只迁移了一半的项目,需要采用不同的处理方式。
检查项SDK6 信号SDK7 信号
package.json
依赖项
decentraland-ecs
@dcl/sdk
package.json
脚本
build-ecs
、
dcl start
sdk-commands start
、
sdk-commands build
scene.json
没有
runtimeVersion
,或缺少
ecs7
"runtimeVersion": "7"
和/或
"ecs7": true
源代码 imports没有显式的
@dcl/sdk/ecs
import——
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

RULE: Do NOT change
scene.json
's
main
field

规则:不要修改
scene.json
的
main
字段

Keep the
main
value the SDK6 project already had (typically
"bin/game.js"
). The SDK7 default for new scenes is
bin/index.js
, but
sdk-commands build
writes the bundle to whatever path
scene.json
main
names (verified:
sdk-commands
bundle.ts
sets the output file from
sceneJson.main
) — so an existing
bin/game.js
keeps working with no changes. Do NOT rename it to
bin/index.js
unless the user explicitly asks.
You SHOULD add
"runtimeVersion": "7"
to
scene.json
(and remove anything ECS6-specific). See [[create-scene]] for the full scene.json schema.
保留 SDK6 项目原有的
main
值(通常是
"bin/game.js"
)。新场景在 SDK7 中的默认值为
bin/index.js
,但
sdk-commands build
会将 bundle 写入
scene.json
中
main
指定的路径(已验证:
sdk-commands
的
bundle.ts
根据
sceneJson.main
设置输出文件)——因此现有的
bin/game.js
无需修改即可继续工作。除非用户明确要求,否则不要将其重命名为
bin/index.js
。
你应该向
scene.json
中添加
"runtimeVersion": "7"
(并移除所有 ECS6 专属内容)。完整的
scene.json
schema 请参见 [[create-scene]]。

RULE: The composite-first rule applies to migrations too

规则:Composite 优先规则同样适用于迁移

After migration, all static scenery (models, lights, primitives, text placed at scene load) belongs in
assets/scene/main.composite
, 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
engine.addEntity()
in TypeScript so the scene runs first.
Order of operations:
  1. Get the SDK7 TypeScript port running (entities still created in code).
  2. THEN, optionally, move static entities into
    assets/scene/main.composite
    . See [[create-scene]] for the composite-first rule and [[composites]] for the format.
迁移完成后,所有静态场景内容(模型、灯光、图元以及在场景加载时放置的文本)都应放在
assets/scene/main.composite
中,而不是放在 TypeScript 中。SDK6 场景没有 composite——每个 Entity 都是通过代码创建的。将这些内容迁移到 composite 属于单独的、可选步骤;最低可行的迁移会将它们保留为 TypeScript 中的
engine.addEntity()
,以确保场景首先能够正常运行。
操作顺序:
  1. 首先让 SDK7 TypeScript 移植版本能够运行起来(Entity 仍由代码创建)。
  2. 然后,再选择性地将静态 Entity 移入
    assets/scene/main.composite
    。有关 Composite 优先规则,请参见 [[create-scene]];有关格式,请参见 [[composites]]。

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.
ConceptSDK6SDK7
Entity creation
const e = new Entity()
then
engine.addEntity(e)
const e = engine.addEntity()
(returns the ID)
Entity type
IEntity
/
Entity
class with methods (
addComponent
,
getComponent
,
setParent
,
alive
)
Entity
= opaque integer ID, NO methods
Adding a component
entity.addComponent(new Transform({...}))
— entry point is the entity
Transform.create(entity, {...})
— entry point is the component type
Defining a component
@Component('name') class GemData { val: number; constructor(...) {...} }
export const GemData = engine.defineComponent('gemData', { val: Schemas.Number }, { val: 2 })
— schema + defaults, NO methods
Component methods (
reset()
, etc.)
Allowed (class methods)Forbidden — components are data only. Move methods into free functions or systems
Reading component data
entity.getComponent(GemData).val
(always mutable)
GemData.get(entity).val
(immutable, fast) OR
GemData.getMutable(entity).val = X
(only when writing)
Modifying component dataMutate the returned referenceMUST use
getMutable()
—
.get()
returns a read-only reference. Using
.get()
and mutating it silently fails
Parenting
child.setParent(parent)
Set
parent
field of child's Transform:
Transform.create(child, { parent: parentEntity })
or
Transform.getMutable(child).parent = parentEntity
Defining a system
class MoveGems implements ISystem { update(dt) {...} }
then
engine.addSystem(new MoveGems())
export function moveGemsSystem(dt: number) { ... }
then
engine.addSystem(moveGemsSystem)
Querying entities
const group = engine.getComponentGroup(Transform, GemData)
then
group.entities
for (const [entity, gemData, transform] of engine.getEntitiesWith(GemData, Transform)) { ... }
Removing an entity
engine.removeEntity(entity)
engine.removeEntity(entity)
(same API)
Event callbacks on components
entity.addComponent(new OnPointerDown((e) => {...}))
pointerEventsSystem.onPointerDown({entity, opts}, () => {...})
— separated from the component
Static "scene-level" objects
Camera.instance
,
Input.instance
(singletons)
engine.PlayerEntity
,
engine.CameraEntity
,
engine.RootEntity
+ component reads (
Transform.get(engine.PlayerEntity)
)
Mutability is a performance contract, not just style. Always prefer
.get()
(read) over
.getMutable()
(write). Using
.getMutable()
when you only read causes a CRDT delta to be sent every frame.
SDK6 将 Entity 建模为附加了 component 的对象。SDK7 是真正的 ECS,其中 Entity 只是不透明的 ID,而 component 是纯粹的数据。
概念SDK6SDK7
创建 Entity
const e = new Entity()
,然后
engine.addEntity(e)
const e = engine.addEntity()
(返回 ID)
Entity 类型带方法的
IEntity
/
Entity
类(
addComponent
、
getComponent
、
setParent
、
alive
)
Entity
= 不透明的整数 ID,没有方法
添加 component
entity.addComponent(new Transform({...}))
——入口点是 Entity
Transform.create(entity, {...})
——入口点是 component 类型
定义 component
@Component('name') class GemData { val: number; constructor(...) {...} }
export const GemData = engine.defineComponent('gemData', { val: Schemas.Number }, { val: 2 })
——schema + 默认值,没有方法
component 方法(
reset()
等)
允许使用类方法禁止使用——component 仅包含数据。将方法移至独立函数或 system 中
读取 component 数据
entity.getComponent(GemData).val
(始终可变)
GemData.get(entity).val
(不可变、速度快),或在写入时使用
GemData.getMutable(entity).val = X
修改 component 数据修改返回的引用必须使用
getMutable()
——
.get()
返回只读引用。使用
.get()
修改数据会静默失败
设置父级
child.setParent(parent)
设置子 Entity 的 Transform 的
parent
字段:
Transform.create(child, { parent: parentEntity })
或
Transform.getMutable(child).parent = parentEntity
定义 system
class MoveGems implements ISystem { update(dt) {...} }
,然后
engine.addSystem(new MoveGems())
export function moveGemsSystem(dt: number) { ... }
,然后
engine.addSystem(moveGemsSystem)
查询 Entity
const group = engine.getComponentGroup(Transform, GemData)
,然后
group.entities
for (const [entity, gemData, transform] of engine.getEntitiesWith(GemData, Transform)) { ... }
移除 Entity
engine.removeEntity(entity)
engine.removeEntity(entity)
(API 相同)
component 上的事件回调
entity.addComponent(new OnPointerDown((e) => {...}))
pointerEventsSystem.onPointerDown({entity, opts}, () => {...})
——回调与 component 分离
静态“场景级”对象
Camera.instance
、
Input.instance
(单例)
engine.PlayerEntity
、
engine.CameraEntity
、
engine.RootEntity
+ component 读取(
Transform.get(engine.PlayerEntity)
)
可变性不仅是一种代码风格,也是性能约定。 读取时始终优先使用
.get()
,仅在写入时使用
.getMutable()
。如果只进行读取却使用
.getMutable()
,会导致每帧都发送一次 CRDT delta。

API Mapping — quick reference

API 映射——快速参考

See
{baseDir}/references/api-mapping.md
for the full table. The most common conversions:
SDK6SDK7
import { ... } from 'decentraland-ecs'
(often implicit)
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)
(read/write)
Transform.get(entity)
(read) /
Transform.getMutable(entity)
(write)
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
(or pass
parent
in
create
)
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)
(add
MeshCollider.setBox(entity)
etc. for collision)
new Material()
+
m.albedoColor = Color3.Blue()
+
entity.addComponent(m)
Material.setPbrMaterial(entity, { albedoColor: Color4.Blue() })
(note: Color4 in SDK7)
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 }] })
; play with
Animator.playSingleAnimation(entity, 'Clip')
; stop with
Animator.stopAllAnimations(entity)
new OnPointerDown(handler, { button, hoverText })
on the entity
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)
inside a system, or
pointerEventsSystem.onPointerDown
for entity-specific
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()
(most SDK7 component fields use Color4)
Vector3.Lerp(a, b, t)
Vector3.lerp(a, b, t)
(lowercase l)
Scalar.Lerp(a, b, t)
Scalar.lerp(a, b, t)
vector.setAll(0.5)
Vector3.create(0.5, 0.5, 0.5)
(no mutating helper — Vector3 is treated as plain data)
class X implements ISystem { update(dt) {...} }
+
engine.addSystem(new X())
function xSystem(dt: number) { ... }
+
engine.addSystem(xSystem)
engine.getComponentGroup(A, B)
returning
.entities
engine.getEntitiesWith(A, B)
returning iterable of
[entity, a, b]
tuples
log(...)
console.log(...)
entity.addComponent(new utils.Delay(ms, cb))
(from
decentraland-ecs-utils
/
@dcl-sdk/utils
)
timers.setTimeout(cb, ms)
(
import { timers } from '@dcl/sdk/ecs'
, argument order is
(callback, ms)
)
entity.addComponent(new utils.Interval(ms, cb))
timers.setInterval(cb, ms)
(
import { timers } from '@dcl/sdk/ecs'
)
Custom per-frame
timerSystem
accumulating
dt
Delete it — use
timers.setTimeout
/
timers.setInterval
instead
完整对照表请参见
{baseDir}/references/api-mapping.md
。最常见的转换如下:
SDK6SDK7
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)
在 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)
在 system 内使用
inputSystem.isTriggered(InputAction.IA_POINTER, PointerEventType.PET_DOWN)
,或针对特定 Entity 使用
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 component 字段使用 Color4)
Vector3.Lerp(a, b, t)
Vector3.lerp(a, b, t)
(首字母小写)
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]
元组组成的 iterable
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'
)
累加
dt
的自定义逐帧
timerSystem
删除它——改用
timers.setTimeout
/
timers.setInterval

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
    @Component
    class — these become
    engine.defineComponent(...)
    with a flat schema. Class methods (
    reset()
    , etc.) must be lifted out into free functions or systems.
  • Every
    class X implements ISystem
    — these become free functions
    (dt: number) => void
    .
  • Every
    OnPointerDown
    /
    OnPointerUp
    /
    Input.instance.subscribe
    — these become
    pointerEventsSystem
    calls or
    inputSystem.isTriggered()
    in a system.
  • Every
    GLTFShape
    ,
    BoxShape
    , etc. — see mapping table.
  • Every
    Camera.instance
    ,
    Input.instance
    — these become static engine entities + component reads.
  • All
    setParent
    calls — these become
    Transform.parent
    field assignments.
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.
读取每一个源文件,并建立以下清单:
  • 每个自定义
    @Component
    类——这些需要转换为带有扁平 schema 的
    engine.defineComponent(...)
    。类方法(
    reset()
    等)必须提取为独立函数或 system。
  • 每个
    class X implements ISystem
    ——这些需要转换为独立函数
    (dt: number) => void
    。
  • 每个
    OnPointerDown
    /
    OnPointerUp
    /
    Input.instance.subscribe
    ——这些需要转换为
    pointerEventsSystem
    调用,或在 system 中使用
    inputSystem.isTriggered()
    。
  • 每个
    GLTFShape
    、
    BoxShape
    等——参见映射表。
  • 每个
    Camera.instance
    、
    Input.instance
    ——这些需要转换为静态 Engine Entity + component 读取。
  • 所有
    setParent
    调用——这些需要转换为
    Transform.parent
    字段赋值。
此清单是步骤 5–10 的完成检查表——只有在每个类别中的所有项目都已移植,且其 SDK6 符号不再出现在源代码中时,才能认为相应步骤已完成。

2. Replace
package.json
,
tsconfig.json
, and update
scene.json

2. 替换
package.json
、
tsconfig.json
并更新
scene.json

  • Replace
    decentraland-ecs
    with
    @dcl/sdk
    (latest).
  • Replace
    build-ecs
    /
    dcl start
    scripts with
    sdk-commands start
    /
    sdk-commands build
    .
  • Replace the old
    tsconfig.json
    with the standard SDK7 one (
    target: ES2020
    ,
    module: ESNext
    ,
    jsx: react-jsx
    ,
    strict: true
    ).
  • Add
    "runtimeVersion": "7"
    to
    scene.json
    . Do NOT modify
    main
    unless the user asks.
  • 将
    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/

3. 将资源文件夹重新组织到
assets/
下

SDK6 scenes commonly kept assets in top-level folders at the project root:
models/
,
images/
,
audio/
(or
sounds/
),
textures/
,
videos/
. SDK7 + Creator Hub expects assets under a top-level
assets/
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.
  1. Identify every top-level asset folder in the SDK6 project. Common SDK6 names:
    models/
    ,
    images/
    ,
    audio/
    ,
    sounds/
    ,
    textures/
    ,
    videos/
    .
  2. Create the new structure under
    assets/
    using the capitalized category convention used elsewhere in these skills (see [[create-scene]], [[add-3d-models]], [[audio-video]]):
    • models/
      →
      assets/Models/
    • images/
      →
      assets/Images/
    • audio/
      or
      sounds/
      →
      assets/Audio/
    • videos/
      →
      assets/Videos/
    • textures/
      →
      assets/Images/
      (textures are images; consolidate unless the user wants a separate folder)
  3. 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).
  4. 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
      .composite
      files.
  5. 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
assets/scene/Models/
(the legacy Creator Hub layout) or
assets/asset-packs/
/
assets/custom/
(Creator Hub adds these for asset-pack items and custom items), reuse those paths instead of creating a parallel
assets/Models/
. The rule is: one canonical location per asset type — don't fragment.
SDK6 场景通常将资源保存在项目根目录下的顶级文件夹中:
models/
、
images/
、
audio/
(或
sounds/
)、
textures/
、
videos/
。SDK7 + Creator Hub 期望资源位于顶级
assets/
文件夹下,以便可视化编辑器能够建立索引。应在移植任何代码之前将移动操作作为一个独立步骤完成,这样后续步骤中的路径重写可以直接使用最终位置。
  1. 识别 SDK6 项目中的每个顶级资源文件夹。 常见名称包括
    models/
    、
    images/
    、
    audio/
    、
    sounds/
    、
    textures/
    、
    videos/
    。
  2. 使用这些 skill 中采用的大写分类约定,在
    assets/
    下创建新结构
    (参见 [[create-scene]]、[[add-3d-models]]、[[audio-video]]):
    • models/
      →
      assets/Models/
    • images/
      →
      assets/Images/
    • audio/
      或
      sounds/
      →
      assets/Audio/
    • videos/
      →
      assets/Videos/
    • textures/
      →
      assets/Images/
      (纹理属于图片;除非用户希望单独保留文件夹,否则应合并)
  3. 将文件移入新结构。 不要保留重复副本——移动完成后应删除旧的顶级文件夹。重复副本会导致 Creator Hub 对过时路径建立索引,并使部署时的场景大小大约增加一倍(两份副本都会被打包)。
  4. 重写所有指向已移动路径的代码引用。 在整个源代码树中 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
      文件中提及旧文件夹名称的任何字符串字面量。
  5. 完成标准: 在项目中 grep 旧文件夹名称应返回零个残留引用,并且在 Creator Hub 中打开场景时,资源树应正确显示来自
    assets/
    的内容。
例外——如果现有布局已经存在,则继续使用。 如果项目已经包含
assets/scene/Models/
(旧版 Creator Hub 布局),或
assets/asset-packs/
/
assets/custom/
(Creator Hub 为 asset-pack 项目和 custom 项目添加的目录),则应继续使用这些路径,而不是创建一套并列的
assets/Models/
。原则是:每种资源类型只能有一个规范位置——不要造成目录碎片化。

4. Create
src/index.ts
with an exported
main()

4. 创建包含导出式
main()
的
src/index.ts

SDK7 scenes start from a top-level exported
main()
function. The convention is to put most code in another file (e.g.
src/game.ts
) and have
index.ts
just call into it:
typescript
import { initializeGame } from "./game";
export function main() {
  initializeGame();
}
SDK7 场景从一个顶层导出的
main()
函数开始。通常应将大部分代码放在另一个文件中(例如
src/game.ts
),并让
index.ts
仅负责调用它:
typescript
import { initializeGame } from "./game";
export function main() {
  initializeGame();
}

5. Port components first

5. 首先移植 component

Convert each
@Component
class to
engine.defineComponent(name, schema, defaults)
. Flatten nested types —
Schemas
does not support arbitrary classes, but does support
Schemas.Vector3
,
Schemas.Quaternion
, primitives, arrays, and nested
Schemas.Map(...)
. A
Vector2
field in SDK6 typically becomes two scalar fields (
posX
,
posY
) in SDK7 — see the 2048 example in
{baseDir}/references/migration-example.md
.
Done when:
grep -rn "@Component(" src/
returns zero hits and every component on the step-1 audit list has a corresponding
engine.defineComponent
call.
将每个
@Component
类转换为
engine.defineComponent(name, schema, defaults)
。扁平化嵌套类型——
Schemas
不支持任意类,但支持
Schemas.Vector3
、
Schemas.Quaternion
、基本类型、数组以及嵌套的
Schemas.Map(...)
。SDK6 中的
Vector2
字段通常需要转换为两个标量字段(
posX
、
posY
)——参见
{baseDir}/references/migration-example.md
中的 2048 示例。
完成标准:
grep -rn "@Component\\(" src/
返回零个匹配,并且步骤 1 审计清单中的每个 component 都有对应的
engine.defineComponent
调用。

6. Port systems

6. 移植 system

Each
class X implements ISystem { update(dt) {...} }
becomes a free function. Replace
engine.getComponentGroup(A, B).entities
iteration with
for (const [entity, a, b] of engine.getEntitiesWith(A, B))
. Inside the loop, use
.getMutable(entity)
only when writing.
Done when:
grep -rnE "implements ISystem|getComponentGroup" src/
returns zero hits and every system on the step-1 audit list is registered via
engine.addSystem
.
每个
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 审计清单中的每个 system 都通过
engine.addSystem
注册。

7. Port the entity/component setup (the bulk of
game.ts
)

7. 移植 Entity/component 设置(
game.ts
中的主体部分)

For each
new Entity()
block, replace with
engine.addEntity()
and a series of
Component.create(entity, ...)
calls. Convert
setParent
to a
parent
field on the Transform. Use the new
assets/Models/...
,
assets/Audio/...
,
assets/Images/...
paths established in step 3 for every
src
/
audioClipUrl
/
texture.src
.
Done when:
grep -rnE "new Entity\(|\.addComponent\(|\.setParent\(" src/
returns zero hits, and no
src
/
audioClipUrl
/ texture path still references a pre-step-3 folder.
对于每个
new Entity()
代码块,替换为
engine.addEntity()
,并使用一系列
Component.create(entity, ...)
调用。将
setParent
转换为 Transform 的
parent
字段。所有
src
/
audioClipUrl
/ 纹理路径都应使用步骤 3 中建立的
assets/Models/...
、
assets/Audio/...
、
assets/Images/...
路径。
完成标准:
grep -rnE "new Entity\\(|\\.addComponent\\(|\\.setParent\\(" src/
返回零个匹配,并且没有任何
src
/
audioClipUrl
/ 纹理路径仍引用步骤 3 之前的文件夹。

8. Port input/pointer handlers

8. 移植输入和指针事件处理器

  • Per-entity click handlers (
    new OnPointerDown(...)
    ) →
    pointerEventsSystem.onPointerDown({ entity, opts }, handler)
    . Add a collider if the entity doesn't already have one (
    MeshCollider.setBox(entity)
    or
    visibleMeshesCollisionMask: 1
    on
    GltfContainer
    ). See [[add-interactivity]].
  • Global key subscriptions (
    Input.instance.subscribe
    ) → use
    inputSystem.isTriggered(InputAction.IA_X, PointerEventType.PET_DOWN)
    inside a system. See [[advanced-input]].
Done when:
grep -rnE "OnPointerDown|OnPointerUp|Input\.instance|ActionButton\." src/
returns zero hits and every handler on the step-1 audit list has a
pointerEventsSystem
or
inputSystem
counterpart.
  • 每 Entity 的点击处理器(
    new OnPointerDown(...)
    )→
    pointerEventsSystem.onPointerDown({ entity, opts }, handler)
    。如果 Entity 尚无 collider,则添加一个(
    MeshCollider.setBox(entity)
    ,或在
    GltfContainer
    上设置
    visibleMeshesCollisionMask: 1
    )。参见 [[add-interactivity]]。
  • 全局按键订阅(
    Input.instance.subscribe
    )→ 在 system 内使用
    inputSystem.isTriggered(InputAction.IA_X, PointerEventType.PET_DOWN)
    。参见 [[advanced-input]]。
完成标准:
grep -rnE "OnPointerDown|OnPointerUp|Input\\.instance|ActionButton\\." src/
返回零个匹配,并且步骤 1 审计清单中的每个处理器都有对应的
pointerEventsSystem
或
inputSystem
实现。

9. Port animations

9. 移植动画

new Animator()
+
new AnimationState('clip')
+
animator.addClip(...)
becomes a single
Animator.create(entity, { states: [{ clip, playing, loop }] })
. Play with
Animator.playSingleAnimation(entity, 'clip')
. Stop all with
Animator.stopAllAnimations(entity)
. See [[animations-tweens]].
Done when:
grep -rnE "new Animator|AnimationState|addClip|getClip" src/
returns zero hits, and every clip name that appears in an SDK6
playAnimation
/
getClip
call is listed in some
states[]
array — clips missing from
states[]
fail silently (see the
playSingleAnimation
pitfall in Common Pitfalls).
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
调用中出现的每个 clip 名称都列在某个
states[]
数组中——遗漏在
states[]
中的 clip 会静默失败(参见“常见陷阱”中的
playSingleAnimation
陷阱)。

10. Port sounds

10. 移植声音

new AudioClip(url)
+
entity.addComponent(new AudioSource(clip))
becomes
AudioSource.create(entity, { audioClipUrl: url, playing: false, ... })
. Play by toggling
AudioSource.getMutable(entity).playing = true
. See [[audio-video]].
Done when:
grep -rnE "new AudioClip|new AudioSource\(" src/
returns zero hits.
new AudioClip(url)
+
entity.addComponent(new AudioSource(clip))
转换为
AudioSource.create(entity, { audioClipUrl: url, playing: false, ... })
。通过将
AudioSource.getMutable(entity).playing
设为
true
来播放。参见 [[audio-video]]。
完成标准:
grep -rnE "new AudioClip|new AudioSource\\(" src/
返回零个匹配。

11. Verify and test

11. 验证和测试

Run
npm install
to fetch the new SDK, then
sdk-commands start
(or
npm start
). The migration is complete only when ALL of these hold:
  1. npm run build
    exits 0.
  2. A final sweep
    grep -rnE "decentraland-ecs|@Component\(|implements ISystem|Input\.instance|Camera\.instance" src/
    returns zero hits — per-step greps get missed when work is interrupted and resumed; this is the end-to-end check.
  3. The preview loads with no errors in the console.
  4. 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.
运行
npm install
获取新 SDK,然后运行
sdk-commands start
(或
npm start
)。只有满足以下所有条件,迁移才算完成:
  1. npm run build
    以状态码 0 退出。
  2. 最终全面检查
    grep -rnE "decentraland-ecs|@Component\\(|implements ISystem|Input\\.instance|Camera\\.instance" src/
    返回零个匹配——工作中断后恢复时,分步骤 grep 很容易被遗漏,因此必须进行这次端到端检查。
  3. 预览加载时控制台中没有错误。
  4. 可见布局(位置/旋转)与 SDK6 场景一致。
如果任何检查失败,应返回到负责处理相应符号类别的步骤——不要报告迁移已经完成。

Common Pitfalls

常见陷阱

  • Mutating a
    .get()
    result
    :
    Transform.get(entity).position.x = 5
    is silently a no-op. Use
    Transform.getMutable(entity).position.x = 5
    .
  • Passing explicit
    rotation: undefined
    /
    scale: undefined
    to
    Transform.create
    crashes the serializer
    : symptom is
    "Cannot read properties of undefined (reading 'x')"
    at serialization. Why (verified —
    @dcl/ecs/dist/components/manual/Transform.js
    ):
    Transform.create
    runs the value through
    TransformSchema.extend
    , which spreads
    ...value
    AFTER its defaults, so an explicit
    undefined
    field overrides the default identity rotation / unit scale and is stored as
    undefined
    ; the binary
    serialize
    then reads
    value.rotation.x
    and throws. This bites when a port builds a Transform from a partial object that may carry
    undefined
    fields (e.g.
    Transform.create(e, { position, rotation: maybeRot, scale: maybeScale })
    ). Fix — omit the field entirely instead of passing
    undefined
    so the schema applies its default:
    typescript
    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)
  • Forgetting the schema:
    engine.defineComponent
    REQUIRES a schema. SDK6 class fields with no type cannot be auto-inferred.
  • Component methods: SDK6 components frequently had a
    reset()
    or constructor logic. These MUST be moved to free functions in SDK7 — components are pure data.
  • Vector2 fields:
    Schemas
    does NOT have
    Schemas.Vector2
    . [UNVERIFIED — confirm by reading the Schemas object exports.] Flatten to two
    Schemas.Number
    fields, or use
    Schemas.Map({ x: Schemas.Number, y: Schemas.Number })
    .
  • setParent
    on the entity object
    : Entities are integers in SDK7 — there is no method. Set the
    parent
    field on the child's
    Transform
    .
  • Color3 vs Color4: Most SDK7 component fields take
    Color4
    (with alpha). Replace
    Color3.X()
    calls with
    Color4.X()
    unless the field is documented as Color3.
  • Materials are not entities or objects: There is no
    new Material()
    instance you keep a reference to.
    Material.setPbrMaterial(entity, props)
    is the only way to set it — a material lives on one entity, not as a shared object.
  • Shape entities (BoxShape, PlaneShape, etc.) need MeshRenderer AND MeshCollider: In SDK6 a single
    BoxShape
    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.
  • GLTFShape had its visible mesh clickable by default with
    withCollisions: true
    : In SDK7,
    GltfContainer
    by 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 explicit
    visibleMeshesCollisionMask
    set to a value including
    CL_POINTER
    (1) for clicks, or
    CL_PHYSICS
    (2) for player collision, or
    3
    for both. See [[add-3d-models]].
  • OnPointerDown
    callbacks no longer fire inside a
    forEach
    over a component group
    : In SDK6 they were event-driven; in SDK7 you register the handler once with
    pointerEventsSystem.onPointerDown(...)
    — don't put that call inside a system's update loop or you'll register a new handler every frame.
  • Animator.playSingleAnimation
    silently no-ops on clips not in
    Animator.states
    (porters who built the Animator in code)
    : this applies specifically to the SDK6-port path where you author the
    Animator
    yourself in TypeScript and call
    Animator.playSingleAnimation(entity, clipName)
    programmatically. In that case,
    playSingleAnimation
    returns
    false
    and does nothing if
    clipName
    isn't already present in the entity's
    states
    array (verified —
    @dcl/ecs/dist-cjs/components/extended/Animator.js
    lines 35-46). No warning. SDK6 hid this —
    Animator.getClip(name)
    auto-created the clip on first use, so SDK6 scenes routinely call
    playAnimation('Appear')
    without declaring
    'Appear'
    upfront. A naive port that maps
    entity.getComponent(Animator).getClip(name).play()
    →
    Animator.playSingleAnimation(entity, name)
    ends up calling it against clip names the porter never added to
    states[]
    , and those calls silently fail. Fix: walk every
    playAnimation
    /
    getClip
    call in the SDK6 source, collect the full set of clip names per entity, and list them all in the
    states
    array at
    Animator.create
    time. For a quick port-friendly shim that restores SDK6's "play any clip, no setup" ergonomics with a lazy
    states.push
    , 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-populates
    states[]
    with every GLB clip) or by asset packs / smart items (which ship with
    states
    populated), nor to models with no
    Animator
    at all — those rely on the autoplay path in the next bullet.
  • Unnecessary serialization overhead when porting
    anim.stop(); anim.play()
    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 mutates
    Animator.states
    (
    s.playing
    ,
    s.shouldReset
    ) 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 in
    @dcl/ecs
    detects identical writes and silently drops them), but calling
    getMutableOrNull()
    every 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 a
    noLoop + revertToIdle
    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-shot
    onPointerDown
    , edge-triggered state machine), nor when you use
    Animator.playSingleAnimation
    from 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.
  • GltfContainer
    autoplays one clip from a multi-animation .glb when no
    Animator
    is 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 no
    Animator
    , the renderer autoplays; when the entity has an
    Animator
    and you call
    playSingleAnimation
    , the clip must be in
    states[]
    . Which clip the renderer autoplays is not stable across models — observed cases include a Halloween scene where small ghosts with no
    Animator
    autoplayed
    die
    straight from the GLB. Fix when you want a different default: attach an
    Animator.create(entity, { states: [{ clip: '<intended-default>', playing: true, loop: true }] })
    to take control. Fix when you also want to switch clips at runtime via
    playSingleAnimation
    : list every clip you'll switch to in
    states[]
    (see previous bullet).
  • Input.instance.subscribe
    with
    BUTTON_DOWN
    /
    BUTTON_UP
    : there is no direct callback equivalent. Use
    inputSystem.isTriggered()
    inside a system for "just pressed this frame".
  • @dcl/ecs-scene-utils
    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
    Camera.instance.position
    against a box/sphere region every frame and fired
    onCameraEnter
    /
    onCameraExit
    . SDK7 has a native
    TriggerArea
    component (
    @dcl/sdk/ecs
    ) — replace the entire pattern with
    TriggerArea.setBox(entity)
    (or
    .setSphere
    ) +
    triggerAreaEventsSystem.onTriggerEnter(entity, cb)
    /
    .onTriggerExit
    /
    .onTriggerStay
    . Size comes from
    Transform.scale
    , position from
    Transform.position
    , 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; native
    TriggerArea
    defaults to
    ColliderLayer.CL_PLAYER
    , which fires for ANY player on that layer. The correct guard for local-player-only behavior is
    if (result.trigger?.entity !== engine.PlayerEntity) return
    (NOT
    result.triggeredEntity
    — despite the confusing name,
    triggeredEntity
    is 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 of
    {baseDir}/references/api-mapping.md
    and [[add-interactivity]] for the full component reference.
  • utils.Delay
    /
    utils.ExpireIn
    from
    decentraland-ecs-utils
    (later
    @dcl-sdk/utils
    ) AND hand-rolled per-frame timer systems
    : do NOT port the community
    Delay
    component, do NOT recreate it in SDK7, and do NOT write a custom
    setSceneTimeout
    helper backed by a
    timerSystem
    that accumulates
    dt
    . Use the
    timers
    named export from
    @dcl/sdk/ecs
    (
    import { timers } from '@dcl/sdk/ecs'
    ) — it provides
    setTimeout
    /
    clearTimeout
    /
    setInterval
    /
    clearInterval
    bound to the scene engine. Replace
    entity.addComponent(new utils.Delay(2000, cb))
    with
    timers.setTimeout(cb, 2000)
    and delete the "timer entity" the SDK6 scene created just to host the Delay component. Argument order is
    (callback, ms)
    , NOT
    (ms, callback)
    — the SDK6
    Delay(ms, cb)
    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 JS
    setTimeout
    /
    setInterval
    globals
    — although QuickJS exposes them, they are not bound to the scene engine and can introduce subtle problems; always go through
    timers
    . See [[scene-runtime]] (Timers section) for the full reference.
  • SDK6 "intercepting tool" smart-items (keys, magnets, inventory items that act on a clicked target via
    Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, true, …)
    + entity-hierarchy walking) have no direct SDK7 equivalent.
    pointerEventsSystem.onPointerDown
    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 own
    pointerEventsSystem.onPointerDown
    consults the registry before falling back to its default behavior. See
    {baseDir}/references/api-mapping.md#click-interception-across-entities-keytool-acting-on-a-target
    .
  • Vector3.GetAngleBetweenVectors
    ,
    Vector3.Up()
    etc.
    : many SDK6 Vector3 helpers existed as static methods. SDK7 keeps most under
    @dcl/sdk/math
    but capitalization changed (e.g.
    Vector3.Lerp
    →
    Vector3.lerp
    ). Grep for
    Vector3.
    after migration and verify each call against the math module.
  • log()
    is not exported in SDK7
    : replace with
    console.log()
    .
  • Attachable.FIRST_PERSON_CAMERA
    /
    Attachable.AVATAR
    on held items (guns, aiming reticles, flashlights, fixed-position tools)
    : SDK6 only offered two coarse follow modes —
    Attachable.FIRST_PERSON_CAMERA
    (follow the camera) and
    Attachable.AVATAR
    (follow the avatar root). In SDK7 you have a new choice that didn't exist before: bone-level
    AvatarAttach
    with
    anchorPointId
    (e.g.
    AAPT_RIGHT_HAND
    ). 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 to
    AAPT_RIGHT_HAND
    jitters every frame and is unaimable. The correct SDK7 mapping for held gameplay items is to parent the entity via
    Transform.parent
    , not
    AvatarAttach
    . For any aim-sensitive item (gun, reticle, flashlight, anything the player should be able to point by looking around), the right SDK7 default is
    Transform.parent = engine.CameraEntity
    — this mirrors SDK6's
    FIRST_PERSON_CAMERA
    and tracks both yaw AND pitch, so the muzzle aims where the camera looks.
    Transform.parent = engine.PlayerEntity
    is 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 to
    PlayerEntity
    stays flat when the player tilts the camera up to aim — the item ignores the look direction. Use
    PlayerEntity
    only for body-fixed items that should NOT track aim (a held shield, a static torch, a non-aimed inventory item carried at hip-level). Reserve
    AvatarAttach
    for cosmetic items where riding the animation is desired (hats, halos, backpacks, name plates). Picking by intent —
    engine.CameraEntity
    (aim-sensitive, the recommended default for held weapons),
    engine.PlayerEntity
    (yaw-only / no pitch, for body-fixed items),
    AvatarAttach(boneId)
    (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.
  • 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
    new Billboard(false, true, false)
    (Y-axis billboard) shows its back to the camera under SDK7's
    Billboard.create(e, { billboardMode: BillboardMode.BM_Y })
    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 SDK7
    PBBillboard
    interface has no
    oppositeDirection
    flag
    — the only field is
    billboardMode
    (verified against
    @dcl/ecs
    billboard.gen.d.ts
    ), so the fix is in the model's Transform, not the Billboard component. Rotate the displayed model 180° on Y (
    Quaternion.fromEulerDegrees(0, 180, 0)
    ). If the entity is a parent (
    Billboard
    'd) + a child model, apply the 180° rotation to the child's
    Transform.rotation
    — 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.
  • UI pixel sizes do NOT carry over 1:1: SDK6 UI (
    UICanvas
    /
    UIImage
    /
    UIText
    ) was authored in raw pixels against a fixed screen size, so positions and sizes like
    width = 200
    or
    positionX = -350
    were 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 on
    ReactEcsRenderer.setUiRenderer
    , 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:
    ts
    ReactEcsRenderer.setUiRenderer(uiRoot, { virtualWidth: 1920, virtualHeight: 1080 })
    1920x1080
    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.
    1280x720
    ), use those numbers instead so existing pixel coordinates land in the same place. See [[build-ui]] for the full React-ECS rewrite.
  • 修改
    .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
    时抛出异常。当移植代码从不完整对象构建 Transform,而该对象可能包含
    undefined
    字段时(例如
    Transform.create(e, { position, rotation: maybeRot, scale: maybeScale })
    ),就会遇到此问题。修复方法——完全省略该字段,不要传入
    undefined
    ,从而让 schema 应用默认值:
    typescript
    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 类中无法推断类型的字段不能自动推断。
  • Component 方法: SDK6 component 经常包含
    reset()
    或构造函数逻辑。这些逻辑必须在 SDK7 中移至独立函数——component 仅包含纯数据。
  • Vector2 字段:
    Schemas
    没有
    Schemas.Vector2
    。[未验证——请通过读取 Schemas 对象导出来确认。] 应将其扁平化为两个
    Schemas.Number
    字段,或使用
    Schemas.Map({ x: Schemas.Number, y: Schemas.Number })
    。
  • 在 Entity 对象上调用
    setParent
    :
    SDK7 中的 Entity 是整数,因此不存在该方法。请设置子 Entity 的
    Transform
    上的
    parent
    字段。
  • Color3 与 Color4: 大多数 SDK7 component 字段接受
    Color4
    (包含 alpha)。除非字段明确记录为 Color3,否则应将
    Color3.X()
    替换为
    Color4.X()
    。
  • Material 既不是 Entity,也不是可保留引用的对象: 不存在需要保留引用的
    new Material()
    实例。设置 Material 的唯一方式是
    Material.setPbrMaterial(entity, props)
    ——Material 附着在一个 Entity 上,而不是共享对象。
  • 形状 Entity(BoxShape、PlaneShape 等)需要同时使用 MeshRenderer 和 MeshCollider: SDK6 中单个
    BoxShape
    同时负责渲染和碰撞(带一个标志)。在 SDK7 中它们是两个独立的 component——如果图元需要支持点击或物理交互,应同时添加两者。
  • 在 SDK6 中,使用
    withCollisions: true
    的
    GLTFShape
    默认可以点击可见 mesh:
    SDK7 中,
    GltfContainer
    默认仅在不可见的
    _collider
    mesh 上启用碰撞,某些模型可能尚未针对此机制进行配置。如果 Entity 因缺少 collider 而无法点击,可能需要显式设置
    visibleMeshesCollisionMask
    ,使其包含
    CL_POINTER
    (1)以支持点击、
    CL_PHYSICS
    (2)以支持玩家碰撞,或设为
    3
    同时支持两者。参见 [[add-3d-models]]。
  • 在遍历 component group 时,
    OnPointerDown
    回调不再触发:
    SDK6 中这些回调由事件驱动;SDK7 中应一次性通过
    pointerEventsSystem.onPointerDown(...)
    注册处理器——不要在 system 的 update 循环中调用该方法,否则每帧都会注册一个新处理器。
  • 对于自行在代码中创建 Animator 的移植场景,
    Animator.playSingleAnimation
    对未包含在
    Animator.states
    中的 clip 会静默无效:
    这 specifically 适用于 SDK6 移植路径,即你在 TypeScript 中自行创建
    Animator
    ,并以编程方式调用
    Animator.playSingleAnimation(entity, clipName)
    。此时,如果
    clipName
    尚未存在于 Entity 的
    states
    数组中,
    playSingleAnimation
    会返回
    false
    且不执行任何操作(已验证——
    @dcl/ecs/dist-cjs/components/extended/Animator.js
    第 35–46 行)。系统不会发出警告。SDK6 中,
    Animator.getClip(name)
    会在首次使用时自动创建 clip,因此 SDK6 场景经常在未预先声明
    'Appear'
    的情况下直接调用
    playAnimation('Appear')
    。如果简单地移植
    entity.getComponent(Animator).getClip(name).play()
    →
    Animator.playSingleAnimation(entity, name)
    ,最终会针对移植者从未加入
    states[]
    的 clip 名称调用该方法,从而静默失败。修复方法: 遍历 SDK6 源代码中的每个
    playAnimation
    /
    getClip
    调用,为每个 Entity 收集完整的 clip 名称集合,并在创建
    Animator
    时将它们全部列入
    states
    数组。如需快速迁移的兼容层,以恢复 SDK6“无需配置即可播放任意 clip”的使用体验并支持延迟添加
    states.push
    ,请参见 [[animations-tweens]] 的 PITFALL 部分。此规则不适用于由 Creator Hub Inspector 创建 Animator 的场景(Inspector 会预先将每个 GLB clip 填入
    states[]
    ),也不适用于随附已填充
    states[]
    的 asset pack / smart item,或完全不存在 Animator 的模型——后两者依赖下一条所述的自动播放机制。
  • 将
    anim.stop(); anim.play()
    移植为每帧调用的自定义 SDK7 helper 会产生不必要的序列化开销:
    当使用自定义 SDK7 helper 直接修改
    Animator.states
    (
    s.playing
    、
    s.shouldReset
    ),并且该 helper 由逐帧调用方——system update,或按键持续按下时触发的输入/射线检测回调——调用时,就会出现此问题。动画不会冻结(
    @dcl/ecs
    中针对可变 CRDT 的抑制层会检测到相同的写入并静默丢弃),但每帧调用
    getMutableOrNull()
    都会产生不必要的序列化和字节比较开销。修复方法: 按 Entity 跟踪最后应用的 clip,如果没有变化则提前返回;对于使用相同 clip 的
    noLoop + revertToIdle
    重新调用,只刷新回退计时器,而不是再次修改 states;在重新应用 idle clip 前清除已跟踪的 clip。不适用于仅在状态切换时调用一次 helper 的情况(一次性
    onPointerDown
    、边沿触发的状态机),也不适用于从状态切换调用
    Animator.playSingleAnimation
    而非逐帧调用 helper 的情况。完整 helper 模式请参见 [[animations-tweens]] 中的“Short-circuit clip-switch helpers”最佳实践。
  • 当未附加
    Animator
    时,
    GltfContainer
    会自动播放多动画
    .glb
    中的一个 clip:
    SDK6 模型默认保持绑定姿势;SDK7 会从
    .glb
    中选取一个 clip 并播放。该机制使无 clip 的 Inspector 场景无需注册即可工作,因此它与上一条并不矛盾。当 Entity 没有
    Animator
    时,渲染器会自动播放;当 Entity 有
    Animator
    且调用
    playSingleAnimation
    时,该 clip 必须位于
    states[]
    中。渲染器自动播放哪个 clip 并不会因模型不同而保持稳定——曾观察到一个万圣节场景,其中没有
    Animator
    的小幽灵会从 GLB 中自动播放
    die
    。如需指定其他默认 clip: 附加
    Animator.create(entity, { states: [{ clip: '<intended-default>', playing: true, loop: true }] })
    来接管控制。如果还希望在运行时通过
    playSingleAnimation
    切换 clip:
    请将所有要切换的 clip 列入
    states[]
    (参见上一条)。
  • 带有
    BUTTON_DOWN
    /
    BUTTON_UP
    的
    Input.instance.subscribe
    :
    不存在直接等价的回调。请在 system 内使用
    inputSystem.isTriggered()
    检测“本帧刚刚按下”。
  • @dcl/ecs-scene-utils
    触发区域(“Utils library”)以及手动实现的逐帧 AABB 检测:
    不要移植自定义逐帧位置检测 system,也不要在 SDK7 中从头重新实现。SDK6 没有原生触发区域,因此社区 Utils 库和许多场景的自定义代码会每帧将
    Camera.instance.position
    与盒状或球状区域进行比较,并触发
    onCameraEnter
    /
    onCameraExit
    。SDK7 提供原生
    TriggerArea
    component(
    @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
    ——尽管名称容易引起混淆,它实际指向触发区域自身的 Entity,因此与玩家比较始终为真,保护逻辑永远不会生效)。完整 component 参考请参见
    {baseDir}/references/api-mapping.md
    中的“Trigger areas”部分和 [[add-interactivity]]。
  • 来自
    decentraland-ecs-utils
    (后来为
    @dcl-sdk/utils
    )的
    utils.Delay
    /
    utils.ExpireIn
    以及手动实现的逐帧计时器 system:
    不要移植社区
    Delay
    component,不要在 SDK7 中重新创建它,也不要编写依赖累加
    dt
    的自定义
    setSceneTimeout
    helper。请使用
    @dcl/sdk/ecs
    的
    timers
    命名导出
    (
    import { timers } from '@dcl/sdk/ecs'
    )——它提供绑定到场景 Engine 的
    setTimeout
    /
    clearTimeout
    /
    setInterval
    /
    clearInterval
    。将
    entity.addComponent(new utils.Delay(2000, cb))
    替换为
    timers.setTimeout(cb, 2000)
    ,并删除 SDK6 场景中为承载 Delay component 而创建的“计时器 Entity”。参数顺序为
    (callback, ms)
    ,不是
    (ms, callback)
    ——SDK6
    Delay(ms, cb)
    的顺序与 JavaScript 标准相反,自定义 helper 沿用 SDK6 顺序是一个已知陷阱,并且会进一步放大最初以错误顺序编写 helper 所造成的问题。不要使用原生 JavaScript
    setTimeout
    /
    setInterval
    全局函数
    ——尽管 QuickJS 提供了这些函数,但它们未绑定到场景 Engine,可能引入难以察觉的问题;应始终通过
    timers
    调用。完整参考请参见 [[scene-runtime]] 的 Timers 部分。
  • SDK6“拦截工具”smart item(钥匙、磁铁,以及通过
    Input.instance.subscribe('BUTTON_DOWN', ActionButton.POINTER, true, …)
    + Entity 层级遍历来作用于点击目标的物品)没有直接对应的 SDK7 API。
    pointerEventsSystem.onPointerDown
    仅适用于单个 Entity——不存在全局命中结果订阅机制。迁移模式是创建一个按目标 Entity 名称索引的注册表;目标自身的
    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
    。迁移后应 grep
    Vector3.
    ,并将每个调用与 math 模块进行核对。
  • SDK7 未导出
    log()
    :
    请替换为
    console.log()
    。
  • 手持物品上的
    Attachable.FIRST_PERSON_CAMERA
    /
    Attachable.AVATAR
    (枪械、瞄准准星、手电筒、固定位置工具):SDK6 仅提供两种粗粒度跟随模式——
    Attachable.FIRST_PERSON_CAMERA
    (跟随相机)和
    Attachable.AVATAR
    (跟随 Avatar 根节点)。SDK7 提供了新的选择:支持骨骼级
    AvatarAttach
    ,可使用
    anchorPointId
    (例如
    AAPT_RIGHT_HAND
    )。骨骼锚点看起来是 SDK7 中“在 Avatar 手中”的自然等价方案,但它会继承 Avatar 动画(待机起伏、行走循环、手势),导致跟随骨骼的枪械每帧抖动且无法准确瞄准。手持游戏物品的正确 SDK7 映射是通过
    Transform.parent
    设置父 Entity
    ,而不是使用
    AvatarAttach
    。对于需要精确瞄准的物品(枪械、准星、手电筒,以及玩家应能通过观察方向来指向的任何物品),SDK7 默认应使用
    Transform.parent = engine.CameraEntity
    ——这与 SDK6 的
    FIRST_PERSON_CAMERA
    一致,并同时跟踪偏航角和俯仰角,因此枪口会朝向相机所指的方向。
    Transform.parent = engine.PlayerEntity
    是另一种选择,也是一种常见且隐蔽的失败模式:它跟随玩家根节点(脚部位置 + 身体仅偏航角,没有俯仰角和动画),因此当玩家向上抬头瞄准时,挂在
    PlayerEntity
    下的枪械会保持水平,无法跟随视线方向。只有对于不应跟随瞄准方向的身体固定物品,才应使用
    PlayerEntity
    (例如手持盾牌、固定手电筒、非瞄准型腰挂物品)。
    AvatarAttach
    (骨骼 ID)仅适用于希望跟随动画的装饰性物品(帽子、光环、背包、名牌)。应根据意图进行选择——
    engine.CameraEntity
    (需要精确瞄准,推荐用于手持武器)、
    engine.PlayerEntity
    (仅偏航角、无俯仰角,用于身体固定物品)、
    AvatarAttach
    (骨骼 ID,仅用于装饰性物品并跟随动画)。比较表和枪械示例请参见 [[player-avatar]] 中的“Held items vs cosmetic items”部分。
  • 移植后 Billboard 模型可能朝向相机的反面: 在一些 SDK6 → SDK7 移植中,使用
    new Billboard(false, true, false)
    (Y 轴 Billboard)时正面朝向相机的模型,在 SDK7 中使用
    Billboard.create(e, { billboardMode: BillboardMode.BM_Y })
    并采用相同父子关系后,会显示背面。两个 SDK 对 Billboard 指向相机的模型正面(+Z 还是 -Z)似乎存在差异。SDK7 的
    PBBillboard
    interface 没有
    oppositeDirection
    标志
    ——唯一字段是
    billboardMode
    (已根据
    @dcl/ecs
    的
    billboard.gen.d.ts
    验证),因此应在模型 Transform 上修复,而不是修改 Billboard component。将显示模型在 Y 轴上旋转 180°(
    Quaternion.fromEulerDegrees(0, 180, 0)
    )。如果 Entity 是父 Entity(挂载
    Billboard
    )并包含一个子模型,则应在子Entity 的
    Transform.rotation
    上应用 180° 旋转——父 Entity 的旋转由 Billboard component 管理,覆盖 billboard Entity 自身的旋转不会生效。[未验证这究竟是 SDK 行为明确变更并附带 changelog,还是模型导入器采用其他约定所导致的结果——面向用户的说明应表述为“两个 SDK 对 Billboard 朝向存在差异”,不要声称是某个特定版本引入了该变化。] 参见 [[advanced-rendering]] 的 Billboard 部分以了解 SDK7 component 参考。
  • UI 像素尺寸无法按 1:1 迁移: SDK6 UI(
    UICanvas
    /
    UIImage
    /
    UIText
    )使用原始像素并针对固定屏幕尺寸设计,因此
    width = 200
    或
    positionX = -350
    等位置和尺寸是绝对值。SDK7 React-ECS UI 默认也使用原始屏幕像素进行布局,但实际屏幕尺寸取决于用户设备——因此,原样复制的 SDK6 布局在不同显示器上会呈现不同的相对大小。修复方法是使用
    ReactEcsRenderer.setUiRenderer
    设置虚拟画布,使 Engine 将像素值视为虚拟空间内的坐标,并缩放到真实屏幕。移植 UI 前,应检查 SDK6 源代码(旧版 ECS 位于 https://github.com/decentraland/ecs),确定原始 UI 面向的像素网格,并将其作为虚拟尺寸传入:
    ts
    ReactEcsRenderer.setUiRenderer(uiRoot, { virtualWidth: 1920, virtualHeight: 1080 })
    1920x1080
    是较为合理的默认值,但正确的
    virtualWidth
    /
    virtualHeight
    取决于 SDK6 UI 最初设计时采用的分辨率——例如,如果原 SDK6 场景基于
    1280x720
    ,就应使用这些数值,以确保现有像素坐标落在相同位置。完整 React-ECS 重写请参见 [[build-ui]]。

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.
    GemData.reset(val, x, y)
    ): split the data into the new component, move the logic into a free function that takes
    (entity: Entity, val: number, ...)
    and calls
    Component.getMutable(entity)
    .
  • Component groups that the user holds a reference to and iterates many times: SDK7
    engine.getEntitiesWith(...)
    is recomputed each call. For hot loops over the same set, cache it inside the system or accept the cost.
  • Object pools that rely on
    entity.alive
    : SDK7 entities don't have an
    alive
    flag. Pools must track liveness with a custom component or by checking
    Component.has(entity)
    . 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.
  • UI built with the SDK6
    UICanvas
    /
    UIImage
    /
    UIText
    /
    UIClickable
    : 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 on
    ReactEcsRenderer.setUiRenderer
    so the SDK6 pixel coordinates scale predictably across screens — see the UI pixel sizing pitfall in Common Pitfalls above.
  • @Component('name')
    decorator metadata
    : SDK7 component IDs are strings supplied to
    engine.defineComponent('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.
以下内容没有清晰的 1:1 映射,需要重新设计:
  • 包含实际逻辑的方法或构造函数 component(例如
    GemData.reset(val, x, y)
    ):将数据拆到新 component 中,并把逻辑移入一个接受
    (entity: Entity, val: number, ...)
    参数并调用
    Component.getMutable(entity)
    的独立函数。
  • 用户持有引用并多次遍历的 component group: SDK7 的
    engine.getEntitiesWith(...)
    会在每次调用时重新计算。对于遍历同一集合的热循环,应在 system 内缓存结果,或接受这项开销。
  • 依赖
    entity.alive
    的对象池:
    SDK7 Entity 没有
    alive
    标志。对象池必须使用自定义 component 跟踪存活状态,或通过检查
    Component.has(entity)
    判断。迁移后的 2048 示例将其简化为“始终添加新 Entity”——这适用于低吞吐量场景,但真正的对象池需要显式状态管理。
  • 使用 SDK6
    UICanvas
    /
    UIImage
    /
    UIText
    /
    UIClickable
    构建的 UI:
    与 SDK7 API 不兼容。SDK7 UI 使用 React-ECS(JSX)。参见 [[build-ui]]——这通常需要从头重写。移植时,还应在
    ReactEcsRenderer.setUiRenderer
    上设置虚拟画布尺寸,使 SDK6 像素坐标能够在不同屏幕上按预期缩放——参见上文“常见陷阱”中的 UI 像素尺寸问题。
  • @Component('name')
    decorator metadata:
    SDK7 component ID 是传给
    engine.defineComponent('name', ...)
    的字符串。如果其他工具(composite、多人同步等)通过名称引用该 component,请使用与 SDK6 decorator 相同的名称;否则应选择一致的命名方案。

Cross-References

交叉引用

  • [[create-scene]] — scene.json schema, composite-first rule, scaffolding a fresh SDK7 project
  • [[composites]] —
    .composite
    file format for the optional second pass that moves static entities out of TypeScript
  • [[add-3d-models]] —
    GltfContainer
    , collider masks for clickability
  • [[add-interactivity]] —
    pointerEventsSystem
    (replaces SDK6
    OnPointerDown
    )
  • [[advanced-input]] —
    inputSystem
    polling (replaces SDK6
    Input.instance.subscribe
    )
  • [[scene-runtime]] —
    timers.setTimeout
    /
    timers.setInterval
    from
    @dcl/sdk/ecs
    (replaces SDK6 community
    decentraland-ecs-utils
    Delay
    /
    Interval
    ),
    executeTask
    ,
    fetch
    , realm/scene info
  • [[animations-tweens]] —
    Animator
    and
    Tween
    (replaces SDK6
    Animator
    /
    AnimationState
    )
  • [[audio-video]] —
    AudioSource
    and
    VideoPlayer
  • [[build-ui]] — React-ECS UI (replaces SDK6
    UICanvas
    UI system)
  • [[advanced-rendering]] — PBR materials, TextShape, Billboard
  • [[multiplayer-sync]] —
    syncEntity
    for multiplayer (replaces SDK6
    MessageBus
    / sync libraries)
  • [[create-scene]]——
    scene.json
    schema、Composite 优先规则、新建 SDK7 项目脚手架
  • [[composites]]——
    .composite
    文件格式,用于将静态 Entity 移出 TypeScript 的可选第二阶段
  • [[add-3d-models]]——
    GltfContainer
    、用于点击检测的 collider mask
  • [[add-interactivity]]——
    pointerEventsSystem
    (替代 SDK6
    OnPointerDown
    )
  • [[advanced-input]]——
    inputSystem
    轮询(替代 SDK6
    Input.instance.subscribe
    )
  • [[scene-runtime]]——
    timers.setTimeout
    /
    timers.setInterval
    (替代 SDK6 社区
    decentraland-ecs-utils
    的
    Delay
    /
    Interval
    )、
    executeTask
    、
    fetch
    、realm/scene 信息
  • [[animations-tweens]]——
    Animator
    和
    Tween
    (替代 SDK6
    Animator
    /
    AnimationState
    )
  • [[audio-video]]——
    AudioSource
    和
    VideoPlayer
  • [[build-ui]]——React-ECS UI(替代 SDK6
    UICanvas
    UI 系统)
  • [[advanced-rendering]]——PBR 材质、
    TextShape
    、Billboard
  • [[multiplayer-sync]]——用于多人同步的
    syncEntity
    (替代 SDK6
    MessageBus
    / 同步库)

References

参考资料

  • {baseDir}/references/api-mapping.md
    — full table of SDK6 → SDK7 symbol conversions
  • {baseDir}/references/migration-example.md
    — annotated before/after using the 2048 game scene (entity setup, components, systems, pointer events, animations)
  • {baseDir}/references/api-mapping.md
    ——完整的 SDK6 → SDK7 符号转换表
  • {baseDir}/references/migration-example.md
    ——使用 2048 游戏场景制作的带注释迁移前后对照(Entity 设置、component、system、指针事件、动画)