From 211696fb96dceda6302ac17ffa11b1f4c83df84b Mon Sep 17 00:00:00 2001 From: nearnshaw Date: Mon, 3 Aug 2026 11:27:49 -0300 Subject: [PATCH 1/4] multiplayer server comments Co-Authored-By: Claude Opus 4.6 --- creator/sdk7/networking/authoritative-servers.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/creator/sdk7/networking/authoritative-servers.md b/creator/sdk7/networking/authoritative-servers.md index a7c5ab05..742344af 100644 --- a/creator/sdk7/networking/authoritative-servers.md +++ b/creator/sdk7/networking/authoritative-servers.md @@ -491,7 +491,7 @@ During local development, storage is written to `node_modules/@dcl/sdk-commands/ Storage is durable persistence for data that must survive server restarts and redeploys. It is not a live datastore. Keep your working game state in memory on the server. That's faster and it's the right pattern for a server. Write to Storage only when you really need to, at meaningful checkpoints. {% hint style="warning" %} -**⚠️ Warning**: The server runtime allows a maximum of **40 in-flight host calls** at once, shared across *everything* the scene asks the runtime to do: every storage request, `signedFetch`, and other runtime APIs all count against the same limit. Excess calls are **not queued**. They reject immediately with a `too many concurrent host calls` error. The SDK catches the rejection and resolves the `Storage.set` promise to `false` instead of throwing. If your code discards that boolean, the failed save is invisible, and your persisted data ends up stale or lost. Check the result of every write (see the Note above). +**⚠️ Warning**: The server runtime allows a maximum of **40 in-flight host calls** at once, shared across _everything_ the scene asks the runtime to do: every storage request, `signedFetch`, and other runtime APIs all count against the same limit. Excess calls are **not queued**. They reject immediately with a `too many concurrent host calls` error. The SDK catches the rejection and resolves the `Storage.set` promise to `false` instead of throwing. If your code discards that boolean, the failed save is invisible, and your persisted data ends up stale or lost. Check the result of every write (see the Note above). {% endhint %} Good moments to persist: @@ -917,7 +917,7 @@ Each connected peer can send up to approximately **300 messages per 1,000 ms**. ### External fetch -Concurrent `signedFetch` calls are capped at **32** in-flight. Additional fetches queue until a slot opens. Each fetch attempt has a **15-second** timeout with up to **2** retries. +Concurrent `signedFetch` calls are capped at **32** in-flight. Additional fetches queue until a slot opens. Each fetch attempt has a **15-second** timeout. Fetch responses are capped at **10 MB**. WebSocket connections are limited to **32** concurrent sockets, with a maximum message size of **1 MB** per message. ### In-flight host calls @@ -978,9 +978,13 @@ engine.addSystem(() => { ### Wait for the server to start up -The server is only active if there's at least one player present in the scene. If nobody's currently there, the server shuts down after a few minutes. +The server is only active while at least one player is in the scene. After the last player leaves, the server stays up for roughly two minutes, then shuts down. The next visit cold-starts a fresh instance, which takes approximately **15 seconds in production**. Local preview starts the server instantly, so cold-start issues almost always surface only after publishing. -When a first player comes into the scene after a while of inactivity, the server takes a few seconds to start up. Your scene's code should be prepared to have to wait for the server to be online. Initial requests to the server should have catch and retry mechanisms to provide resilience. +Your scene's code should be prepared to wait for the server to come online. Messages sent before the server finishes starting are silently lost. Show a "server waking up" message to the player while waiting, and add retry logic to initial server requests. + +{% hint style="info" %} +**Tip:** A reliable way to detect server readiness is a heartbeat: have the server write `Date.now()` to a synced component field every 2 seconds, and have the client track when it last saw the value change. If no change arrives within 6 seconds, treat the server as offline. This is more robust than `isStateSyncronized()`, which only confirms the transport is connected, not that the server is running. +{% endhint %} ## Complete Example From d047669ca9bccf4eacce3e555d0c86aadbcf8bbf Mon Sep 17 00:00:00 2001 From: nearnshaw Date: Mon, 3 Aug 2026 11:28:49 -0300 Subject: [PATCH 2/4] camera FOV Co-Authored-By: Claude Opus 4.6 --- creator/sdk7/3d-essentials/camera.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/creator/sdk7/3d-essentials/camera.md b/creator/sdk7/3d-essentials/camera.md index b435a2a6..1599638b 100644 --- a/creator/sdk7/3d-essentials/camera.md +++ b/creator/sdk7/3d-essentials/camera.md @@ -165,6 +165,18 @@ function main() { } ``` +## Field of View + +You can set a custom field of view (FOV) on a virtual camera with the `fov` property. The value is in degrees. If omitted, it defaults to 60. + +```ts +VirtualCamera.create(myCustomCamera, { + fov: 90, +}) +``` + +A wider FOV (higher value) shows more of the scene at once and creates a sense of speed, which can be useful for racing games. A narrower FOV (lower value) zooms in and is useful for aiming or cinematic shots. + ## Camera Transitions Whenever the scene switches between virtual cameras, or between the default camera behavior and virtual cameras, players see a transition. The position, rotation and any other parameters of the virtual camera change smoothly over a period time. From 511267b12cf198c472fe693166c65cf90e81d405 Mon Sep 17 00:00:00 2001 From: nearnshaw Date: Mon, 3 Aug 2026 11:29:30 -0300 Subject: [PATCH 3/4] open Explorer UI Co-Authored-By: Claude Opus 4.6 --- creator/sdk7/interactivity/external-links.md | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/creator/sdk7/interactivity/external-links.md b/creator/sdk7/interactivity/external-links.md index b2120a71..676e8b0c 100644 --- a/creator/sdk7/interactivity/external-links.md +++ b/creator/sdk7/interactivity/external-links.md @@ -68,6 +68,68 @@ The link is opened in a new tab, keeping the original tab in Decentraland. If players tick the _trust this domain_ checkbox, they won't be prompted again during their session, as long as the link comes from the same scene and is to the same domain. +## Open Explorer UI panels + +Use `openExplorerUi()` to open one of the Explorer's built-in fullscreen panels (the map, settings, backpack, and others) from your scene. The player must trigger the action with an explicit click or button event, the same as `openExternalUrl`. + +```ts +import { openExplorerUi } from '~system/RestrictedActions' + +// Open the map panel +const result = await openExplorerUi({ ui: 1 }) +``` + +The `ui` field selects which panel to open. Use the `ExplorerUi` values from `~system/RestrictedActions`: + +| Value | Name | Panel | +|---|---|---| +| 0 | `EU_SETTINGS` | Settings | +| 1 | `EU_MAP` | Map | +| 2 | `EU_BACKPACK` | Backpack | +| 3 | `EU_CAMERA_REEL` | Camera Reel | +| 4 | `EU_COMMUNITIES` | Communities | +| 5 | `EU_PLACES` | Places | +| 6 | `EU_EVENTS` | Events | + +The function returns a promise with an `openResult` field that tells you what happened: + +| Result | Meaning | +|---|---| +| `OPENED` | The panel was opened successfully. | +| `WAS_ALREADY_OPEN` | A fullscreen panel was already open. | +| `REJECTED_NOT_CURRENT_SCENE` | The player is no longer in the scene that made the request. | +| `REJECTED_FEATURE_DISABLED` | The requested panel is disabled or unavailable in this client. | +| `REJECTED_NO_USER_GESTURE` | The call did not come from a user gesture (click or button event). | + +Here is a full example that opens the map when a player clicks a cube: + +```ts +import { openExplorerUi } from '~system/RestrictedActions' + +// Create a clickable cube that opens the map +const mapButton = engine.addEntity() +Transform.create(mapButton, { position: Vector3.create(8, 1, 8) }) +MeshRenderer.setBox(mapButton) +MeshCollider.setBox(mapButton) + +pointerEventsSystem.onPointerDown( + { + entity: mapButton, + opts: { button: InputAction.IA_POINTER, hoverText: 'Open Map' }, + }, + async () => { + const result = await openExplorerUi({ ui: 1 }) + if (result.openResult === 1) { + console.log('Map opened') + } + } +) +``` + +{% hint style="warning" %} +**Note:** Like other restricted actions, `openExplorerUi` can only be called from an explicit user gesture (a click or button event). Calling it from a timer, collision area, or any other trigger will be rejected. +{% endhint %} + ## Copy to clipboard To copy a string to the player's clipboard, use `copyToClipboard()`. After this, when the player does _paste_ in the Decentraland chat or in any other application on their machine, they will be pasting your string. From 61679ba180ab4bb48859cabbecb1f715d099377e Mon Sep 17 00:00:00 2001 From: nearnshaw Date: Thu, 6 Aug 2026 12:38:13 -0300 Subject: [PATCH 4/4] comments and links from other pages Co-Authored-By: Claude Opus 4.6 --- .../scene-editor/configure/scene-settings.md | 36 +++--- creator/sdk7/interactivity/external-links.md | 43 ++++--- .../optimizing/performance-optimization.md | 57 ++++++++++ creator/sdk7/projects/scene-metadata.md | 107 +++++++++--------- 4 files changed, 155 insertions(+), 88 deletions(-) diff --git a/creator/scene-editor/configure/scene-settings.md b/creator/scene-editor/configure/scene-settings.md index 17c1281c..4739c686 100644 --- a/creator/scene-editor/configure/scene-settings.md +++ b/creator/scene-editor/configure/scene-settings.md @@ -20,17 +20,18 @@ The **Details** tab lets you configure several fields about your scene. These fi The following fields are available: -* **Name** -* **Description** -* **Thumbnail** - - {% hint style="info" %} - **πŸ’‘ Tip**: If no thumbnail is provided, it uses the automatic capture you see on the scene's card. We recommend uploading a more attractive image - {% endhint %} -* **Categories** -* **Creator name** (optional) -* **Creator contact email** (optional) -* **Creator wallet address** (optional) +- **Name** +- **Description** +- **Thumbnail** + + {% hint style="info" %} + **πŸ’‘ Tip**: If no thumbnail is provided, it uses the automatic capture you see on the scene's card. We recommend uploading a more attractive image + {% endhint %} + +- **Categories** +- **Creator name** (optional) +- **Creator contact email** (optional) +- **Creator wallet address** (optional) The thumbnail should be a .png image of a recommended size of 228x160 pixels. The minimum supported size is 196x143 pixels. The image may be stretched if the width-to-height proportions don’t match 228x160. @@ -40,14 +41,12 @@ See [scene metadata](../../sdk7/projects/scene-metadata.md) for more details on **πŸ“” Note**: The scene's **Age Rating** is not edited on this panel. You can set the `rating` field directly in the `scene.json` file, or, for scenes published to a World, change the **Content Rating** in the Creator Hub's World Settings after publishing. Decentraland is an 18+ platform, so the rating to set is `A` for Adults. See [Age Rating](../../sdk7/projects/scene-metadata.md#age-rating). {% endhint %} - ### Tipping You can receive tips from players who visit your scene. To enable tipping, got to the **Details** tab on the scene settings and provide an Ethereum address under **Creator wallet address**. ![](../../.gitbook/assets/creator-wallet-address.png) - When a player visits your scene, they will see a piggy bank icon on the top-left of the screen. Clicking on it opens a modal where they can send you a tip. This menu can also be accessed by opening your scene's info on the map. ![](../../.gitbook/assets/TipIcon.png) @@ -56,9 +55,12 @@ The tip modal allows the player to select the amount of MANA they want to send. ![](../../.gitbook/assets/tipping-modal.png) - You will receive a notification on the Decentraland notifications tab whenever a player sends you a tip. +{% hint style="info" %} +**πŸ’‘ Tip**: You can also make tipping more discoverable by adding 3D content that invites players to tip, like a tip jar or a donation sign, and using [`openExplorerUi()`](../../sdk7/interactivity/external-links.md#open-explorer-ui-panels) so that clicking it opens the tipping UI. +{% endhint %} + ## Layout You can edit the size of your scene by clicking the _pencil icon_ and then changing the number of rows and columns. @@ -99,9 +101,9 @@ You can disable certain functionalities on your scene if you chose, in case they ![](../../images/editor/scene-restrictions.png) -* **Silence Voice Chat**: Prevent players on your scene from using voice chat. -* **Disable Nearby Voice Chat**: Prevent players on your scene from using the nearby (proximity-based) voice chat. -* **Disable Smart Wearables & Portable Experiences**: Prevent players from using [Smart Wearables](../../sdk7/projects/smart-wearables.md) or [Portable Experiences](../../sdk7/projects/portable-experiences.md). +- **Silence Voice Chat**: Prevent players on your scene from using voice chat. +- **Disable Nearby Voice Chat**: Prevent players on your scene from using the nearby (proximity-based) voice chat. +- **Disable Smart Wearables & Portable Experiences**: Prevent players from using [Smart Wearables](../../sdk7/projects/smart-wearables.md) or [Portable Experiences](../../sdk7/projects/portable-experiences.md). ## Skybox Control diff --git a/creator/sdk7/interactivity/external-links.md b/creator/sdk7/interactivity/external-links.md index 676e8b0c..54c40ee6 100644 --- a/creator/sdk7/interactivity/external-links.md +++ b/creator/sdk7/interactivity/external-links.md @@ -48,7 +48,6 @@ The player will spawn in one of the spawn points of the scene in that world, reg To send a player back to Genesis City from a world, use `changeRealm` setting the `realm` field to _'https://realm-provider-ea.decentraland.org/main'_. - ## External links To add a link to an external website, use the `openExternalUrl()` command. @@ -67,7 +66,6 @@ The link is opened in a new tab, keeping the original tab in Decentraland. If players tick the _trust this domain_ checkbox, they won't be prompted again during their session, as long as the link comes from the same scene and is to the same domain. - ## Open Explorer UI panels Use `openExplorerUi()` to open one of the Explorer's built-in fullscreen panels (the map, settings, backpack, and others) from your scene. The player must trigger the action with an explicit click or button event, the same as `openExternalUrl`. @@ -81,25 +79,25 @@ const result = await openExplorerUi({ ui: 1 }) The `ui` field selects which panel to open. Use the `ExplorerUi` values from `~system/RestrictedActions`: -| Value | Name | Panel | -|---|---|---| -| 0 | `EU_SETTINGS` | Settings | -| 1 | `EU_MAP` | Map | -| 2 | `EU_BACKPACK` | Backpack | -| 3 | `EU_CAMERA_REEL` | Camera Reel | -| 4 | `EU_COMMUNITIES` | Communities | -| 5 | `EU_PLACES` | Places | -| 6 | `EU_EVENTS` | Events | +| Value | Name | Panel | +| ----- | ---------------- | ----------- | +| 0 | `EU_SETTINGS` | Settings | +| 1 | `EU_MAP` | Map | +| 2 | `EU_BACKPACK` | Backpack | +| 3 | `EU_CAMERA_REEL` | Camera Reel | +| 4 | `EU_COMMUNITIES` | Communities | +| 5 | `EU_PLACES` | Places | +| 6 | `EU_EVENTS` | Events | The function returns a promise with an `openResult` field that tells you what happened: -| Result | Meaning | -|---|---| -| `OPENED` | The panel was opened successfully. | -| `WAS_ALREADY_OPEN` | A fullscreen panel was already open. | -| `REJECTED_NOT_CURRENT_SCENE` | The player is no longer in the scene that made the request. | -| `REJECTED_FEATURE_DISABLED` | The requested panel is disabled or unavailable in this client. | -| `REJECTED_NO_USER_GESTURE` | The call did not come from a user gesture (click or button event). | +| Result | Meaning | +| ---------------------------- | ------------------------------------------------------------------ | +| `OPENED` | The panel was opened successfully. | +| `WAS_ALREADY_OPEN` | A fullscreen panel was already open. | +| `REJECTED_NOT_CURRENT_SCENE` | The player is no longer in the scene that made the request. | +| `REJECTED_FEATURE_DISABLED` | The requested panel is disabled or unavailable in this client. | +| `REJECTED_NO_USER_GESTURE` | The call did not come from a user gesture (click or button event). | Here is a full example that opens the map when a player clicks a cube: @@ -130,6 +128,15 @@ pointerEventsSystem.onPointerDown( **Note:** Like other restricted actions, `openExplorerUi` can only be called from an explicit user gesture (a click or button event). Calling it from a timer, collision area, or any other trigger will be rejected. {% endhint %} +### Practical uses + +Opening Explorer panels directly from your scene's content removes friction for players who might not know where to find these menus. Some ideas: + +- **Tipping**: If your scene has [tipping enabled](../projects/scene-metadata.md#tipping), place a tip jar or donation sign as clickable 3D content that UI. +- **Performance settings**: If your scene detects that the player is running at a low frame rate, show a UI prompt suggesting they lower their graphics settings, with a button that opens the settings panel. See [performance optimization](../optimizing/performance-optimization.md#help-players-adjust-their-settings). +- **Onboarding**: If your experience relies on wearables or emotes, add a mirror or wardrobe that opens the backpack panel so players can equip them without leaving the flow of your scene. +- **Photo opportunities**: Next to a scenic photo spot, add a sign that opens the camera reel panel so players can review the pictures they've taken. + ## Copy to clipboard To copy a string to the player's clipboard, use `copyToClipboard()`. After this, when the player does _paste_ in the Decentraland chat or in any other application on their machine, they will be pasting your string. diff --git a/creator/sdk7/optimizing/performance-optimization.md b/creator/sdk7/optimizing/performance-optimization.md index 2e08dd29..7f2ea945 100644 --- a/creator/sdk7/optimizing/performance-optimization.md +++ b/creator/sdk7/optimizing/performance-optimization.md @@ -193,3 +193,60 @@ Keep in mind that the performance you experience in preview may differ from that It's always a good practice to try deploying your scene first to a [Decentraland World](../publishing/publishing-options.md#decentraland-worlds) to do some more thorough testing. Always ask players for feedback. Never take for granted that how you experience the scene is the same for everyone else. + +## Help players adjust their settings + +Even a well optimized scene can run poorly on low-end hardware, or when the player has their graphics settings set too high for their machine. You can help these players by detecting a low frame rate from your scene's code and showing a UI tip that recommends lowering their graphics settings, with a button that opens the Explorer's settings panel directly via [`openExplorerUi()`](../interactivity/external-links.md#open-explorer-ui-panels). + +Your scene can estimate the frame rate by counting how many times a system runs over a period of time. In the example below, if the average is under 20 FPS for a 5 second period, the scene displays a UI prompt. Clicking the prompt's button opens the settings panel, this click counts as the user gesture that `openExplorerUi()` requires. + +```tsx +import { engine } from '@dcl/sdk/ecs' +import { openExplorerUi } from '~system/RestrictedActions' +import ReactEcs, { Button, Label, ReactEcsRenderer, UiEntity } from '@dcl/sdk/react-ecs' + +let showSettingsTip = false + +// Estimate the average FPS over 5 second intervals +let elapsed = 0 +let frames = 0 +engine.addSystem((dt: number) => { + elapsed += dt + frames += 1 + if (elapsed >= 5) { + if (frames / elapsed < 20) { + showSettingsTip = true + } + elapsed = 0 + frames = 0 + } +}) + +ReactEcsRenderer.setUiRenderer(() => ( + +