scene-runtime について
Decentraland SDK7 のシーン開発者向けに、ランタイム API を解説するガイドです。非同期タスクやネットワークリクエスト、シーン情報、エンジンシステムを扱い、制限付きアクションや実行順序、シーンの表示状態に関する注意点もまとめています。
主な機能
- 01非同期タスクと HTTP リクエストの実行
- 02WebSocket による通信
- 03シーン、Realm、ワールド時刻の取得
- 04デプロイ済みファイルとエンジン状態の読み取り
- 05システム実行優先度の設定
活用シーン
- 01外部 API からのデータ取得
- 02シーン表示時に演出を開始
- 03システム更新順序の制御
- 04プレイヤー操作が必要なアクションの呼び出し
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ decentraland/ sdk-skills --skill scene-runtimeインストール後、Claude Code で/scene-runtimeを入力して読み込まれたか確認します。
同梱ファイルreferences/runtime-apis.md
詳細
シーンランタイム API
すべての Decentraland SDK7 シーンで利用できる、シーン横断型のランタイム API です。
非同期タスク
シーンのランタイムはシングルスレッドです。非同期処理はすべて executeTask()(または非同期関数)でラップしてください。ラップされていない Promise は黙って破棄されます。
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" });
signedFetch は { ok, status, statusText, headers, body } を返します。body は文字列です(JSON.parse(response.body) を自分で呼び出してください。.json() はありません)。署名済みの ID ヘッダーは自動的に追加されます。バックエンドでは ADR-44 に従って検証します。
権限:外部 HTTP に対する名目上の権限は
"USE_FETCH"です(ソケットの場合は"USE_WEBSOCKET")。これらはscene.jsonのrequiredPermissionsで宣言します。実際には、プレビューや Worlds では、権限がなくてもメディア以外のホストへの通常のfetch/signedFetchがハードブロックされることはありません(66,6-signed-fetchテストシーンはrequiredPermissionsが空のままsignedFetchを呼び出します)。正確性と将来の互換性のため、USE_FETCHは宣言してください。signedFetchにはプレイヤーの事前操作は必要ありません。制限付きアクションには必要ですが、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, 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 を確認してください。
ワールド時刻
import { getWorldTime } from "~system/Runtime";
executeTask(async () => {
const { seconds } = await getWorldTime({});
// seconds = coordinated world time (cycles 0-86400 for day/night)
});
デプロイ済みファイルの読み込み
シーンとともにデプロイされたファイルを、実行時に読み込みます。JSON 設定やレベルデータなどのデータファイルに使用してください。
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 コンポーネント
engine.RootEntity 上で利用できる、フレーム単位のタイミングとシーンの表示状態です。
import { EngineInfo, engine } from "@dcl/sdk/ecs";
engine.addSystem(() => {
const info = EngineInfo.getOrNull(engine.RootEntity);
if (info) {
console.log(info.frameNumber, info.tickNumber, info.totalRuntime);
// info.sceneHidden: true when a fullscreen Explorer UI (map, backpack,
// settings, loading screen, etc.) covers the scene viewport.
if (info.sceneHidden) {
// Pause expensive work, audio, animations — the player cannot see or
// interact with the scene while it is hidden.
}
}
});
EngineInfo のフィールド
| フィールド | 型 | 説明 |
|---|---|---|
frameNumber | number | エンジンのフレームカウンター |
tickNumber | number | シーンのティックカウンター(ADR-148 に準拠) |
totalRuntime | number | このシーンの合計実行時間(秒) |
sceneHidden | boolean | シーンが全画面の Explorer UI(マップ、バックパック、設定、カメラリール、ローディング画面)の背後に隠れている場合は true です。frameNumber/tickNumber と同じく「physics」ステージで書き込まれます。シーンが表示されていない間、ゲームプレイ、オーディオ、アニメーション、負荷の高いシステムを一時停止するために使用します。デフォルトは false です。 |
プロトコルのコミット 0b3d285(PBEngineInfo のフィールド 4、bool scene_hidden、コンポーネント ID 1048)と js-sdk-toolchain のコミット ffb26183(PBEngineInfo 上で sceneHidden: boolean として公開)に照らして検証済みです。
ローディング画面がフェードアウトしたタイミングの検出
Explorer のローディング画面がシーンを覆っている間、sceneHidden は true で、画面がフェードアウトした瞬間に false に切り替わります。プレイヤーが実際にシーンを目にする最初の瞬間を、シーンが検知できる唯一の合図です。totalRuntime、フレームカウンター、または utils.timers の遅延に基づく onSceneReady 風のタイミングは推測にすぎません。ローディング画面の表示時間は状況によって異なり、プレイヤーやマシンごとに変わります。
プレイヤーに見せる演出を始めるタイミングの制御に使います。たとえば、イントロのシネマティック、ウェルカムサウンド/ボイスオーバー、オープニングのトゥイーン、タイトル UI、またはプレイヤーがローディング画面を見ている間に発火させるべきではない分析イベントなどです。
import { EngineInfo, engine } from "@dcl/sdk/ecs";
engine.addSystem(function waitForSceneRevealed() {
const info = EngineInfo.getOrNull(engine.RootEntity);
if (!info || info.sceneHidden) return;
engine.removeSystem(waitForSceneRevealed); // one-shot
// the player is now looking at the scene — start the intro here
});
sceneHiddenがtrueの間もシーンのティックは通常どおり進みます。一時停止しているのではなく、表示されていないだけです。このフラグでシーンのロジックを制御せず、演出のタイミング調整のみに使用してください。- 必ず
getOrNullでガードしてください。最初の数フレームでは、コンポーネントが存在しない場合があります。 - 発火後はシステムを削除するか、
doneフラグを設定してください。そうしないと、毎フレーム再実行されます。 - このフィールドより前の Explorer ビルドでは、
sceneHiddenは proto のデフォルトであるfalseのままです。そのため、このパターンは停止するのではなく、「可能な限り早く発火する」動作になります。
システムの実行順序と優先度
engine.addSystem(fn, priority?, name?) は毎フレーム fn(dt) を実行します。priority パラメーターは、ほかのシステムに対してフレーム内のいつ実行するかを制御します。
優先度の数値が大きいほど、フレーム内で早く実行されます。 システムは優先度の降順で並べ替えられます(@dcl/ecs の sort((a, b) => b.priority - a.priority))。SDK 自身の JSDoc には、次のように記載されています。「優先度を表す数値で、大きな数値のものが小さな数値のものより先に呼び出されます。」
WARNING — 直感に反します: これは、優先度の数値が小さいほど先に実行される 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)。
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 に記載されています)。優先度が同じシステム同士の実行順序に依存しないでください。
制限付きアクション
これらのアクションを実行するには、事前にプレイヤーの操作(クリックなど)が必要です。~system/RestrictedActions からインポートしてください:
import {
movePlayerTo,
teleportTo,
triggerEmote,
changeRealm,
openExternalUrl,
openNftDialog,
triggerSceneEmote,
copyToClipboard,
setCommunicationsAdapter,
openExplorerUi,
} 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 } });
// Teleport into another realm / World. `realm` takes a World name
// (`foo.dcl.eth`) or a realm URL; the client does a FULL RECONNECT to
// that realm, then lands on the parcel. Requires @dcl/sdk 7.28.0+.
// The player is asked to confirm first ("Are you sure you want to enter
// this World?"); the switch only happens if they accept.
teleportTo({ realm: "foo.dcl.eth", worldCoordinates: { x: 0, y: 0 } });
// Omit worldCoordinates to land on that realm's default spawn.
// This replaces changeRealm.
teleportTo({ realm: "foo.dcl.eth" });
// 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!" });
// [DEPRECATED] changeRealm — use teleportTo({ realm }) instead.
// The player always gets a confirmation dialog. `message` is OPTIONAL:
// omit it to show the default text, include it to show custom text.
changeRealm({ realm: "https://peer.decentraland.org" }); // default prompt text
changeRealm({ realm: "other-realm.dcl.eth", message: "Join this realm?" }); // custom prompt text
changeRealm は [DEPRECATED] です — teleportTo({ realm }) を優先してください
プロトコル e89d7fa、SDK ピン 6b7c3586。realm フィールドは @dcl/js-runtime 7.28.0 で型定義されています。
TeleportToRequestは現在{ worldCoordinates?: Vector2, realm?: string }です — どちらも省略可能です。realmには World 名(foo.dcl.eth)またはレルム URL を指定できます。設定すると、クライアントは最初にデフォルトの文言(「この World に入りますか?」)で確認プロンプトをプレイヤーに表示します。プレイヤーが承諾すると、クライアントはそのレルムに完全に再接続し(すでにプレイヤーがいるレルムの場合でも)、パーセルに移動します。realmを設定し、worldCoordinatesを省略すると、そのレルムのデフォルトのスポーン地点に移動します。これはchangeRealmとまったく同じ動作です。realmを省略すると、プレイヤーが現在いるレルム内でテレポートします(従来の動作です)。
changeRealm に代わる理由: changeRealm は、新しいレルムが有効になったときではなく、リクエストが受け付けられたときに解決されます。そのため、changeRealm(...) のあとに teleportTo(coords) を呼び出すと、古いレルム内のパーセルに移動してしまいます。teleportTo({ realm, worldCoordinates }) を 1 回呼び出せば、この競合は起きません。
changeRealm は引き続き動作し、既存のシーン向けに維持されています。changeRealm と teleportTo({ realm }) はどちらも、レルムを切り替える前にプレイヤーに確認を求めます。シーン側で message を使って確認プロンプトの文言をカスタマイズできるのは、changeRealm だけです。
Unity エクスプローラーは realm フィールドに対応しています。
openExplorerUi -- エクスプローラーパネルを開く
シーンのコードから全画面のエクスプローラーパネル(マップ、バックパック、設定など)を開きます。事前にプレイヤーの操作が必要です。
import { openExplorerUi } from "~system/RestrictedActions";
import { ExplorerUi } from "@dcl/sdk/ecs";
// Open the map panel
openExplorerUi({ ui: ExplorerUi.EU_MAP });
呼び出しは{ openResult }に解決されます。これはOpenExplorerUiResult(~system/RestrictedActionsからも利用可能)で、OPENED、WAS_ALREADY_OPEN(そのパネルが使用するスロットはすでに埋まっています。パネルはスロットを共有している場合は互いに排他的なため、別のパネルまたはセクションが表示されている可能性があります)、REJECTED_NOT_CURRENT_SCENE(プレイヤーがこのシーンにいません)、REJECTED_FEATURE_DISABLED(クライアントがそのパネルを非表示にしているか、サポートしていません)、REJECTED_NO_USER_GESTURE(プレイヤーの操作から呼び出されていません)、およびデフォルトのUNSPECIFIEDが返されます。入力コールバックから呼び出してください。トップレベルで呼び出すと、常にREJECTED_NO_USER_GESTUREが返されます。成功として扱うのはOPENEDだけにしてください。それ以外の場合は別の方法で情報を表示するか、プレイヤーに何を開けばよいか伝えてください。
import { openExplorerUi, OpenExplorerUiResult } from "~system/RestrictedActions";
import { ExplorerUi, InputAction, pointerEventsSystem } from "@dcl/sdk/ecs";
pointerEventsSystem.onPointerDown(
{ entity: placesSign, opts: { button: InputAction.IA_POINTER, hoverText: "Places" } },
async () => {
const { openResult } = await openExplorerUi({ ui: ExplorerUi.EU_PLACES });
if (openResult !== OpenExplorerUiResult.OPENED) {
showHint("Open Places from the menu to find more scenes"); // your own UI fallback
}
},
);
ExplorerUi列挙型(@dcl/sdk/ecsからインポート)は、EU_SETTINGS(0)、EU_MAP(1)、EU_BACKPACK(2)、EU_CAMERA_REEL(3)、EU_COMMUNITIES(4)、EU_PLACES(5)、EU_EVENTS(6)です。この列挙型はopenExplorerUiとExplorerUiEventsResultで共有されるため、比較時にキャストは不要です。
ExplorerUiEventsResult -- パネルの開閉を監視する
シーンでは、エクスプローラーパネルが開いたときや閉じたときにそのイベントを監視できます。ExplorerUiEventsResultはengine.RootEntity上の追記専用の値セット(APPENDセマンティクス、最大100エントリー)です。各エントリーには、どのパネルが開いたか、または閉じたかが記録されます。
import { engine, ExplorerUiEventsResult, ExplorerUi } from "@dcl/sdk/ecs";
engine.addSystem(() => {
const results = ExplorerUiEventsResult.get(engine.RootEntity);
for (const entry of results.values()) {
if (entry.ui === ExplorerUi.EU_MAP) {
if (entry.event?.$case === "opened") {
console.log("Map was opened");
} else if (entry.event?.$case === "closed") {
console.log("Map was closed");
}
}
}
});
各エントリーには、ui(ExplorerUi列挙型)、timestamp(シーンのティック)、event(oneof:{ $case: 'opened' }または{ $case: 'closed' })が含まれます。コンポーネントIDは1220です。プロトコルのコミット86c4613とjs-sdk-toolchainのコミットfc8cfc65を参照して確認済みです。
タイマー
必ず@dcl/sdk/ecsのエンジンにバインドされたtimersオブジェクトを使用してください。 ネイティブJSのsetTimeout/setIntervalグローバルは使用しないでください。QuickJSランタイムではJS標準のsetTimeout/clearTimeout/setInterval/clearIntervalがグローバルとして公開されています(@dcl/js-runtime/index.d.tsで宣言されています)が、Decentralandのシーンで呼び出すと動作しているように見えても、微妙な問題が生じることがあります。これらはシーンのエンジンにバインドされていません。代わりにtimers.setTimeoutを使用してください。
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標準のタイマーと同じです。
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)ではありません。この順序を逆にするカスタムヘルパーは作成しないでください。
タイマーのエラー処理: タイマーのコールバックで例外がスローされても、後続のタイマーは引き続き正確に計測されます。SDKはtry/finallyを使って内部の時間計測コンテキストをクリアするため、あるコールバックで例外がスローされても、後続のタイマーの経過時間計測が破損することはありません。js-sdk-toolchainのコミットa2ccd0b1を参照して確認済みです。
dtを蓄積して遅延コールバックを実行する、カスタムのフレームごとのタイマーシステムは作成しないでください。 SDKにはすでにtimersが用意されています。カスタムシステムは処理を重複させ、エンジン独自のスケジューリングとのずれを生み、一度限りの遅延処理には適さない抽象化です。
カスタムエンジンインスタンスでは、@dcl/sdk/ecsのcreateTimers(engineInstance)を使用して、そのエンジンにスコープされたTimersオブジェクトを取得してください。
システムベースのタイマー(ゲームロジックに推奨。フレームループと同期します):
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);
}
});
エンティティの削除
engine.removeEntity(entity): boolean
エンティティからすべてのコンポーネントを削除し、その ID を再利用できるようにします。boolean を返します。
true— エンティティは受け入れられました。コンポーネントが削除され、ID は再利用のために解放されます。false— エンティティは拒否されました。コンポーネントには変更がなく、ID は予約されたままです。これは、レンダラー予約範囲内のエンティティ ID(アバターエンティティ。バージョンを問わず、3 からreservedStaticEntities - 1まで)で発生します。名前付き静的エンティティ 3 つ(engine.RootEntity、engine.PlayerEntity、engine.CameraEntity)も予約されており、解放されることはありません。ただし、これらのコンポーネントは引き続き削除されます(レンダラーはこの 3 つのシーン削除を受け入れます)。
js-sdk-toolchain のコミット e712ef71 以降、戻り値の型は void から boolean に変更されました。戻り値を無視する既存のコードには影響しません。
注意点 — アバターエンティティの衝突: この修正以前は、engine.removeEntity が、稼働中のリモートプレイヤーのアバターエンティティのコンポーネントを気付かれずに削除することがありました。エンジンはレンダラー予約 ID の削除を拒否するようになり、これを防止します。名前付き静的エンティティを明示的にクリアする意図がない限り、PlayerIdentityData を反復して得られたエンティティに対して removeEntity を呼び出さないでください。
removeEntityWithChildren
エンティティとそのすべての子を再帰的に削除します。複雑なエンティティ階層をクリーンアップするときに使用してください。
import { removeEntityWithChildren } from "@dcl/sdk/ecs";
removeEntityWithChildren(engine, parentEntity);
注意 — 部分的な完了: removeEntityWithChildren は、報告なしに部分的にしか完了しないことがあります。Transform ツリー内のどこかにレンダラー予約ノード(例:アバターエンティティ)がある場合、そのノードの削除は removeEntity によって拒否される一方で、その子孫は削除されます。その結果、残ったノードの Transform.parent は、削除済みのエンティティを指したままになります。シーンが予約済みエンティティをシーンエンティティの下に親として配置した場合にのみ、この状況が発生します。この関数は void を返すため、ノードごとの結果は得られません。js-sdk-toolchain のコミット e712ef71 で確認済みです。
Portable Experiences
ワールド間の移動後も存続するシーンです。~system/PortableExperiences からインポートします。
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は使用しません。- ウェアラブルを装着している間に実行されるポータブルエクスペリエンスを作成する方法については、smart-wearables スキルを参照してください。
- ホストシーンでは、
scene.jsonで有効にする必要があります:"featureToggles": { "portableExperiences": "enabled" }。値は"enabled"、"disabled"、"hideUi"(PX を起動しますが、その UI を非表示にします)です。"disabled"の場合、spawn()は何も行わないか、拒否されます。
Testing Framework
シーンには @dcl/sdk/testing を使用してユニットテストを含められます。テストはジェネレーターです。yield すると次のフレームまで一時停止するため、ティックをまたいでエンジンの状態を確認できます。
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)— 次の 4 つのみです。
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 テストランナーやテスト対応エクスプローラーです。通常のプレビューでは、テストランナーはログを出力するだけの何もしない処理で、本番ビルドでは DEBUG によって保護されています。テストはシーンと同じ QuickJS ランタイム内で実行されるため、同じ制約が適用されます(Node.js API は使用できず、SDK タイマーなどを使用してください)。
Logging
ランタイムで宣言されているのは console.log() と console.error() のみです。console.warn()、.info()、.debug()、.trace() は使用できません。
シーンの例
これらの API を実際のランタイムに対してテストする、エンジンチームのテストシーンです。
- https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/66,6-signed-fetch — クリック時に
signedFetchを呼び出します。response.ok/.status/.bodyを読み取り、自動追加された署名付きヘッダーを調べます。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/2,0-skybox-scene-json —
executeTaskとgetSceneInformation({})を使用し、ランタイムでシーン自身のscene.jsonを読み取ります。JSON.parse(sceneInfo.metadataJson)の後、worldConfiguration.skyboxConfigを調べ、存在しない場合はトップレベルのskyboxConfigを使用します。隣接する https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/3,0-skybox-world-json では、worldConfigurationデプロイメントに対して同じコードを実行します。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/80,-4-restricted-actions — UI ボタンですべての RestrictedAction を実行します。
movePlayerTo(cameraTargetとavatarTargetを指定)、teleportTo、triggerEmote、triggerSceneEmote、openExternalUrl、openNftDialog、changeRealm(messageを指定する場合と指定しない場合)を試せます。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/8,8-portable-experience — spawn の応答から
spawn({ ens })/kill({ pid })を呼び出します。ホストのscene.jsonではportableExperiences: "enabled"を指定しています。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/8,9-portable-experience-disabled — 同じですが、ホストの
scene.jsonではportableExperiences: "disabled"を指定しています(spawn は抑制されます)。 - https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/8,7-portable-experience-hide-ui — ホストの
scene.jsonではportableExperiences: "hideUi"を指定しています(PX は実行され、UI は非表示になります)。
RestrictedActions の完全なリファレンス(triggerSceneEmote の要件(_emote.glb)、setCommunicationsAdapter、その場での回転を伴う movePlayerTo、定義済みのエモート名)と、追加の executeTask バリエーション(エラー処理、逐次実行)については、{baseDir}/references/runtime-apis.md を参照してください。