| name | scene-runtime |
| description | Scene runtime APIs — async tasks, HTTP fetch, WebSockets, realm/scene info, timers, restricted actions, portable experiences, testing framework |
Scene Runtime APIs
Cross-cutting runtime APIs available in every Decentraland SDK7 scene.
Async Tasks
The scene runtime is single-threaded. Wrap any async work in executeTask():
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
Plain fetch works for public APIs:
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):
import { signedFetch, getHeaders } from '~system/SignedFetch'
const res = await signedFetch({ url: 'https://your-server.com/api', init: { method: 'POST', body: JSON.stringify(payload) } })
const { headers } = await getHeaders({ url: 'https://your-server.com/api' })
Permission: External HTTP requires "ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE" or no special permission for plain fetch; signedFetch needs the player to have interacted with the scene.
WebSocket
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
import { getSceneInformation, getRealm } from '~system/Runtime'
import { getExplorerInformation } from '~system/EnvironmentApi'
executeTask(async () => {
const scene = await getSceneInformation({})
const metadata = JSON.parse(scene.metadataJson)
console.log(scene.urn, scene.baseUrl, metadata)
const realm = await getRealm({})
console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview)
const explorer = await getExplorerInformation({})
console.log(explorer.agent, explorer.platform)
})
World Time
import { getWorldTime } from '~system/EnvironmentApi'
executeTask(async () => {
const { seconds } = await getWorldTime({})
})
Read Deployed Files
Read files deployed with the scene at runtime:
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
Access frame-level timing:
import { EngineInfo } from '@dcl/sdk/ecs'
engine.addSystem(() => {
const info = EngineInfo.getOrNull(engine.RootEntity)
if (info) {
console.log(info.frameNumber, info.tickNumber, info.totalRuntime)
}
})
Restricted Actions
These require player interaction before they can execute. Import from ~system/RestrictedActions:
import {
movePlayerTo,
teleportTo,
triggerEmote,
changeRealm,
openExternalUrl,
openNftDialog,
triggerSceneEmote,
copyToClipboard,
setCommunicationsAdapter
} from '~system/RestrictedActions'
movePlayerTo({ newRelativePosition: { x: 8, y: 0, z: 8 } })
teleportTo({ worldCoordinates: { x: 50, y: 70 } })
triggerEmote({ predefinedEmote: 'wave' })
openExternalUrl({ url: 'https://decentraland.org' })
openNftDialog({ urn: 'urn:decentraland:ethereum:erc721:0x06012c8cf97BEaD5deAe237070F9587f8E7A266d:558536' })
copyToClipboard({ value: 'Hello from Decentraland!' })
changeRealm({ realm: 'other-realm.dcl.eth', message: 'Join this realm?' })
Timers
setTimeout / setInterval are supported via the QuickJS runtime polyfill:
setTimeout(() => console.log('delayed'), 2000)
const id = setInterval(() => console.log('tick'), 1000)
clearInterval(id)
System-based timers (recommended for game logic — synchronized with the frame loop):
let elapsed = 0
engine.addSystem((dt: number) => {
elapsed += dt
if (elapsed >= 3) {
elapsed = 0
}
})
Component.onChange() Listener
React to component changes on any entity:
Transform.onChange(engine.PlayerEntity, (newValue) => {
if (newValue) {
console.log('Player moved to', newValue.position)
}
})
Utility: removeEntityWithChildren
Recursively remove an entity and all its children:
import { removeEntityWithChildren } from '@dcl/sdk/ecs'
removeEntityWithChildren(engine, parentEntity)
Portable Experiences
Scenes that persist across world navigation:
import { spawn, kill, exit, getPortableExperiencesLoaded } from '~system/PortableExperiences'
const result = await spawn({ urn: 'urn:decentraland:entity:bafk...' })
const loaded = await getPortableExperiencesLoaded({})
await kill({ urn: 'urn:decentraland:entity:bafk...' })
await exit({})
Testing Framework
SDK7 includes a testing framework for automated scene tests:
import { test, assert, assertEquals, assertComponentValue } from '@dcl/sdk/testing'
import { setCameraTransform } from '@dcl/sdk/testing'
test('cube is at correct position', async (context) => {
setCameraTransform({ position: { x: 8, y: 1, z: 8 } })
await context.helpers.waitNTicks(2)
assertComponentValue(cubeEntity, Transform, {
position: Vector3.create(8, 1, 8)
})
assert(Transform.has(cubeEntity), 'Entity should have Transform')
assertEquals(1 + 1, 2)
})
Run tests with:
npx @dcl/sdk-commands test
Best Practices
- Always wrap async code in
executeTask() — bare promises will be silently dropped
- Use
signedFetch (not plain fetch) when your backend needs to verify the player's identity
- Prefer system-based timers over
setTimeout/setInterval for game logic — they stay in sync with the frame loop
- Check
realm.realmInfo?.isPreview to detect preview mode and enable debug features
- Use
readFile() for data files (JSON configs, level data) deployed alongside the scene
removeEntityWithChildren() is essential when cleaning up complex entity hierarchies