scene-runtime

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Scene Runtime APIs

场景运行时 API

Cross-cutting runtime APIs available in every Decentraland SDK7 scene.
每个 Decentraland SDK7 场景都可使用的横跨各领域运行时 API。

Async Tasks

异步任务

The scene runtime is single-threaded. Wrap any async work in
executeTask()
(or an async function) — bare promises are silently dropped:
typescript
import { executeTask } from "@dcl/sdk/ecs";

executeTask(async () => {
  const res = await fetch("https://api.example.com/data");
  const data = await res.json();
  console.log(data);
});
场景运行时是单线程的。将所有异步工作包装在
executeTask()
(或 async 函数)中——未包装的 Promise 会被静默丢弃:
typescript
import { executeTask } from "@dcl/sdk/ecs";

executeTask(async () => {
  const res = await fetch("https://api.example.com/data");
  const data = await res.json();
  console.log(data);
});

HTTP: fetch & signedFetch

HTTP:fetch 与 signedFetch

Plain fetch works for public APIs:
typescript
const res = await fetch("https://api.example.com/data");
signedFetch proves the player's identity to your backend. Use
getHeaders()
to obtain only the signed headers (useful when a library manages its own fetch):
typescript
import { signedFetch, getHeaders } from "~system/SignedFetch";

// Full signed request
const res = await signedFetch({
  url: "https://your-server.com/api",
  init: { method: "POST", body: JSON.stringify(payload) },
});

// Get signed headers only (for custom fetch calls)
const { headers } = await getHeaders({ url: "https://your-server.com/api" });
signedFetch
returns
{ ok, status, statusText, headers, body }
where
body
is a string (call
JSON.parse(response.body)
yourself — there is no
.json()
). The signed identity headers are added automatically; your backend verifies them per ADR-44.
Permission: the nominal permission for external HTTP is
"USE_FETCH"
(and
"USE_WEBSOCKET"
for sockets), declared in
scene.json
requiredPermissions
. In practice plain/signed
fetch
to non-media hosts is not hard-blocked in preview/Worlds even without it (the
66,6-signed-fetch
test scene calls
signedFetch
with an empty
requiredPermissions
). Declare
USE_FETCH
anyway for correctness and forward-compat.
signedFetch
does not require prior player interaction — restricted actions do,
fetch
/
signedFetch
do not.
普通 fetch 可用于公共 API:
typescript
const res = await fetch("https://api.example.com/data");
signedFetch 用于向后端证明玩家身份。使用
getHeaders()
仅获取已签名的标头(当某个库自行管理其 fetch 时,这会很有用):
typescript
import { signedFetch, getHeaders } from "~system/SignedFetch";

// Full signed request
const res = await signedFetch({
  url: "https://your-server.com/api",
  init: { method: "POST", body: JSON.stringify(payload) },
});

// Get signed headers only (for custom fetch calls)
const { headers } = await getHeaders({ url: "https://your-server.com/api" });
signedFetch
返回
{ ok, status, statusText, headers, body }
,其中
body
是字符串(请自行调用
JSON.parse(response.body)
——没有
.json()
)。签名身份标头会自动添加;你的后端可根据 ADR-44 对其进行验证。
权限:外部 HTTP 的权限名称为
"USE_FETCH"
(socket 则为
"USE_WEBSOCKET"
),需在
scene.json
的
requiredPermissions
中声明。实际上,即使未声明该权限,在预览/Worlds 中对非媒体主机的普通/签名
fetch
也不会被硬性拦截(
66,6-signed-fetch
测试场景在
requiredPermissions
为空时调用了
signedFetch
)。为确保正确性和向前兼容,仍应声明
USE_FETCH
。
signedFetch
不要求玩家事先进行交互——受限操作需要,而
fetch
/
signedFetch
不需要。

WebSocket

WebSocket

typescript
const ws = new WebSocket("wss://your-server.com/ws");
ws.onopen = () => ws.send("hello");
ws.onmessage = (event) => console.log(event.data);
ws.onclose = () => console.log("disconnected");
typescript
const ws = new WebSocket("wss://your-server.com/ws");
ws.onopen = () => ws.send("hello");
ws.onmessage = (event) => console.log(event.data);
ws.onclose = () => console.log("disconnected");

Scene & Realm Information

场景与 Realm 信息

typescript
import { getSceneInformation, getRealm, getExplorerInformation } from "~system/Runtime";

executeTask(async () => {
  // Scene info: URN, content mappings, metadata JSON, baseUrl
  const scene = await getSceneInformation({});
  const metadata = JSON.parse(scene.metadataJson);
  // metadata.scene?.parcels (parcel list), metadata.display?.title (scene title)
  console.log(scene.urn, scene.baseUrl, metadata);

  // Realm info: baseUrl, realmName, isPreview, networkId (1 = mainnet, 5 = goerli), commsAdapter
  const realm = await getRealm({});
  console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview); // realmName e.g. "peer-us-1"

  // Explorer info: agent string, platform, configurations
  const explorer = await getExplorerInformation({});
  console.log(explorer.agent, explorer.platform);
});
Check
realm.realmInfo?.isPreview
to detect preview mode and gate debug features.
typescript
import { getSceneInformation, getRealm, getExplorerInformation } from "~system/Runtime";

executeTask(async () => {
  // Scene info: URN, content mappings, metadata JSON, baseUrl
  const scene = await getSceneInformation({});
  const metadata = JSON.parse(scene.metadataJson);
  // metadata.scene?.parcels (parcel list), metadata.display?.title (scene title)
  console.log(scene.urn, scene.baseUrl, metadata);

  // Realm info: baseUrl, realmName, isPreview, networkId (1 = mainnet, 5 = goerli), commsAdapter
  const realm = await getRealm({});
  console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview); // realmName e.g. "peer-us-1"

  // Explorer info: agent string, platform, configurations
  const explorer = await getExplorerInformation({});
  console.log(explorer.agent, explorer.platform);
});
检查
realm.realmInfo?.isPreview
以检测预览模式,并据此控制调试功能的启用。

World Time

世界时间

typescript
import { getWorldTime } from "~system/Runtime";

executeTask(async () => {
  const { seconds } = await getWorldTime({});
  // seconds = coordinated world time (cycles 0-86400 for day/night)
});
typescript
import { getWorldTime } from "~system/Runtime";

executeTask(async () => {
  const { seconds } = await getWorldTime({});
  // seconds = coordinated world time (cycles 0-86400 for day/night)
});

Read Deployed Files

读取已部署文件

Read files deployed with the scene at runtime — use it for data files like JSON configs or level data:
typescript
import { readFile } from "~system/Runtime";

executeTask(async () => {
  const result = await readFile({ fileName: "data/config.json" });
  const text = new TextDecoder().decode(result.content);
  const config = JSON.parse(text);
});
在运行时读取随场景部署的文件——适用于 JSON 配置或关卡数据等数据文件:
typescript
import { readFile } from "~system/Runtime";

executeTask(async () => {
  const result = await readFile({ fileName: "data/config.json" });
  const text = new TextDecoder().decode(result.content);
  const config = JSON.parse(text);
});

EngineInfo Component

EngineInfo 组件

Access frame-level timing:
typescript
import { EngineInfo } from "@dcl/sdk/ecs";

engine.addSystem(() => {
  const info = EngineInfo.getOrNull(engine.RootEntity);
  if (info) {
    console.log(info.frameNumber, info.tickNumber, info.totalRuntime);
  }
});
访问帧级计时信息:
typescript
import { EngineInfo } from "@dcl/sdk/ecs";

engine.addSystem(() => {
  const info = EngineInfo.getOrNull(engine.RootEntity);
  if (info) {
    console.log(info.frameNumber, info.tickNumber, info.totalRuntime);
  }
});

System Execution Order & Priority

系统执行顺序与优先级

engine.addSystem(fn, priority?, name?)
runs
fn(dt)
every frame. The
priority
parameter controls when in the frame it runs relative to other systems.
HIGHER priority number = runs EARLIER in the frame. Systems are sorted descending by priority (
sort((a, b) => b.priority - a.priority)
in
@dcl/ecs
). The SDK's own JSDoc states: "a number with the priority, big number are called before smaller ones."
WARNING — counter-intuitive: This is the OPPOSITE of Unity/Godot/many engines where a lower number runs first. In Decentraland SDK7, "make this run first" means giving it a large priority number, NOT
1
. A system with priority
1
runs almost LAST.
Key numbers:
  • Default priority is
    100000
    (
    SYSTEMS_REGULAR_PRIORITY = 100e3
    ).
    engine.addSystem(fn)
    with no priority uses this.
  • @dcl/react-ecs
    registers its UI renderer system at
    100000
    (and a UI-scale system at
    100001
    ). So UI runs alongside/just before default-priority systems.
  • To run before all regular systems, pass a priority above
    100000
    (e.g.
    engine.addSystem(fn, 1000000)
    ). To run after them, pass a priority below
    100000
    (e.g.
    10
    , or the default
    0
    used by scripts).
typescript
engine.addSystem(earlySystem, 1000000);  // runs before regular systems
engine.addSystem(regularSystem);          // priority 100000 (default)
engine.addSystem(lateSystem, 10);         // runs after regular systems
  • Physics libraries (e.g. cannon.js) stepped inside a system's
    update(dt)
    obey the same rule — there is no separate physics loop. Give the physics-stepping system a high priority if other systems must read post-step state the same frame.
  • Systems with the same priority currently run in insertion order, but the engine does not guarantee a stable tie-break (noted as a TODO in the source). Do not rely on ordering between equal-priority systems.
engine.addSystem(fn, priority?, name?)
每帧运行
fn(dt)
。
priority
参数控制该系统在一帧中相对于其他系统何时运行。
优先级数字越高 = 在帧中运行得越早。 系统按优先级降序排列(
@dcl/ecs
中为
sort((a, b) => b.priority - a.priority)
)。SDK 自身的 JSDoc 写道:“一个表示优先级的数字,数值越大越先于较小值调用。”
警告——这违反直觉: 这与 Unity/Godot/许多引擎的做法相反,在那些引擎中,较小数字会先运行。在 Decentraland SDK7 中,“让该系统先运行”意味着赋予其较大的优先级数字,而不是
1
。优先级为
1
的系统几乎最后运行。
关键数值:
  • 默认优先级为
    100000
    (
    SYSTEMS_REGULAR_PRIORITY = 100e3
    )。调用
    engine.addSystem(fn)
    且不指定优先级时使用此值。
  • @dcl/react-ecs
    会将其 UI 渲染器系统注册为
    100000
    (UI 缩放系统为
    100001
    )。因此,UI 会与默认优先级系统同时或刚好在其前运行。
  • 要在所有常规系统之前运行,请传入高于
    100000
    的优先级(例如
    engine.addSystem(fn, 1000000)
    )。要在其之后运行,请传入低于
    100000
    的优先级(例如
    10
    ,或脚本使用的默认值
    0
    )。
typescript
engine.addSystem(earlySystem, 1000000);  // runs before regular systems
engine.addSystem(regularSystem);          // priority 100000 (default)
engine.addSystem(lateSystem, 10);         // runs after regular systems
  • 物理库(例如 cannon.js)在某个系统的
    update(dt)
    内执行步进时,同样遵循此规则——不存在独立的物理循环。如果其他系统必须在同一帧读取步进后的状态,请为物理步进系统设置高优先级。
  • 优先级相同的系统目前按插入顺序运行,但引擎不保证稳定的平局决胜顺序(源码中将其标记为 TODO)。不要依赖优先级相同的系统之间的执行顺序。

Restricted Actions

受限操作

These require prior player interaction (e.g. a click) before they can execute. Import from
~system/RestrictedActions
:
typescript
import {
  movePlayerTo,
  teleportTo,
  triggerEmote,
  changeRealm,
  openExternalUrl,
  openNftDialog,
  triggerSceneEmote,
  copyToClipboard,
  setCommunicationsAdapter,
} from "~system/RestrictedActions";

// Move player within scene bounds. Optional: cameraTarget (where the
// CAMERA looks), avatarTarget (where the AVATAR faces — for rotating in
// place), duration (seconds, for a smooth glide instead of a snap).
movePlayerTo({
  newRelativePosition: { x: 8, y: 0, z: 8 },
  cameraTarget: { x: 8, y: 1, z: 12 },
});

// Teleport to coordinates in Genesis City
teleportTo({ worldCoordinates: { x: 50, y: 70 } });

// Play a built-in emote
triggerEmote({ predefinedEmote: "wave" });

// Open URL in browser (prompts user)
openExternalUrl({ url: "https://decentraland.org" });

// Open NFT detail dialog
openNftDialog({
  urn: "urn:decentraland:ethereum:erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d:558536",
});

// Copy text to clipboard
copyToClipboard({ text: "Hello from Decentraland!" });

// Change realm. `message` is OPTIONAL: omit it to switch with no prompt,
// include it to show the player a confirmation dialog first.
changeRealm({ realm: "https://peer.decentraland.org" }); // no prompt
changeRealm({ realm: "other-realm.dcl.eth", message: "Join this realm?" });
这些操作要求玩家事先进行交互(例如点击)后才能执行。请从
~system/RestrictedActions
导入:
typescript
import {
  movePlayerTo,
  teleportTo,
  triggerEmote,
  changeRealm,
  openExternalUrl,
  openNftDialog,
  triggerSceneEmote,
  copyToClipboard,
  setCommunicationsAdapter,
} from "~system/RestrictedActions";

// Move player within scene bounds. Optional: cameraTarget (where the
// CAMERA looks), avatarTarget (where the AVATAR faces — for rotating in
// place), duration (seconds, for a smooth glide instead of a snap).
movePlayerTo({
  newRelativePosition: { x: 8, y: 0, z: 8 },
  cameraTarget: { x: 8, y: 1, z: 12 },
});

// Teleport to coordinates in Genesis City
teleportTo({ worldCoordinates: { x: 50, y: 70 } });

// Play a built-in emote
triggerEmote({ predefinedEmote: "wave" });

// Open URL in browser (prompts user)
openExternalUrl({ url: "https://decentraland.org" });

// Open NFT detail dialog
openNftDialog({
  urn: "urn:decentraland:ethereum:erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d:558536",
});

// Copy text to clipboard
copyToClipboard({ text: "Hello from Decentraland!" });

// Change realm. `message` is OPTIONAL: omit it to switch with no prompt,
// include it to show the player a confirmation dialog first.
changeRealm({ realm: "https://peer.decentraland.org" }); // no prompt
changeRealm({ realm: "other-realm.dcl.eth", message: "Join this realm?" });

Timers

定时器

Always use the engine-bound
timers
object from
@dcl/sdk/ecs
.
Do NOT use the native JS
setTimeout
/
setInterval
globals. Although the QuickJS runtime exposes JS-standard
setTimeout
/
clearTimeout
/
setInterval
/
clearInterval
as globals (declared in
@dcl/js-runtime/index.d.ts
), calling them in a Decentraland scene may appear to work but can introduce subtle problems — they are not bound to the scene's engine. Use
timers.setTimeout
instead.
typescript
import { timers } from "@dcl/sdk/ecs";

const timeoutId = timers.setTimeout(() => console.log("delayed"), 2000);
timers.clearTimeout(timeoutId);

const intervalId = timers.setInterval(() => console.log("tick"), 1000);
timers.clearInterval(intervalId);
The signatures match the JS-standard timers:
ts
timers.setTimeout(callback: () => void, ms: number): number
timers.clearTimeout(timerId: number): void
timers.setInterval(callback: () => void, ms: number): number
timers.clearInterval(timerId: number): void
Argument order is
(callback, ms)
— not
(ms, callback)
. Do NOT write a custom helper that flips them.
Do NOT write a custom per-frame timer system that accumulates
dt
to fire delayed callbacks. The SDK already ships
timers
. Custom systems duplicate work, drift from the engine's own scheduling, and are the wrong abstraction for one-shot delays.
For a custom engine instance, use
createTimers(engineInstance)
from
@dcl/sdk/ecs
to get a
Timers
object scoped to that engine.
System-based timers (recommended for game logic — synchronized with the frame loop):
typescript
let elapsed = 0;
engine.addSystem((dt: number) => {
  elapsed += dt;
  if (elapsed >= 3) {
    elapsed = 0;
    // Do something every 3 seconds
  }
});
始终使用从
@dcl/sdk/ecs
获取的、绑定至 engine 的
timers
对象。不要使用原生 JS
setTimeout
/
setInterval
全局函数。尽管 QuickJS 运行时将符合 JS 标准的
setTimeout
/
clearTimeout
/
setInterval
/
clearInterval
暴露为全局函数(
@dcl/js-runtime/index.d.ts
中有声明),但在 Decentraland 场景中调用它们可能看似正常,却可能引入微妙问题——它们没有绑定到场景的 engine。请改用
timers.setTimeout
。
typescript
import { timers } from "@dcl/sdk/ecs";

const timeoutId = timers.setTimeout(() => console.log("delayed"), 2000);
timers.clearTimeout(timeoutId);

const intervalId = timers.setInterval(() => console.log("tick"), 1000);
timers.clearInterval(intervalId);
其签名与符合 JS 标准的定时器一致:
ts
timers.setTimeout(callback: () => void, ms: number): number
timers.clearTimeout(timerId: number): void
timers.setInterval(callback: () => void, ms: number): number
timers.clearInterval(timerId: number): void
参数顺序为
(callback, ms)
——不是
(ms, callback)
。不要编写会颠倒它们的自定义辅助函数。
不要编写自定义的逐帧定时器系统来累积
dt
以触发延迟回调。SDK 已经提供了
timers
。自定义系统会重复执行工作、与 engine 自身的调度产生偏差,而且对于一次性延迟而言是错误的抽象。
对于自定义 engine 实例,请使用
createTimers(engineInstance)
(来自
@dcl/sdk/ecs
)获取作用域限定到该 engine 的
Timers
对象。
基于系统的定时器(推荐用于游戏逻辑——与帧循环同步):
typescript
let elapsed = 0;
engine.addSystem((dt: number) => {
  elapsed += dt;
  if (elapsed >= 3) {
    elapsed = 0;
    // Do something every 3 seconds
  }
});

Component.onChange() Listener

Component.onChange() 监听器

React to component changes on any entity:
typescript
Transform.onChange(engine.PlayerEntity, (newValue) => {
  if (newValue) {
    console.log("Player moved to", newValue.position);
  }
});
响应任意实体上的组件变化:
typescript
Transform.onChange(engine.PlayerEntity, (newValue) => {
  if (newValue) {
    console.log("Player moved to", newValue.position);
  }
});

Utility: removeEntityWithChildren

工具函数:removeEntityWithChildren

Recursively remove an entity and all its children — reach for this when cleaning up complex entity hierarchies:
typescript
import { removeEntityWithChildren } from "@dcl/sdk/ecs";

removeEntityWithChildren(engine, parentEntity);
递归移除一个实体及其所有子实体——清理复杂实体层级时应使用此工具:
typescript
import { removeEntityWithChildren } from "@dcl/sdk/ecs";

removeEntityWithChildren(engine, parentEntity);

Portable Experiences

可移植体验

Scenes that persist across world navigation. Import from
~system/PortableExperiences
.
typescript
import {
  spawn,
  kill,
  exit,
  getPortableExperiencesLoaded,
} from "~system/PortableExperiences";

// Spawn by ENS name (a deployed World) OR by pid. NOT by "urn".
const result = await spawn({ ens: "boedo.dcl.eth" });
// result: { pid, parentCid, name, ens }

// Kill a running one by its pid (from the spawn response). NOT by urn.
if (result.pid) await kill({ pid: result.pid });

// List currently loaded portable experiences
const { loaded } = await getPortableExperiencesLoaded({});

// Exit self (only if THIS scene IS a portable experience)
await exit({});
  • spawn({ ens?, pid? })
    →
    SpawnResponse { pid, parentCid, name, ens? }
    . Field is
    ens
    /
    pid
    , not
    urn
    .
  • kill({ pid })
    returns
    { status: boolean }
    ;
    kill({ pid })
    and
    getPortableExperiencesLoaded({})
    both key off
    pid
    , never
    urn
    .
  • The host scene must enable them in
    scene.json
    :
    "featureToggles": { "portableExperiences": "enabled" }
    . Values:
    "enabled"
    |
    "disabled"
    |
    "hideUi"
    (spawns PX but hides their UI). With
    "disabled"
    ,
    spawn()
    is a no-op / rejected.
跨世界导航仍会保留的场景。请从
~system/PortableExperiences
导入。
typescript
import {
  spawn,
  kill,
  exit,
  getPortableExperiencesLoaded,
} from "~system/PortableExperiences";

// Spawn by ENS name (a deployed World) OR by pid. NOT by "urn".
const result = await spawn({ ens: "boedo.dcl.eth" });
// result: { pid, parentCid, name, ens }
// Kill a running one by its pid (from the spawn response). NOT by urn.
if (result.pid) await kill({ pid: result.pid });

// List currently loaded portable experiences
const { loaded } = await getPortableExperiencesLoaded({});

// Exit self (only if THIS scene IS a portable experience)
await exit({});
  • spawn({ ens?, pid? })
    →
    SpawnResponse { pid, parentCid, name, ens? }
    。字段是
    ens
    /
    pid
    ,不是
    urn
    。
  • kill({ pid })
    返回
    { status: boolean }
    ;
    kill({ pid })
    和
    getPortableExperiencesLoaded({})
    都以
    pid
    为键,绝不会使用
    urn
    。
  • 宿主场景必须在
    scene.json
    中启用它们:
    "featureToggles": { "portableExperiences": "enabled" }
    。可选值为
    "enabled"
    |
    "disabled"
    |
    "hideUi"
    (生成 PX 但隐藏其 UI)。使用
    "disabled"
    时,
    spawn()
    不会执行任何操作/会被拒绝。

Testing Framework

测试框架

Scenes can ship unit tests using
@dcl/sdk/testing
. Tests are generators — yielding pauses until the next frame so you can observe engine state across ticks.
typescript
import { test } from "@dcl/sdk/testing";
import {
  assertComponentValue,
  assertEquals,
} from "@dcl/sdk/testing/assert";
import { engine, Transform, MeshRenderer } from "@dcl/sdk/ecs";
import { Vector3, Quaternion } from "@dcl/sdk/math";

test("transform is applied after one frame", function* () {
  const entity = engine.addEntity();
  Transform.create(entity, { position: Vector3.One() });

  // Let the engine run for a frame before asserting
  yield;

  assertComponentValue(entity, Transform, {
    position: Vector3.One(),
    scale: Vector3.One(),
    rotation: Quaternion.Identity(),
    parent: 0 as any,
  });
});

test("five meshes are present", function* () {
  yield;
  assertEquals(1 + 1, 2, "basic math");
  // No count assertion exists — count via getEntitiesWith + Array.from
  assertEquals(
    Array.from(engine.getEntitiesWith(MeshRenderer)).length,
    5,
    "should have 5 meshes"
  );
});
Available assertions (
@dcl/sdk/testing/assert
) — exactly these four:
  • assertEquals(actual, expected, message?)
    — deep-equals check
  • assert(condition, message?)
    — truthiness check
  • assertComponentValue(entity, Component, expected)
    — full component value comparison
  • deepCloseTo(actual, expected, options?)
    — deep numeric comparison with tolerance (for floats)
There is NO count assertion — count entities with
assertEquals(Array.from(engine.getEntitiesWith(Comp)).length, n)
.
Running tests: there is no CLI test command (
npx @dcl/sdk-commands test
does not exist). Tests execute only when the hosting runtime exposes the
~system/Testing
module — CI test runners or test-enabled explorers. In a normal preview the test runner is a no-op that just logs, and it's guarded behind DEBUG in production builds. Tests run inside the same QuickJS runtime as the scene, so the same restrictions apply (no Node.js APIs, use SDK timers, etc.).
场景可以随包提供使用
@dcl/sdk/testing
编写的单元测试。测试以生成器形式实现——通过
yield
暂停至下一帧,以便跨 tick 观察 engine 状态。
typescript
import { test } from "@dcl/sdk/testing";
import {
  assertComponentValue,
  assertEquals,
} from "@dcl/sdk/testing/assert";
import { engine, Transform, MeshRenderer } from "@dcl/sdk/ecs";
import { Vector3, Quaternion } from "@dcl/sdk/math";

test("transform is applied after one frame", function* () {
  const entity = engine.addEntity();
  Transform.create(entity, { position: Vector3.One() });

  // Let the engine run for a frame before asserting
  yield;

  assertComponentValue(entity, Transform, {
    position: Vector3.One(),
    scale: Vector3.One(),
    rotation: Quaternion.Identity(),
    parent: 0 as any,
  });
});

test("five meshes are present", function* () {
  yield;
  assertEquals(1 + 1, 2, "basic math");
  // No count assertion exists — count via getEntitiesWith + Array.from
  assertEquals(
    Array.from(engine.getEntitiesWith(MeshRenderer)).length,
    5,
    "should have 5 meshes"
  );
});
可用断言(
@dcl/sdk/testing/assert
)——仅限以下四种:
  • assertEquals(actual, expected, message?)
    — 深度相等检查
  • assert(condition, message?)
    — 真值检查
  • assertComponentValue(entity, Component, expected)
    — 完整组件值比较
  • deepCloseTo(actual, expected, options?)
    — 带容差的深度数值比较(用于浮点数)
不存在数量断言——使用
assertEquals(Array.from(engine.getEntitiesWith(Comp)).length, n)
统计实体。
运行测试:没有 CLI 测试命令(
npx @dcl/sdk-commands test
并不存在)。测试仅在宿主运行时提供
~system/Testing
模块时执行——即 CI 测试运行器或启用测试的 explorer。在普通预览中,测试运行器只是一个仅记录日志的空操作,并且在生产构建中受 DEBUG 开关控制。测试在与场景相同的 QuickJS 运行时中执行,因此同样受到限制(无 Node.js API,请使用 SDK 定时器等)。

Logging

日志

Only
console.log()
and
console.error()
are declared in the runtime —
console.warn()
,
.info()
,
.debug()
,
.trace()
are NOT available.
运行时中只声明了
console.log()
和
console.error()
——
console.warn()
、
.info()
、
.debug()
、
.trace()
均不可用。

Example scenes

示例场景

Engine-team test scenes exercising these APIs against the real runtime:
Full RestrictedActions reference —
triggerSceneEmote
(
_emote.glb
requirement),
setCommunicationsAdapter
,
movePlayerTo
rotate-in-place, predefined emote names — plus extra
executeTask
variants (error handling, sequential):
{baseDir}/references/runtime-apis.md
.
引擎团队用于在真实运行时中测试这些 API 的测试场景:
完整的 RestrictedActions 参考——包括
triggerSceneEmote
(
_emote.glb
要求)、
setCommunicationsAdapter
、
movePlayerTo
原地旋转、预定义 emote 名称——以及额外的
executeTask
变体(错误处理、顺序执行):
{baseDir}/references/runtime-apis.md
。