script-components

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Writing Script Components for Creator Hub

为 Creator Hub 编写 Script 组件

This document explains how to write
.tsx
files that are used inside a Script component on an entity in a Creator Hub scene. These scripts run as self-contained classes attached to individual entities.
本文介绍如何编写
.tsx
文件,以供 Creator Hub 场景中实体上的 Script 组件 使用。这些脚本以自包含类的形式运行,并附加到单个实体上。

Where script files must live

Script 文件的存放位置

Script files referenced by a Script component MUST live inside the
assets/
folder
— use
assets/scripts/
(e.g.
assets/scripts/MyScript.ts
). Do NOT place them in a root-level
scripts/
folder.
Why: the scene's
tsconfig.json
only includes
src/**/*
and
assets/**/*
:
json
"include": ["src/**/*.ts", "src/**/*.tsx", "assets/**/*.ts", "assets/**/*.tsx"]
A file in a root-level
scripts/
folder falls outside these globs, so the TypeScript type checker (and the build's type-check step) won't cover it. The Script component's
path
field is resolved relative to the project root, so it must match the real file location — e.g.
"path": "assets/scripts/SitChair.ts"
.
Script 组件引用的 Script 文件必须位于 assets/ 文件夹内——请使用
assets/scripts/
(例如
assets/scripts/MyScript.ts
)。不要将其放在根目录下的
scripts/
文件夹中。
原因:场景的
tsconfig.json
仅包含
src/**/*
和
assets/**/*
:
json
"include": ["src/**/*.ts", "src/**/*.tsx", "assets/**/*.ts", "assets/**/*.tsx"]
位于根目录
scripts/
文件夹中的文件不在这些 glob 的匹配范围内,因此 TypeScript 类型检查器(以及构建过程中的类型检查步骤)不会覆盖它。Script 组件的
path
字段相对于项目根目录进行解析,因此必须与实际文件位置一致——例如
"path": "assets/scripts/SitChair.ts"
。

Script structure

脚本结构

Every script is a single exported class with:
  • A constructor that receives configurable parameters (exposed in the Creator Hub UI).
  • An optional
    start()
    method, called once when the scene loads.
  • An optional
    update(dt: number)
    method, called every frame (~30 FPS; see "When scripts run" below for ordering — by default scripts update after all regular systems).
The first two constructor parameters must always be
public src: string
and
public entity: Entity
— do not remove or reorder them.
ts
import { engine, Entity, Transform } from '@dcl/sdk/ecs'

export class MyScript {
  constructor(
    public src: string,
    public entity: Entity,
    public speed: number = 1
  ) {}

  start() {
    console.log('Script started on entity:', this.entity)
  }

  update(dt: number) {
    const transform = Transform.getMutable(this.entity)
    transform.rotation.y += this.speed * dt
  }
}
每个脚本都是一个单独导出的类,包含:
  • 一个 constructor,用于接收可配置参数(这些参数会显示在 Creator Hub UI 中)。
  • 一个可选的
    start()
    方法,在场景加载时调用一次。
  • 一个可选的
    update(dt: number)
    方法,每帧调用一次(约 30 FPS;有关执行顺序,请参阅下文“脚本何时运行”——默认情况下,脚本在所有常规 system 之后更新)。
constructor 的前两个参数必须始终是
public src: string
和
public entity: Entity
——不要删除或调整它们的顺序。
ts
import { engine, Entity, Transform } from '@dcl/sdk/ecs'

export class MyScript {
  constructor(
    public src: string,
    public entity: Entity,
    public speed: number = 1
  ) {}

  start() {
    console.log('Script started on entity:', this.entity)
  }

  update(dt: number) {
    const transform = Transform.getMutable(this.entity)
    transform.rotation.y += this.speed * dt
  }
}

When scripts run — the
priority
field (IMPORTANT)

脚本何时运行——
priority
字段(重要)

The Creator Hub Script component has a
priority
field (separate from constructor params — it's set in the component UI, not in your class). It defaults to
0
, and this has non-obvious consequences:
  • Default priority
    0
    = scripts run LAST each frame.
    At build time
    @dcl/sdk-commands
    groups all scripts by their
    priority
    value and registers ONE engine system per group via
    engine.addSystem(updateLoop, Number(priority))
    . Because engine systems run highest-priority-first (regular systems use
    100000
    , UI uses
    100000
    ), priority
    0
    runs after everything else.
  • All scripts sharing the same
    priority
    share ONE system callback
    and run sequentially inside it. A single heavy script's
    update()
    therefore delays every other script in the same priority group that frame.
  • To run a script's
    update()
    before regular systems
    , raise its
    priority
    (e.g.
    1000000
    ) in the Script component. This also gives it its own dedicated system. This is how you'd step a physics library (cannon.js) inside
    update(dt)
    ahead of systems that read the result — there is no separate physics loop.
Higher
priority
number = earlier execution — the OPPOSITE of the "priority 1 = first" assumption. See the
scene-runtime
skill's "System Execution Order & Priority" section for the underlying engine rule.
Creator Hub Script 组件有一个
priority
字段(它与 constructor 参数相互独立——通过组件 UI 设置,而不是在类中设置)。该字段默认为
0
,这会带来一些不明显的影响:
  • 默认 priority
    0
    意味着脚本每帧最后运行。
    构建时,
    @dcl/sdk-commands
    会按
    priority
    值对所有脚本进行分组,并通过
    engine.addSystem(updateLoop, Number(priority))
    为每组注册一个 engine system。由于 engine system 按优先级从高到低运行(常规 system 使用
    100000
    ,UI 也使用
    100000
    ),因此 priority
    0
    会在所有其他内容之后运行。
  • 共享相同
    priority
    的所有脚本会共用一个 system callback
    ,并在其中依次执行。因此,某个开销较大的脚本,其
    update()
    会拖慢同一 priority 组中该帧执行的所有其他脚本。
  • 如需让脚本的
    update()
    在常规 system 之前运行
    ,请在 Script 组件中提高其
    priority
    (例如
    1000000
    )。这也会让脚本拥有自己专用的 system。如果需要在读取结果的 system 运行前,于
    update(dt)
    中推进 physics library(cannon.js),应采用这种方式——因为不存在独立的 physics loop。
priority
数字越高 = 执行越早——这与“priority 1 = 最先执行”的假设正好相反。底层 engine 规则请参阅
scene-runtime
skill 的“System Execution Order & Priority”一节。

Constructor parameters

Constructor 参数

Parameters declared in the constructor are exposed in the Creator Hub UI and can be configured per-entity. Allowed types:
  • string
  • number
  • boolean
  • Entity
    (lets the user pick another entity from the scene)
Both
public
and
private
parameters are exposed to Creator Hub. Use
this.<paramName>
to access values in your code.
在 constructor 中声明的参数会显示在 Creator Hub UI 中,并可为每个实体单独配置。允许的类型:
  • string
  • number
  • boolean
  • Entity
    (允许用户从场景中选择另一个 entity)
public 和 private 参数都会暴露给 Creator Hub。使用
this.<paramName>
在代码中访问这些值。

Default values

默认值

Provide default values so the script works out of the box:
ts
constructor(
  public src: string,
  public entity: Entity,
  public radius: number = 5,
  public label: string = 'Hello',
  public enabled: boolean = true,
) {}
请提供默认值,使脚本无需额外配置即可运行:
ts
constructor(
  public src: string,
  public entity: Entity,
  public radius: number = 5,
  public label: string = 'Hello',
  public enabled: boolean = true,
) {}

Optional parameters

可选参数

Use
?
for parameters that may be left empty by the user:
ts
constructor(
  public src: string,
  public entity: Entity,
  public targetEntity?: Entity,
  public message?: string,
) {}
对于用户可能留空的参数,请使用
?
:
ts
constructor(
  public src: string,
  public entity: Entity,
  public targetEntity?: Entity,
  public message?: string,
) {}

Parameter tooltips

参数 Tooltip

Add
@param
annotations in a JSDoc comment block directly before the constructor to show tooltips in the Creator Hub UI:
ts
/**
 * @param startDate - The start date of the campaign in YYYY-MM-DD format
 * @param endDate - The end date of the campaign in YYYY-MM-DD format
 * @param wearableYOffset - How many meters above the ground the wearable should be displayed
 */
constructor(
  public src: string,
  public entity: Entity,
  public startDate?: string,
  public endDate?: string,
  public wearableYOffset: number = 0.5,
) {}
在 constructor 前紧邻位置添加带
@param
注解的 JSDoc comment block,即可在 Creator Hub UI 中显示 tooltip:
ts
/**
 * @param startDate - The start date of the campaign in YYYY-MM-DD format
 * @param endDate - The end date of the campaign in YYYY-MM-DD format
 * @param wearableYOffset - How many meters above the ground the wearable should be displayed
 */
constructor(
  public src: string,
  public entity: Entity,
  public startDate?: string,
  public endDate?: string,
  public wearableYOffset: number = 0.5,
) {}

Referencing assets with
this.src

使用
this.src
引用 assets

If your script uses additional assets that are only loaded via code (sound files, textures, models, etc.), they won't be automatically included in the custom item folder. You must add those files manually.
Always use
this.src
to build the path to bundled asset files, because the actual file location may differ when the item is used in another scene:
ts
import { AudioSource } from '@dcl/sdk/ecs'

start() {
  AudioSource.create(this.entity, {
    audioClipUrl: this.src + '/sounds/click.mp3',
    playing: false
  })
}
如果脚本使用了仅能通过代码加载的其他 assets(声音文件、纹理、模型等),这些文件不会自动包含在 custom item 文件夹中。你必须手动添加这些文件。
始终使用
this.src
构建打包 asset 文件的路径,因为当该 item 在另一个 scene 中使用时,文件的实际位置可能发生变化:
ts
import { AudioSource } from '@dcl/sdk/ecs'

start() {
  AudioSource.create(this.entity, {
    audioClipUrl: this.src + '/sounds/click.mp3',
    playing: false
  })
}

Referencing child entities

引用 child entities

Do not pass entities that belong to the same custom item as
Entity
input parameters. Entity IDs are not stable across scenes — an entity ID that is valid in your development scene may not exist in a user's scene.
Instead, find child entities at runtime by iterating over the entity hierarchy and matching their
Name
component value with a substring match (e.g.
.startsWith(...)
or
.includes(...)
).
Best practice: match a substring of the child's name, not an exact name, and not a per-instance constructor parameter. When a user duplicates a smart item or drops multiple copies into a scene, the Creator Hub editor auto-numbers the duplicated child entities — e.g.
"Sit Spot"
,
"Sit Spot 2"
,
"Sit Spot 3"
, … A substring match (
name.includes('Sit Spot')
) finds the matching child in every copy automatically, with zero per-instance configuration.
  • Exact-name match fails on every duplicate after the first, because their names are auto-suffixed.
  • A constructor
    string
    /
    Entity
    parameter for the child name
    forces the user to manually rename or rewire each copy, which defeats the purpose of a reusable scripted item.
The example below uses
.startsWith('Needle')
— a substring-style match — for exactly this reason.
ts
import { engine, Entity, Transform, Name } from '@dcl/sdk/ecs'

export class ClapMeter {
  private needleEntities: Entity[] = []

  constructor(
    public src: string,
    public entity: Entity,
  ) {}

  start() {
    for (const [childEntity, transform] of engine.getEntitiesWith(Transform)) {
      if (transform.parent === this.entity) {
        const nameComponent = Name.getOrNull(childEntity)
        if (nameComponent && nameComponent.value.startsWith('Needle')) {
          this.needleEntities.push(childEntity)
        }
      }
    }
  }
}
This pattern keeps the script portable: as long as the child entities have names containing the expected substring, it works in any scene and across any number of duplicated copies.
不要将属于同一个 custom item 的 entities 作为
Entity
输入参数传入。Entity ID 在不同 scene 之间并不稳定——在开发 scene 中有效的 Entity ID,在用户的 scene 中可能不存在。
相反,应在运行时遍历 entity 层级,并通过将
Name
component 的值与预期名称做 substring 匹配来查找 child entities(例如使用
.startsWith(...)
或
.includes(...)
)。
最佳实践:匹配 child entity 名称的 substring,而不是精确名称,也不要将其作为每个实例的 constructor 参数。 当用户复制 smart item 或将多个副本放入 scene 时,Creator Hub editor 会自动为复制出的 child entities 编号——例如
"Sit Spot"
、
"Sit Spot 2"
、
"Sit Spot 3"
,等等。substring 匹配(
name.includes('Sit Spot')
)会自动在每个副本中找到对应的 child entity,无需进行任何逐实例配置。
  • 精确名称匹配会在第一个副本之后的所有副本上失败,因为它们的名称会自动添加后缀。
  • 将 child name 设为 constructor 的
    string
    /
    Entity
    参数,会迫使用户为每个副本手动重命名或重新连接,从而违背可复用脚本 item 的初衷。
下面的示例正是出于这一原因使用了
.startsWith('Needle')
——即 substring 风格的匹配。
ts
import { engine, Entity, Transform, Name } from '@dcl/sdk/ecs'

export class ClapMeter {
  private needleEntities: Entity[] = []

  constructor(
    public src: string,
    public entity: Entity,
  ) {}

  start() {
    for (const [childEntity, transform] of engine.getEntitiesWith(Transform)) {
      if (transform.parent === this.entity) {
        const nameComponent = Name.getOrNull(childEntity)
        if (nameComponent && nameComponent.value.startsWith('Needle')) {
          this.needleEntities.push(childEntity)
        }
      }
    }
  }
}
这种模式可让脚本保持可移植性:只要 child entities 的名称包含预期 substring,该脚本便能在任何 scene 中、跨任意数量的复制副本正常工作。

Defining actions (
@action
)

定义 actions(
@action
)

If your script has functions that could be useful to call from other items in the scene, mark them by adding a JSDoc comment block (
/** ... */
) with an
@action
tag directly before the method. Then add an Action component to the entity and define a corresponding action. This lets other smart items (e.g. a button) pick and trigger this action.
CRITICAL: Use ONLY the
@action
JSDoc tag — NEVER decorator syntax (
@action()
above the method). The Creator Hub parser has no decorators plugin, so a decorator makes parsing fail and ALL params and actions silently disappear from the UI.
An optional description line before the
@action
tag becomes the action's description shown in the Creator Hub UI.
ts
import { engine, Entity } from '@dcl/sdk/ecs'

export class TreasureChest {
  private isOpen = false

  constructor(
    public src: string,
    public entity: Entity,
  ) {}

  /**
   * Opens the chest
   * @action
   */
  open() {
    if (this.isOpen) return
    this.isOpen = true
    console.log('Chest opened!')
  }

  /**
   * Closes the chest
   * @action
   */
  close() {
    if (!this.isOpen) return
    this.isOpen = false
    console.log('Chest closed!')
  }
}
With the
@action
JSDoc tag,
open
and
close
become available in the Actions component dropdown and can be triggered by other smart items or scripts.
如果脚本中包含可从 scene 内其他 items 调用的 functions,请通过在相应方法前直接添加 JSDoc comment block(
/** ... */
)并加入
@action
tag 来标记。然后,在 entity 上添加 Action 组件并定义对应的 action。这样,其他 smart items(例如按钮)便可以选择并触发该 action。
关键:只能使用
@action
JSDoc 标签——绝不要使用 decorator 语法(在方法上方写
@action()
)。Creator Hub parser 没有 decorators plugin,因此使用 decorator 会导致解析失败,并使 UI 中的所有 params 和 actions 悄然消失。
在
@action
tag 之前添加可选的 description 行,该行会成为 Creator Hub UI 中显示的 action description。
ts
import { engine, Entity } from '@dcl/sdk/ecs'

export class TreasureChest {
  private isOpen = false

  constructor(
    public src: string,
    public entity: Entity,
  ) {}

  /**
   * Opens the chest
   * @action
   */
  open() {
    if (this.isOpen) return
    this.isOpen = true
    console.log('Chest opened!')
  }

  /**
   * Closes the chest
   * @action
   */
  close() {
    if (!this.isOpen) return
    this.isOpen = false
    console.log('Chest closed!')
  }
}
使用
@action
JSDoc tag 后,
open
和
close
就会出现在 Actions 组件的下拉菜单中,并可由其他 smart items 或 scripts 触发。

ActionCallback parameters

ActionCallback 参数

Use the
ActionCallback
type from
~sdk/script-utils
to let users wire up editor-configured actions as callbacks on your script. The user can then assign any action (from any item) to that callback in the Creator Hub UI.
ts
import { Entity } from '@dcl/sdk/ecs'
import type { ActionCallback } from '~sdk/script-utils'

export class Padlock {
  constructor(
    public src: string,
    public entity: Entity,
    public onUnlock: ActionCallback,
  ) {}

  /**
   * @action
   */
  solve() {
    this.onUnlock()
  }
}
使用
~sdk/script-utils
中的
ActionCallback
类型,允许用户将编辑器中配置的 actions 作为 script 上的 callbacks 进行连接。随后,用户便可以在 Creator Hub UI 中,将任意 item 的任意 action 指派给该 callback。
ts
import { Entity } from '@dcl/sdk/ecs'
import type { ActionCallback } from '~sdk/script-utils'

export class Padlock {
  constructor(
    public src: string,
    public entity: Entity,
    public onUnlock: ActionCallback,
  ) {}

  /**
   * @action
   */
  solve() {
    this.onUnlock()
  }
}

Calling other scripts from code

从代码调用其他 scripts

Use the runtime utilities in
~sdk/script-utils
to call methods on other Script component instances:
ts
import {
  callScriptMethod,
  getScriptInstance,
  getAllScriptInstances,
  getScriptInstancesByPath
} from '~sdk/script-utils'

callScriptMethod(entity, 'assets/scripts/Padlock.ts', 'solve', 123)

const instance = getScriptInstance(entity, 'assets/scripts/Padlock.ts')
const allOnEntity = getAllScriptInstances(entity)
const allByPath = getScriptInstancesByPath('assets/scripts/Padlock.ts')
使用
~sdk/script-utils
中的 runtime utilities,可调用其他 Script 组件实例上的 methods:
ts
import {
  callScriptMethod,
  getScriptInstance,
  getAllScriptInstances,
  getScriptInstancesByPath
} from '~sdk/script-utils'

callScriptMethod(entity, 'assets/scripts/Padlock.ts', 'solve', 123)

const instance = getScriptInstance(entity, 'assets/scripts/Padlock.ts')
const allOnEntity = getAllScriptInstances(entity)
const allByPath = getScriptInstancesByPath('assets/scripts/Padlock.ts')