ZEPETO 玩家

- ZepetoPlayers 是一个用于控制 ZEPETO 玩家和 ZEPETO 角色的管理器(单例)类。
- 通过将 ZepetoPlayers 添加到场景中,您可以创建、删除和操作 ZEPETO 玩家。
- ZepetoPlayer 代表一个 ZEPETO 角色的单独实例,用于管理您在多人世界中直接控制的玩家和其他玩家。
- 在多人世界中创建的每个 ZepetoPlayer 都会分配一个唯一的会话 ID,并使用该会话 ID 进行管理。
- 因为 ZepetoPlayer 具有 ZepetoCharacter 的属性,所以可以使用与 ZepetoCharacter 相关的函数进行控制。
- 有三种类型的 ZepetoPlayer:
ZepetoPlayer | 描述 |
|---|---|
本地玩家 | 表示一个由本地用户直接控制的ZEPETO角色实例。 - 附带角色控制器/Zepeto相机组件。 |
网络玩家(远程玩家) | 一个可以在多人游戏内容中加载和使用的ZEPETO角色实例。 - 没有附带角色控制器/Zepeto相机组件。 |
机器人玩家 | 一个用于多人游戏内容的ZEPETO角色实例,但由机器人控制,而不是由真实用户控制。 在启动玩家不足的多人世界时或玩家在游戏中离开时用作替代。 - 没有附带角色控制器/Zepeto相机组件。 |
- ZepetoCharacter 是ZEPETO角色的基本实例单元,可以在世界场景中加载和控制。
- ZepetoCharacter具有通过ZEPETO应用程序创建的头像的外观。
添加ZepetoPlayers
在层级窗口中,选择ZEPETO → ZepetoPlayers选项卡。

您现在可以将其添加到您的场景中。

请注意,仅添加 ZepetoPlayers 并不会将 Zepeto Player 引入场景。您需要使用 ZepetoPlayers 的角色创建 API 实现一个脚本。
如果您希望快速创建并尝试仅在场景中的本地玩家,请参考以下指南:
有关如何显示Zepeto玩家的名称和头像的示例,请查看指南:
ZEPETO 玩家 API
如果您对ZepetoPlayers API感兴趣,请参考文档:
本指南主要涵盖在多人场景中使用ZEPETO玩家的示例。
使用ZEPETO玩家实现多人位置同步
在单人世界中,只需要创建一个本地玩家,由于只有本地玩家出现在屏幕上,因此不需要同步。
然而,在多人世界中,不仅需要显示您直接控制的本地玩家,还需要显示其他玩家,称为网络玩家。
网络玩家的每一个动作——移动、跳跃和执行特定手势——都应该在您的屏幕上完全相同。
这个过程称为同步。
如果不实现同步脚本,您将无法看到网络玩家的外观或移动。
在没有同步的多人游戏世界中,知道另一个客户端是否已进入房间的唯一方法是通过主页按钮。

步骤 1 : 设置多人游戏环境
建议通过多人游戏教程视频来了解多人游戏的基本设置和概念。
步骤 2 : 在您的屏幕上显示其他玩家
您的本地玩家被视为其他设备上的网络玩家。
这意味着即使是您的本地玩家也必须向服务器发送信息以进行同步。
所有连接到多人游戏世界的玩家应共享他们的信息。
所有连接到多人游戏房间的客户端共享多人游戏房间状态数据。
此房间状态数据遵循在Schemas.json中定义的模式。
(请将 Schema 视为数据结构)
在本指南中,您将通过房间状态数据同步玩家的位置。因此,在 Schemas.json 中定义一个可以表示每个玩家位置数据的 Schema。
{
"State" : {"players" : {"map" : "Player"}},
"Player" : {"sessionId" : "string","zepetoUserId" : "string","transform" : "Transform","state" : "number","subState" : "number"},
"Transform" : {"position" : "Vector3","rotation" : "Vector3"},
"Vector3" : {"x" : "number","y" : "number","z" : "number"}
}
👍 提示
- 将所有服务器和所有客户端应共享的信息存储在 多人房间状态。
- 为每个玩家保存单独的数据,例如等级、经验值、分数等,使用 数据存储。
服务器脚本可以识别当另一个玩家进入房间时,并可以将该信息发送给客户端以将该玩家加载到场景中。
步骤 2-1 : 基本服务器脚本
当玩家进入多人游戏世界的房间时,调用 onJoin()。
在服务器脚本中,将连接玩家的信息添加到房间状态的玩家列表中。
此外,当玩家退出房间时,调用 onLeave()。
服务器脚本将从房间状态的玩家中移除离开的玩家的信息。
import {Sandbox, SandboxOptions, SandboxPlayer} from "ZEPETO.Multiplay";
import {Player} from "ZEPETO.Multiplay.Schema";
export default class extends Sandbox {
onCreate(options: SandboxOptions) {
}
async onJoin(client: SandboxPlayer) {
const player = new Player();
player.sessionId = client.sessionId;
if (client.userId) {
player.zepetoUserId = client.userId;
}
// 使用 sessionId 管理玩家对象,这是客户端对象的唯一键值。
// 客户端可以通过将 add_OnAdd 事件添加到玩家对象来检查添加的玩家对象的信息。
this.state.players.set(client.sessionId, player);
}
async onLeave(client: SandboxPlayer, consented?: boolean) {
this.state.players.delete(client.sessionId);
}
}
步骤 2-2 : 基本客户端脚本
在创建多人游戏世界时,需要一个客户端脚本与服务器进行通信。
下面是一个用于多人游戏的基本客户端脚本示例。
在客户端,我们使用 currentPlayers 映射数据结构来管理要显示的玩家数据。
下面展示了关键代码行:
ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
当客户端从服务器的房间状态接收到玩家信息,并且有新玩家加入房间时,将分配一个会话 ID。如果正在创建的玩家的会话 ID 与我们自己的会话 ID 匹配,则该玩家被视为本地玩家。在这种情况下,玩家将被实例化为 isLocal = true, 表示这是本地玩家。
随后,非本地玩家被创建为 isLocal = false。这确保了所有玩家的外观,无论是本地还是非本地,都在屏幕上呈现。
import {ZepetoScriptBehaviour} from 'ZEPETO.Script'
import {ZepetoWorldMultiplay} from 'ZEPETO.World'
import {Room, RoomData} from 'ZEPETO.Multiplay'
import {Player, State, Vector3} from 'ZEPETO.Multiplay.Schema'
import {CharacterState, SpawnInfo, ZepetoPlayers, ZepetoPlayer, CharacterJumpState} from 'ZEPETO.Character.Controller'
import * as UnityEngine from "UnityEngine";
export default class MultiplayClientCode extends ZepetoScriptBehaviour {
public multiplay: ZepetoWorldMultiplay;
private room: Room;
private currentPlayers: Map<string, Player> = new Map<string, Player>();
private zepetoPlayer: ZepetoPlayer;
private Start() {
this.multiplay.RoomCreated += (room: Room) => {
this.room = room;
};
this.multiplay.RoomJoined += (room: Room) => {
room.OnStateChange += this.OnStateChange;
};
}
private OnStateChange(state: State, isFirst: boolean) {
// 当接收到第一个 OnStateChange 事件时,记录完整的状态快照。
if (isFirst) {
// [CharacterController] (本地) 当玩家实例在场景中完全加载时调用
ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
this.zepetoPlayer = myPlayer;
});
// [CharacterController] (本地) 当玩家实例在场景中完全加载时调用
ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
const isLocal = this.room.SessionId === sessionId;
if (!isLocal) {
const player: Player = this.currentPlayers.get(sessionId);
}
});
}
let join = new Map<string, Player>();
let leave = new Map<string, Player>(this.currentPlayers);
state.players.ForEach((sessionId: string, player: Player) => {
if (!this.currentPlayers.has(sessionId)) {
join.set(sessionId, player);
}
leave.delete(sessionId);
});
// [RoomState] 为进入房间的玩家创建玩家实例
join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
// [RoomState] 移除退出房间的玩家实例
leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
}
private OnJoinPlayer(sessionId: string, player: Player) {
console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);
this.currentPlayers.set(sessionId, player);
// 创建玩家
const isLocal = this.room.SessionId === player.sessionId;
ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, new SpawnInfo(), isLocal);
}
private OnLeavePlayer(sessionId: string, player: Player) {
console.log(`[OnRemove] players - sessionId : ${sessionId}`);
this.currentPlayers.delete(sessionId);
ZepetoPlayers.instance.RemovePlayer(sessionId);
}
}
现在,当玩家进入时,您可以确认ZEPETO角色已在您的屏幕上创建。
但是,玩家的移动尚未在屏幕上反映出来。

接下来,让我们继续同步位置。
步骤 3 : 通过获取其他玩家的信息进行同步
为了同步,每当玩家移动或采取某些行动时,他们必须将状态更改发送到服务器。
通过房间消息通信将包含状态更改的消息发送到服务器。
当服务器收到有关玩家状态更改的消息时,它会更新房间状态。
例如,假设您的本地玩家名为B。
当网络玩家A加入房间时,他们会在坐标x:0, y:0, z:0处实例化。
如果玩家A移动到位置x: -10, y: 0, z: 0,B实际上无法知道A正在移动。
这是因为两者在不同的设备上本地操作,因此在不同的客户端上。
因此,控制A的人需要通过房间消息将他们的移动传达给服务器。
一旦服务器收到这些信息,它会通知房间内的所有人A的实时位置。这样,B最终意识到A正在移动。
为了让服务器通知其他玩家,有两种方法:
- 使用房间消息广播。
- 更新房间状态,然后让客户端获取并应用房间状态。
本指南使用更新房间状态的第二种方法。

考虑到加载到场景中的 Zepeto 玩家具有 Zepeto 角色属性,您可以使用 Zepeto 角色功能来指挥移动到特定位置或发起跳跃。
为了让 B 的客户端可视化 A 移动到 x:-10, y:0, z:0,最直观的方法是使用 MoveToPosition()。此功能将把玩家移动到从服务器接收到的 A 的最新位置。
不仅仅是位置变化,手势、技能使用、物品收集和所有状态变化都需要服务器与客户端之间的通信以进行同步。
您需要实现同步,以保持网络中每个动作的和谐。
👍 同步概念总结
- 当本地玩家状态发生变化时,他们通过房间消息将其发送到服务器。
- 服务器通知除本地玩家之外的所有其他玩家状态变化。
- 在接收到状态变化消息后,客户端代码更新发送消息的玩家的状态。
步骤 3-1 : 完成位置同步的服务器脚本
在基本服务器脚本中,需要额外的实现来更新房间状态,每当收到来自本地玩家客户端的状态变化消息时。
import {Sandbox, SandboxOptions, SandboxPlayer} from "ZEPETO.Multiplay";
import {Player, Transform, Vector3} from "ZEPETO.Multiplay.Schema";
export default class extends Sandbox {
constructor() {
super();
}
onCreate(options: SandboxOptions) {
// 当房间对象被创建时调用。
// 处理房间对象的状态或数据初始化。
this.onMessage("onChangedTransform", (client, message) => {
const player = this.state.players.get(client.sessionId);
const transform = new Transform();
transform.position = new Vector3();
transform.position.x = message.position.x;
transform.position.y = message.position.y;
transform.position.z = message.position.z;
transform.rotation = new Vector3();
transform.rotation.x = message.rotation.x;
transform.rotation.y = message.rotation.y;
transform.rotation.z = message.rotation.z;
if (player) {
player.transform = transform;
}
});
this.onMessage("onChangedState", (client, message) => {
const player = this.state.players.get(client.sessionId);
if (player) {
player.state = message.state;
player.subState = message.subState; // 角色控制器 V2
}
});
}
async onJoin(client: SandboxPlayer) {
// 创建在 schemas.json 中定义的玩家对象并设置初始值。
console.log(`[OnJoin] sessionId : ${client.sessionId}, userId : ${client.userId}`)
const player = new Player();
player.sessionId = client.sessionId;
if (client.userId) {
player.zepetoUserId = client.userId;
}
// 使用 sessionId 管理玩家对象,这是客户端对象的唯一键值。
// 客户端可以通过将 add_OnAdd 事件添加到 players 对象来检查有关添加的玩家对象的信息。
this.state.players.set(client.sessionId, player);
}
async onLeave(client: SandboxPlayer, consented?: boolean) {
// 通过设置 allowReconnection,可以保持电路的连接,但在基本指南中立即清理。
// 客户端可以通过将 add_OnRemove 事件添加到 players 对象来检查有关已删除玩家对象的信息。
this.state.players.delete(client.sessionId);
}
}
步骤 3-2 : 完成位置同步的客户端脚本
基本客户端脚本中的关键实现是:
- 当服务器的房间状态改变时,自动调用 OnStateChange。
- 使用 SendMessageLoop(0.04) 函数,每0.04秒将本地玩家的位置和角色跳跃状态信息发送到服务器。
import {ZepetoScriptBehaviour} from 'ZEPETO.Script'
import {ZepetoWorldMultiplay} from 'ZEPETO.World'
import {Room, RoomData} from 'ZEPETO.Multiplay'
import {Player, State, Vector3} from 'ZEPETO.Multiplay.Schema'
import {CharacterState, SpawnInfo, ZepetoPlayers, ZepetoPlayer, CharacterJumpState} from 'ZEPETO.Character.Controller'
import * as UnityEngine from "UnityEngine";
export default class ClientStarterV2 extends ZepetoScriptBehaviour {
public multiplay: ZepetoWorldMultiplay;
private room: Room;
private currentPlayers: Map<string, Player> = new Map<string, Player>();
private zepetoPlayer: ZepetoPlayer;
private Start() {
this.multiplay.RoomCreated += (room: Room) => {
this.room = room;
};
this.multiplay.RoomJoined += (room: Room) => {
room.OnStateChange += this.OnStateChange;
};
this.StartCoroutine(this.SendMessageLoop(0.04));
}
// 在预定的间隔时间内将本地角色变换发送到服务器。
private* SendMessageLoop(tick: number) {
while (true) {
yield new UnityEngine.WaitForSeconds(tick);
if (this.room != null && this.room.IsConnected) {
const hasPlayer = ZepetoPlayers.instance.HasPlayer(this.room.SessionId);
if (hasPlayer) {
const character = ZepetoPlayers.instance.GetPlayer(this.room.SessionId).character;
this.SendTransform(character.transform);
this.SendState(character.CurrentState);
}
}
}
}
private OnStateChange(state: State, isFirst: boolean) {
// 当接收到第一个 OnStateChange 事件时,记录完整的状态快照。
if (isFirst) {
// [CharacterController] (Local) 当玩家实例在场景中完全加载时调用
ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
this.zepetoPlayer = myPlayer;
});
// [CharacterController] (Local) 当玩家实例在场景中完全加载时调用
ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
const isLocal = this.room.SessionId === sessionId;
if (!isLocal) {
const player: Player = this.currentPlayers.get(sessionId);
// [RoomState] 每当玩家实例的状态更新时调用。
player.OnChange += (changeValues) => this.OnUpdatePlayer(sessionId, player);
}
});
}
let join = new Map<string, Player>();
let leave = new Map<string, Player>(this.currentPlayers);
state.players.ForEach((sessionId: string, player: Player) => {
if (!this.currentPlayers.has(sessionId)) {
join.set(sessionId, player);
}
leave.delete(sessionId);
});
// [RoomState] 为进入房间的玩家创建玩家实例
join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
// [RoomState] 移除退出房间的玩家实例
leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
}
private OnJoinPlayer(sessionId: string, player: Player) {
console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);
this.currentPlayers.set(sessionId, player);
const spawnInfo = new SpawnInfo();
const position = this.ParseVector3(player.transform.position);
const rotation = this.ParseVector3(player.transform.rotation);
spawnInfo.position = position;
spawnInfo.rotation = UnityEngine.Quaternion.Euler(rotation);
const isLocal = this.room.SessionId === player.sessionId;
ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
}
private OnLeavePlayer(sessionId: string, player: Player) {
console.log(`[OnRemove] players - sessionId : ${sessionId}`);
this.currentPlayers.delete(sessionId);
ZepetoPlayers.instance.RemovePlayer(sessionId);
}
private OnUpdatePlayer(sessionId: string, player: Player) {
const position = this.ParseVector3(player.transform.position);
const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
var moveDir = UnityEngine.Vector3.op_Subtraction(position, zepetoPlayer.character.transform.position);
moveDir = new UnityEngine.Vector3(moveDir.x, 0, moveDir.z);
if (moveDir.magnitude < 0.05) {
if (player.state === CharacterState.MoveTurn)
return;
zepetoPlayer.character.StopMoving();
} else {
zepetoPlayer.character.MoveContinuously(moveDir);
}
if (player.state === CharacterState.Jump) {
if (zepetoPlayer.character.CurrentState !== CharacterState.Jump) {
zepetoPlayer.character.Jump();
}
if (player.subState === CharacterJumpState.JumpDouble) {
zepetoPlayer.character.DoubleJump();
}
}
}
private SendTransform(transform: UnityEngine.Transform) {
const data = new RoomData();
const pos = new RoomData();
pos.Add("x", transform.localPosition.x);
pos.Add("y", transform.localPosition.y);
pos.Add("z", transform.localPosition.z);
data.Add("position", pos.GetObject());
const rot = new RoomData();
rot.Add("x", transform.localEulerAngles.x);
rot.Add("y", transform.localEulerAngles.y);
rot.Add("z", transform.localEulerAngles.z);
data.Add("rotation", rot.GetObject());
this.room.Send("onChangedTransform", data.GetObject());
}
private SendState(state: CharacterState) {
const data = new RoomData();
data.Add("state", state);
if(state === CharacterState.Jump) {
data.Add("subState", this.zepetoPlayer.character.MotionV2.CurrentJumpState);
}
this.room.Send("onChangedState", data.GetObject());
}
private ParseVector3(vector3: Vector3): UnityEngine.Vector3 {
return new UnityEngine.Vector3
(
vector3.x,
vector3.y,
vector3.z
);
}
}

👍 提示
- 本指南仅实现位置同步。手势同步、对象同步等尚未实现。
- 原理对所有人都是相同的,但在每个必要时刻发送和接收房间消息的过程是必要的。
- 如果您想更方便地实现同步,请考虑使用多玩家同步模块。