> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/creator/content-creator-zh/chang-jing-sdk7/wang-luo/serverless-multiplayer.md).

# 无服务器多人游戏

在玩家之间同步场景状态。

Decentraland 在玩家的浏览器实例中本地运行场景。默认情况下，玩家可以彼此可见并直接交互，但每个玩家都是独立与环境交互的。默认情况下，环境中的变化不会在玩家之间共享。

让所有人看到相同内容，并处于相同状态，对玩家以更有意义的方式进行交互极其重要。

有三种同步场景状态的方法，这样所有玩家看到的内容都相同：

* **将实体标记为已同步**：最简单的选项。参见 [已将实体标记为已同步](#mark-an-entity-as-synced)
* **发送显式 MessageBus 消息**：手动发送并监听特定消息。参见 [发送显式 MessageBus 消息](#send-explicit-messagebus-messages)
* **使用多人服务器**：参见 [多人服务器](/creator/content-creator-zh/chang-jing-sdk7/wang-luo/authoritative-servers.md)。服务器会验证所有状态变更，并且是唯一的事实来源。需要更多设置，但如果玩家有动机利用你的场景，强烈建议使用。

本文档涵盖前两个选项。它们更简单，因为不需要服务器。缺点是你更依赖玩家的连接速度，而且当所有玩家离开场景时，场景状态不会被持久化。

## 将实体标记为已同步

在 [创作者中心](/creator/content-creator-zh/chang-jing-bian-ji-qi/kai-shi-shi-yong/about-editor.md)，可通过添加一个 **多人组件** 来将实体标记为已同步。它包含实体上其他每个组件的复选框，允许你选择要更新哪些组件。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-f64a1bf75807de2bc206e2b7faaacbee74d976d7%2Fmultiplayer-component.png?alt=media)

若要通过代码将实体标记为已同步，请使用 `syncEntity` 函数：

```ts
import { syncEntity } from "@dcl/sdk/network";

export function main() {
  const doorEntity = engine.addEntity();

  syncEntity(doorEntity, [Transform.componentId, Animator.componentId], 1);
}
```

{% hint style="warning" %}
**📔 注意**：始终调用 `syncEntity()` 在 `main()` 函数，或者在一个运行于之后的函数中调用 `main()` （例如回调或系统）。在文件顶层调用它会抛出错误，因为此时玩家配置文件尚未初始化。
{% endhint %}

{% hint style="warning" %}
**📔 注意**：在无服务器多人模式中，每个客户端都会自行调用 `syncEntity` 。如果你升级到 [多人服务器](/creator/content-creator-zh/chang-jing-sdk7/wang-luo/authoritative-servers.md)，则模式会改变：只有服务器应该调用 `syncEntity`，并由 `isServer()`保护。客户端应避免声明共享实体的同步，除非这些实体是由客户端创建的。
{% endhint %}

该 `syncEntity` 函数接收以下输入：

* **entityId**：要同步的实体引用
* **componentIds**：需要从该实体同步的一组组件。这是一个数组，可包含任意多个组件。所有值都应为 `componentId` 属性。
* **entityEnumId**：（可选）一个唯一 id，由所有玩家一致使用，参见 [关于枚举 id](#about-the-enum-id).

并非所有实体或组件都需要同步。像树这样保持在同一位置的静态元素不需要同步。对于需要同步的实体，只应同步那些会随时间变化的组件。例如，如果一个立方体在被点击时改变颜色，你只应同步 Material 组件，而不是 MeshRenderer 或 Transform，因为它们不会发生变化。

{% hint style="info" %}
**💡 提示**：如果你想共享的数据并不存在于某个组件中，请定义一个 [自定义组件](/creator/content-creator-zh/chang-jing-sdk7/jia-gou/custom-components.md) 来保存该数据。
{% endhint %}

### 关于实体的枚举 id

该 **entityEnumId** 必须唯一。它与在 `engine.addEntity()`上分配的本地 entityId 无关，后者是自动生成的，并且在运行同一场景的不同玩家之间可能不同。实体的 entityEnumId 必须在代码中显式定义，并且必须唯一。

显式设置此 ID 很重要，可避免因竞态条件导致场景的一部分先于另一部分加载而出现不一致。也许对玩家 A 来说，场景中的门是实体 *512*，但对玩家 B 来说，同一扇门是实体 *513*。在这种情况下，如果玩家 A 打开门，玩家 B 看到的就会变成整栋建筑在移动。

{% hint style="info" %}
**💡 提示**：在你的场景中创建一个枚举，以便清晰引用场景中每个可同步的 id。

```ts
import { syncEntity } from "@dcl/sdk/network";

enum EntityEnumId {
  DOOR = 1,
  DRAW_BRIDGE = 2,
  ELEVATOR = 3,
}

export function main() {
  syncEntity(
    doorEntity,
    [Transform.componentId, Animator.componentId],
    EntityEnumId.DOOR
  );
}
```

这里使用 EntityEnumId 枚举为实体添加唯一标识，确保无论创建顺序如何，每个客户端都能识别被修改的实体。
{% endhint %}

{% hint style="warning" %}
**📔 注意**：如果你的场景还包含 Smart Items，请避免使用高于 **8001** 的数字。由 [创作者中心](/creator/content-creator-zh/chang-jing-bian-ji-qi/kai-shi-shi-yong/about-editor.md) 创建且带有 Multiplayer 组件的物品会自动使用从 8001 开始分配的 ID。任何低于 8001 的 ID 都可以安全地分配给你的同步实体。
{% endhint %}

**由玩家创建的实体**

如果实体是由玩家交互产生的，并且该实体应与其他玩家同步，则该实体不需要 entityEnumId。你可以使用 `syncEntity()` ，只传入实体和组件列表。entityEnumId 的唯一值会在幕后自动分配。

在场景初始化时实例化的所有实体都需要手动分配 ID。这样可以确保所有玩家都使用相同的 ID。当某个实体由单个玩家负责实例化时，就不需要显式 ID。其他玩家会收到这个新实体的更新，其 ID 已经分配好，因此不会有 ID 不匹配的风险。

例如，在一场打雪仗的场景中，每当玩家扔出一个雪球时，都会实例化一个新的实体，并与其他玩家同步。这个雪球不需要唯一的 entityEnumId。

```ts
import { syncEntity } from "@dcl/sdk/network";

function onThrow() {
  const ball = engine.addEntity();
  Transform.create(ball, {});
  GltfContainer.create(ball, { src: "assets/snowBall.glb" });
  syncEntity(ball, [Transform.componentId, GltfContainer.componentId]);
}
```

**父子实体**

实体的父级通常通过 `parent` 中的属性 `Transform` 组件定义。不过，该属性指向的是父级的本地实体 id，这可能会变化，参见 [关于枚举 id](#about-the-enum-id)。要为需要同步的实体建立父子关系，或者为具有需要同步的子实体建立父子关系，请使用 `parentEntity()` 函数，而不是 `Transform`.

```ts
import { syncEntity, parentEntity } from "@dcl/sdk/network";

export function main() {
  const parent = engine.addEntity();
  Transform.create(parent, { position: somePosition });
  syncEntity(parent, []);

  const child: Entity = engine.addEntity();
  syncEntity(child, [Transform.componentId]);

  parentEntity(child, parent);
}
```

注意，父实体和子实体都通过 `syncEntity`进行了同步，因此所有玩家都能对这两个实体使用的 id 达成一致理解。即使父实体的组件可能永远不需要改变，这一点也很必要。在这个例子中， `syncEntity` 包含了一个空的组件数组，以避免同步任何不必要的组件。

{% hint style="warning" %}
**📔 注意**：如果一个实体同时被 `parentEntity()` 以及 `parent` 中的属性 `Transform` 组件设为父级， `Transform` 组件中的属性将被忽略。
{% endhint %}

当实体通过 `parentEntity()` 函数建立父子关系时，你还可以使用以下辅助函数：

* **removeParent()**：撤销 `parentEntity()`的效果。它只需要你传入子实体。该实体的新父级会变为场景的根实体。原来的父实体不会从场景中移除。
* **getParent()**：返回你传入实体的父实体。
* **getChildren()**：以可迭代对象的形式返回你传入实体的子实体列表。
* **getFirstChild()**：返回你传入实体列表中的第一个子实体。

```ts
import {
  syncEntity,
  parentEntity,
  getParent,
  getFirstChild,
  getChildren,
  removeParent,
} from "@dcl/sdk/network";

export function main() {
  const parent = engine.addEntity();
  Transform.create(parent, { position: somePosition });
  syncEntity(parent, []);

  const child: Entity = engine.addEntity();
  syncEntity(child, [Transform.componentId]);

  // 将 parent 设为父级
  parentEntity(child, parent);

  // getParent
  const getParentResult = getParent(child);
  // 返回父级

  // getFirstChild
  const getFirstChildResult = getFirstChild(parent);
  // 返回子级

  // getChildren
  const getChildrenResult = Array.from(getChildren(parent));
  // 返回 [child]

  // 从 child 中移除父级
  removeParent(child);
}
```

## 检查同步状态

当玩家刚进入场景时，他们可能尚未与周围其他玩家同步。如果玩家在同步前开始改变游戏状态，这可能会在你的游戏中引发问题。我们建议始终先检查玩家是否已同步，然后再允许他们编辑场景中的任何内容。

如果玩家走出场景的地块范围，他们在站在外面时也会与场景不同步。因此，场景的系统也必须处理这种情况，因为当玩家在附近时，场景会继续运行。一旦玩家重新进入，他们会自动收到场景状态中发生的任何变化。

你可以通过 `isStateSyncronized()` 函数检查玩家当前是否已与场景状态同步。该函数返回一个布尔值，如果玩家已经与场景同步，则为 true。

```ts
import { isStateSyncronized } from "@dcl/sdk/network";

const isConnected = isStateSyncronized();
```

例如，你可以在系统中加入此检查，并在该函数返回 false 时阻止任何交互。

```ts
import { isStateSyncronized } from "@dcl/sdk/network";

engine.addSystem(() => {
  if (isStateSyncronized() && !button.enabled) {
    console.log("启用开始游戏");
    button.enable();
  }

  if (!isStateSyncronized() && button.enabled) {
    console.log(`禁用开始游戏。`);
    button.disable();
  }
});
```

## 发送显式 MessageBus 消息

{% hint style="warning" %}
**📔 注意**： `MessageBus` API 在 SDK 中被标记为已弃用，并且可能在未来版本中被移除。对于大多数用例，建议使用 [将实体标记为已同步](#mark-an-entity-as-synced).
{% endhint %}

**初始化消息总线**

创建一个消息总线对象，用于处理玩家之间发送和接收消息所需的方法。

```ts
import { MessageBus } from "@dcl/sdk/message-bus";

const sceneMessageBus = new MessageBus();
```

**发送消息**

使用 `.emit` 消息总线的命令，用于向场景中的所有其他玩家发送消息。

```ts
import { MessageBus } from "@dcl/sdk/message-bus";

const sceneMessageBus = new MessageBus();

const myEntity = engine.addEntity();
MeshRenderer.setBox(myEntity);
MeshCollider.setBox(myEntity);

pointerEventsSystem.onPointerDown(
  {
    entity: myEntity,
    opts: { button: InputAction.IA_PRIMARY, hoverText: "点击" },
  },
  function () {
    sceneMessageBus.emit("box1Clicked", {});
  }
);
```

每条消息都可以在第二个参数中包含一个负载。负载的类型为 `Object`，并且可以包含你希望发送的任何相关数据。

```ts
import { MessageBus } from "@dcl/sdk/message-bus";

const sceneMessageBus = new MessageBus();

sceneMessageBus.emit("spawn", { position: { x: 10, y: 2, z: 10 } });
```

{% hint style="info" %}
**💡 提示**：如果你需要一条消息包含来自多个变量的数据，请创建一个自定义类型，将所有这些数据保存在单个对象中。
{% endhint %}

**接收消息**

要处理该场景中所有其他玩家发来的消息，请使用 `.on`。使用此函数时，你提供一个消息字符串并定义一个要执行的函数。每当收到一个匹配字符串的消息时，给定的函数会执行一次。

```ts
import { MessageBus } from "@dcl/sdk/message-bus";

const sceneMessageBus = new MessageBus();

type NewBoxPosition = {
  position: { x: number; y: number; z: number };
};

sceneMessageBus.on("spawn", (info: NewBoxPosition) => {
  const myEntity = engine.addEntity();
  Transform.create(myEntity, {
    position: { x: info.position.x, y: info.position.y, z: info.position.z },
  });
  MeshRenderer.setBox(myEntity);
  MeshCollider.setBox(myEntity);
});
```

{% hint style="warning" %}
**📔 注意**：玩家发送的消息也会被该玩家自己接收到。 `.on` 方法无法区分由同一玩家发出的消息和由其他玩家发出的消息。
{% endhint %}

**完整的 MessageBus 示例**

此示例使用消息总线在主立方体被点击时发送一条新消息，在随机位置生成一个新立方体。消息包含新立方体的位置，因此所有玩家都会在相同位置看到这些新立方体。

```ts
import { MessageBus } from "@dcl/sdk/message-bus";

/// --- 创建消息总线 ---
const sceneMessageBus = new MessageBus();

// 立方体工厂
function createCube(x: number, y: number, z: number): Entity {
  const meshEntity = engine.addEntity();
  Transform.create(meshEntity, { position: { x, y, z } });
  MeshRenderer.setBox(meshEntity);
  MeshCollider.setBox(meshEntity);

  // 当立方体被点击时，发送消息以生成另一个立方体
  pointerEventsSystem.onPointerDown(
    {
      entity: meshEntity,
      opts: { button: InputAction.IA_PRIMARY, hoverText: "按 E 生成" },
    },
    function () {
      sceneMessageBus.emit("spawn", {
        position: {
          x: 1 + Math.random() * 8,
          y: Math.random() * 8,
          z: 1 + Math.random() * 8,
        },
      });
    }
  );

  return meshEntity;
}

// 初始化
createCube(8, 1, 8);

// 定义数据类型
type NewBoxPosition = {
  position: { x: number; y: number; z: number };
};

// 收到 spawn 消息时，创建新的立方体
sceneMessageBus.on("spawn", (info: NewBoxPosition) => {
  createCube(info.position.x, info.position.y, info.position.z);
});
```

## 在本地测试多人场景

如果你启动场景预览并在两个（或更多）不同的 explorer 窗口中打开它，每个打开的窗口都会被视为一个独立玩家，而一个模拟通信服务器会让这些玩家保持同步。

在一个窗口中与场景交互，然后切换到另一个窗口，看看该交互的效果是否也会在那里显示。

使用 Creator Hub，再次点击“预览”按钮，它会打开第二个 Decentraland explorer 窗口。你必须在两个窗口中使用不同的地址连接。即使场景重新加载，两个会话也会保持打开。

![](https://2460066822-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoPnXBby9S6MrsW83Y9qZ%2Fuploads%2Fgit-blob-30317b50bc4b4e646a1b28effe1ff76939ab0f28%2Fpreview-button.png?alt=media)

作为替代方案，你可以在浏览器 URL 中输入以下内容，以打开第二个 Decentraland explorer 窗口：

> `decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true&multi-instance=true`

## 单人场景

如果你的场景部署到 [Decentraland 世界](/creator/content-creator-zh/chang-jing-sdk7/fa-bu/publishing-options.md#decentraland-worlds)，你可以将其设为单人场景。玩家不会彼此可见，也无法聊天或看到彼此行为的影响。

要做到这一点，请配置场景的 `scene.json` 文件，以设置 **fixedAdapter** 为 `offline:offline`。该场景将完全没有通信服务，加入该世界的每个用户始终都是独自一人。

**示例：**

```json
{
  "worldConfiguration": {
    "name": "my-name.dcl.eth",
    "fixedAdapter": "offline:offline"
  }
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/creator/content-creator-zh/chang-jing-sdk7/wang-luo/serverless-multiplayer.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
