script-components
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWriting Script Components for Creator Hub
为 Creator Hub 编写 Script 组件
This document explains how to write 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 组件 使用。这些脚本以自包含类的形式运行,并附加到单个实体上。
.tsxWhere script files must live
Script 文件的存放位置
Script files referenced by a Script component MUST live inside the folder — use (e.g. ). Do NOT place them in a root-level folder.
assets/assets/scripts/assets/scripts/MyScript.tsscripts/Why: the scene's only includes and :
tsconfig.jsonsrc/**/*assets/**/*json
"include": ["src/**/*.ts", "src/**/*.tsx", "assets/**/*.ts", "assets/**/*.tsx"]A file in a root-level 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 field is resolved relative to the project root, so it must match the real file location — e.g. .
scripts/path"path": "assets/scripts/SitChair.ts"Script 组件引用的 Script 文件必须位于 assets/ 文件夹内——请使用 (例如 )。不要将其放在根目录下的 文件夹中。
assets/scripts/assets/scripts/MyScript.tsscripts/原因:场景的 仅包含 和 :
tsconfig.jsonsrc/**/*assets/**/*json
"include": ["src/**/*.ts", "src/**/*.tsx", "assets/**/*.ts", "assets/**/*.tsx"]位于根目录 文件夹中的文件不在这些 glob 的匹配范围内,因此 TypeScript 类型检查器(以及构建过程中的类型检查步骤)不会覆盖它。Script 组件的 字段相对于项目根目录进行解析,因此必须与实际文件位置一致——例如 。
scripts/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 method, called once when the scene loads.
start() - An optional method, called every frame (~30 FPS; see "When scripts run" below for ordering — by default scripts update after all regular systems).
update(dt: number)
The first two constructor parameters must always be and — do not remove or reorder them.
public src: stringpublic entity: Entityts
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() - 一个可选的 方法,每帧调用一次(约 30 FPS;有关执行顺序,请参阅下文“脚本何时运行”——默认情况下,脚本在所有常规 system 之后更新)。
update(dt: number)
constructor 的前两个参数必须始终是 和 ——不要删除或调整它们的顺序。
public src: stringpublic entity: Entityts
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脚本何时运行——priority
字段(重要)
priorityThe Creator Hub Script component has a field (separate from constructor params — it's set in the component UI, not in your class). It defaults to , and this has non-obvious consequences:
priority0- Default priority = scripts run LAST each frame. At build time
0groups all scripts by their@dcl/sdk-commandsvalue and registers ONE engine system per group viapriority. Because engine systems run highest-priority-first (regular systems useengine.addSystem(updateLoop, Number(priority)), UI uses100000), priority100000runs after everything else.0 - All scripts sharing the same share ONE system callback and run sequentially inside it. A single heavy script's
prioritytherefore delays every other script in the same priority group that frame.update() - To run a script's before regular systems, raise its
update()(e.g.priority) in the Script component. This also gives it its own dedicated system. This is how you'd step a physics library (cannon.js) inside1000000ahead of systems that read the result — there is no separate physics loop.update(dt)
Highernumber = earlier execution — the OPPOSITE of the "priority 1 = first" assumption. See thepriorityskill's "System Execution Order & Priority" section for the underlying engine rule.scene-runtime
Creator Hub Script 组件有一个 字段(它与 constructor 参数相互独立——通过组件 UI 设置,而不是在类中设置)。该字段默认为 ,这会带来一些不明显的影响:
priority0- 默认 priority 意味着脚本每帧最后运行。 构建时,
0会按@dcl/sdk-commands值对所有脚本进行分组,并通过priority为每组注册一个 engine system。由于 engine system 按优先级从高到低运行(常规 system 使用engine.addSystem(updateLoop, Number(priority)),UI 也使用100000),因此 priority100000会在所有其他内容之后运行。0 - 共享相同 的所有脚本会共用一个 system callback,并在其中依次执行。因此,某个开销较大的脚本,其
priority会拖慢同一 priority 组中该帧执行的所有其他脚本。update() - 如需让脚本的 在常规 system 之前运行,请在 Script 组件中提高其
update()(例如priority)。这也会让脚本拥有自己专用的 system。如果需要在读取结果的 system 运行前,于1000000中推进 physics library(cannon.js),应采用这种方式——因为不存在独立的 physics loop。update(dt)
数字越高 = 执行越早——这与“priority 1 = 最先执行”的假设正好相反。底层 engine 规则请参阅priorityskill 的“System Execution Order & Priority”一节。scene-runtime
Constructor parameters
Constructor 参数
Parameters declared in the constructor are exposed in the Creator Hub UI and can be configured per-entity. Allowed types:
stringnumberboolean- (lets the user pick another entity from the scene)
Entity
Both and parameters are exposed to Creator Hub. Use to access values in your code.
publicprivatethis.<paramName>在 constructor 中声明的参数会显示在 Creator Hub UI 中,并可为每个实体单独配置。允许的类型:
stringnumberboolean- (允许用户从场景中选择另一个 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 annotations in a JSDoc comment block directly before the constructor to show tooltips in the Creator Hub UI:
@paramts
/**
* @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 前紧邻位置添加带 注解的 JSDoc comment block,即可在 Creator Hub UI 中显示 tooltip:
@paramts
/**
* @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使用 this.src
引用 assets
this.srcIf 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 to build the path to bundled asset files, because the actual file location may differ when the item is used in another scene:
this.srcts
import { AudioSource } from '@dcl/sdk/ecs'
start() {
AudioSource.create(this.entity, {
audioClipUrl: this.src + '/sounds/click.mp3',
playing: false
})
}如果脚本使用了仅能通过代码加载的其他 assets(声音文件、纹理、模型等),这些文件不会自动包含在 custom item 文件夹中。你必须手动添加这些文件。
始终使用 构建打包 asset 文件的路径,因为当该 item 在另一个 scene 中使用时,文件的实际位置可能发生变化:
this.srcts
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 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.
EntityInstead, find child entities at runtime by iterating over the entity hierarchy and matching their component value with a substring match (e.g. or ).
Name.startsWith(...).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. , , , … A substring match () finds the matching child in every copy automatically, with zero per-instance configuration.
"Sit Spot""Sit Spot 2""Sit Spot 3"name.includes('Sit Spot')- Exact-name match fails on every duplicate after the first, because their names are auto-suffixed.
- A constructor /
stringparameter for the child name forces the user to manually rename or rewire each copy, which defeats the purpose of a reusable scripted item.Entity
The example below uses — a substring-style match — for exactly this reason.
.startsWith('Needle')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 ID 在不同 scene 之间并不稳定——在开发 scene 中有效的 Entity ID,在用户的 scene 中可能不存在。
Entity相反,应在运行时遍历 entity 层级,并通过将 component 的值与预期名称做 substring 匹配来查找 child entities(例如使用 或 )。
Name.startsWith(...).includes(...)最佳实践:匹配 child entity 名称的 substring,而不是精确名称,也不要将其作为每个实例的 constructor 参数。 当用户复制 smart item 或将多个副本放入 scene 时,Creator Hub editor 会自动为复制出的 child entities 编号——例如 、、,等等。substring 匹配()会自动在每个副本中找到对应的 child entity,无需进行任何逐实例配置。
"Sit Spot""Sit Spot 2""Sit Spot 3"name.includes('Sit Spot')- 精确名称匹配会在第一个副本之后的所有副本上失败,因为它们的名称会自动添加后缀。
- 将 child name 设为 constructor 的 /
string参数,会迫使用户为每个副本手动重命名或重新连接,从而违背可复用脚本 item 的初衷。Entity
下面的示例正是出于这一原因使用了 ——即 substring 风格的匹配。
.startsWith('Needle')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
)
@action定义 actions(@action
)
@actionIf 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 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.
/** ... */@actionCRITICAL: Use ONLY the JSDoc tag — NEVER decorator syntax ( 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.
@action@action()An optional description line before the tag becomes the action's description shown in the Creator Hub UI.
@actionts
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 JSDoc tag, and become available in the Actions component dropdown and can be triggered by other smart items or scripts.
@actionopenclose如果脚本中包含可从 scene 内其他 items 调用的 functions,请通过在相应方法前直接添加 JSDoc comment block()并加入 tag 来标记。然后,在 entity 上添加 Action 组件并定义对应的 action。这样,其他 smart items(例如按钮)便可以选择并触发该 action。
/** ... */@action关键:只能使用 JSDoc 标签——绝不要使用 decorator 语法(在方法上方写 )。Creator Hub parser 没有 decorators plugin,因此使用 decorator 会导致解析失败,并使 UI 中的所有 params 和 actions 悄然消失。
@action@action()在 tag 之前添加可选的 description 行,该行会成为 Creator Hub UI 中显示的 action description。
@actionts
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!')
}
}使用 JSDoc tag 后, 和 就会出现在 Actions 组件的下拉菜单中,并可由其他 smart items 或 scripts 触发。
@actionopencloseActionCallback parameters
ActionCallback 参数
Use the type from 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.
ActionCallback~sdk/script-utilsts
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()
}
}使用 中的 类型,允许用户将编辑器中配置的 actions 作为 script 上的 callbacks 进行连接。随后,用户便可以在 Creator Hub UI 中,将任意 item 的任意 action 指派给该 callback。
~sdk/script-utilsActionCallbackts
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 to call methods on other Script component instances:
~sdk/script-utilsts
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')使用 中的 runtime utilities,可调用其他 Script 组件实例上的 methods:
~sdk/script-utilsts
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')