关于 scene-runtime
面向 Decentraland SDK7 场景开发者的运行时 API 参考,整理异步任务、网络、场景信息、计时器及测试等跨场景能力。
主要功能
- 01封装异步任务
- 02使用 HTTP 与 WebSocket
- 03读取场景、Realm 与世界时间
- 04读取已部署文件
- 05使用运行时计时器与变更监听
- 06运行场景自动化测试
适用场景
- 01在场景中请求公共或身份签名 API
- 02按世界时间或帧循环驱动逻辑
- 03读取随场景部署的配置文件
- 04为 SDK7 场景编写自动化测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill scene-runtime安装后在 Claude Code 中输入/scene-runtime确认已加载。
附带文件references/runtime-apis.md
详细介绍
场景运行时 API
Decentraland SDK7 场景中均可使用的跨领域运行时 API。
异步任务
场景运行时是单线程的。请将所有异步工作包装在 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
普通 fetch 适用于公共 API:
const res = await fetch('https://api.example.com/data')
signedFetch 可向你的后端证明玩家的身份。使用 getHeaders() 仅获取已签名的标头(适用于由库管理自身 fetch 的情况):
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' })
权限:外部 HTTP 需要
"ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE",普通 fetch 无需特殊权限;signedFetch则需要玩家与场景交互过。
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')
场景与 Realm 信息
import { getSceneInformation, getRealm } from '~system/Runtime'
import { getExplorerInformation } from '~system/EnvironmentApi'
executeTask(async () => {
// Scene info: URN, content mappings, metadata JSON, baseUrl
const scene = await getSceneInformation({})
const metadata = JSON.parse(scene.metadataJson)
console.log(scene.urn, scene.baseUrl, metadata)
// Realm info: baseUrl, realmName, isPreview, networkId, commsAdapter
const realm = await getRealm({})
console.log(realm.realmInfo?.realmName, realm.realmInfo?.isPreview)
// Explorer info: agent string, platform, configurations
const explorer = await getExplorerInformation({})
console.log(explorer.agent, explorer.platform)
})
世界时间
import { getWorldTime } from '~system/Runtime'
executeTask(async () => {
const { seconds } = await getWorldTime({})
// seconds = coordinated world time (cycles 0-86400 for day/night)
})
读取已部署的文件
在运行时读取随场景部署的文件:
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 组件
访问帧级计时信息:
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/RestrictedActions 导入:
import {
movePlayerTo,
teleportTo,
triggerEmote,
changeRealm,
openExternalUrl,
openNftDialog,
triggerSceneEmote,
copyToClipboard,
setCommunicationsAdapter
} from '~system/RestrictedActions'
// Move player within scene bounds
movePlayerTo({ newRelativePosition: { x: 8, y: 0, z: 8 } })
// 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({ value: 'Hello from Decentraland!' })
// Change realm
changeRealm({ realm: 'other-realm.dcl.eth', message: 'Join this realm?' })
计时器
请使用 @dcl/sdk/ecs 中的 timers API,不要使用全局 setTimeout/setInterval。这些全局函数在 QuickJS 运行时中不可靠。
import { timers } from '@dcl/sdk/ecs'
const t = timers.setTimeout(() => console.log('delayed'), 2000)
timers.clearTimeout(t)
const id = timers.setInterval(() => console.log('tick'), 1000)
timers.clearInterval(id)
基于系统的计时器(推荐用于游戏逻辑,可与帧循环同步):
let elapsed = 0
engine.addSystem((dt: number) => {
elapsed += dt
if (elapsed >= 3) {
elapsed = 0
// Do something every 3 seconds
}
})
Component.onChange() 监听器
响应任意实体上的组件更改:
Transform.onChange(engine.PlayerEntity, (newValue) => {
if (newValue) {
console.log('Player moved to', newValue.position)
}
})
实用工具:removeEntityWithChildren
递归移除一个实体及其所有子实体:
import { removeEntityWithChildren } from '@dcl/sdk/ecs'
removeEntityWithChildren(engine, parentEntity)
便携式体验
可在世界导航期间持续运行的场景:
import { spawn, kill, exit, getPortableExperiencesLoaded } from '~system/PortableExperiences'
// Spawn a portable experience by URN
const result = await spawn({ urn: 'urn:decentraland:entity:bafk...' })
// List currently loaded portable experiences
const loaded = await getPortableExperiencesLoaded({})
// Kill a specific portable experience
await kill({ urn: 'urn:decentraland:entity:bafk...' })
// Exit self (if this scene IS a portable experience)
await exit({})
测试框架
SDK7 包含用于自动化场景测试的测试框架:
import { test, assert, assertEquals, assertComponentValue } from '@dcl/sdk/testing'
import { setCameraTransform } from '@dcl/sdk/testing'
test('cube is at correct position', async (context) => {
// Set up camera for the test
setCameraTransform({ position: { x: 8, y: 1, z: 8 } })
// Wait for systems to run
await context.helpers.waitNTicks(2)
// Assert component values
assertComponentValue(cubeEntity, Transform, {
position: Vector3.create(8, 1, 8)
})
// Basic assertions
assert(Transform.has(cubeEntity), 'Entity should have Transform')
assertEquals(1 + 1, 2)
})
运行测试:
npx @dcl/sdk-commands test
最佳实践
- 始终将异步代码包装在
executeTask()中——裸 Promise 会被静默丢弃 - 后端需要验证玩家身份时,请使用
signedFetch(而非普通fetch) - 游戏逻辑优先使用基于系统的计时器,而不是
timers.setTimeout/timers.setInterval——它们可与帧循环同步。单次定时操作(自动关门、延迟播放声音等)请使用timers.*。 - 检查
realm.realmInfo?.isPreview以检测预览模式并启用调试功能 - 对于与场景一同部署的数据文件(JSON 配置、关卡数据),请使用
readFile() - 清理复杂的实体层级时,
removeEntityWithChildren()至关重要
如需了解完整的 executeTask 用法模式、所有 RestrictedActions、Realm 检测和便携式体验,请参阅 {baseDir}/references/runtime-apis.md。