- ZepetoPlayers is a Manager (Singleton) class designed for controlling both the ZEPETO Player and ZEPETO Character.
- By adding ZepetoPlayers to a Scene, you can create, delete, and manipulate a ZEPETO Player.
- ZepetoPlayer represents an individual instance of a ZEPETO character used to manage both the player you control directly in a multiplayer world and other players.
- Every ZepetoPlayer created in a multiplayer world is assigned a unique session ID and is managed using this session ID.
- Because ZepetoPlayer possesses the attributes of ZepetoCharacter, it can be controlled using functions related to ZepetoCharacter.
- There are three types of ZepetoPlayer:
ZepetoPlayer | Description |
---|---|
Local Player | Represents a ZEPETO character instance directly controlled by the local user. - Attached with Character Controller/Zepeto Camera components. |
Network Player (Remote Player) | A ZEPETO character instance that can be loaded and utilized in multiplay content. - Does not have the Character Controller/Zepeto Camera components attached. |
Bot Player | A ZEPETO character instance for multiplay content, but controlled by a bot instead of a real user. Used as a substitute when starting a multiplayer world with insufficient players or if a player leaves during play. - Does not have the Character Controller/Zepeto Camera components attached. |
- Please refer to the following guide.
- ZepetoCharacter is a basic instance unit of ZEPETO character that can be loaded and controlled in the World Scene.
- ZepetoCharacter possesses the appearance of an avatar created through the ZEPETO app.
- Please refer to the following guide. [ZepetoCharacter]
Adding ZepetoPlayers
In the Hierarchy window, select ZEPETO → ZepetoPlayers tab.
You can now add it to your Scene.
Note that just adding ZepetoPlayers doesn't bring the Zepeto Player into the Scene. You need to implement a script using the character creation API of ZepetoPlayers.
If you wish to quickly create and try out only the Local Player in a Scene, refer to the guide:
- Please refer to the following guide. [Create a ZEPETO Character]
For a sample on how to display a Zepeto player's name and profile picture, check out the guide:
- Please refer to the following guide. [User Information]
ZEPETO Players API
If you're interested in the ZepetoPlayers API, refer to the documentation:
- Please refer to the following guide. [ZEPETO.Character.Controller API]
This guide primarily covers examples of using ZEPETO Players in multiplayer scenarios.
Implementing Multiplay Position Synchronization using ZEPETO Players
In a single-player world, only a Local Player needs to be created and since only the Local Player appears on the screen, synchronization is unnecessary.
However, in a multiplay world, not only the Local Player which you control directly but also other Players, referred to as Network Players, need to be displayed on the screen.
Every action of the Network Players - moving, jumping, and performing specific gestures - should appear identically on your screen.
This process is called synchronization.
Without implementing the synchronization script, you won't be able to see the Network Player's appearance or movement.
In a multiplay world without synchronization, the only way to know if another client has entered the Room is through the home button.
STEP 1 : Setting up the Multiplay Environment
It's recommended to start by understanding the basic settings and concepts of multiplay through a multiplay tutorial video.
- Please refer to the following guide.
STEP 2 : Displaying Other Players on Your Screen
Your Local Player is treated as a Network Player on someone else's device.
This means that even your Local Player must send information to the server for synchronization.
All Players connected to the multiplay world should share their information.
All Clients connected to the Multiplay Room share the Multiplay Room State data.
This Room State data follows the Schema defined in Schemas.json.
(Please consider Schema as a data structure)
In this guide, you will synchronize the player's position through the Room State data. Thus, define a Schema in Schemas.json that can represent the position data for each player.
{
"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"}
}
- Please refer to the following guide. [Multiplay Room State]
Tips
- Store all the information that servers and all clients should share in the Multiplay Room State.
- Save individual data for each Player, such as levels, experience points, scores, etc., using DataStorage.
The server script can recognize when another Player has entered the Room and can send that information to the client to load that Player into the Scene.
STEP 2-1 : Basic Server Script
When a Player enters the multiplay world's Room, onJoin() is called.
In the server script, add the information of the connected player to the Room State's Players.
Also, when a Player exits the Room, onLeave() is called.
The server script will remove the information of the player who left from the Room State's Players.
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;
}
// Manage the Player object using sessionId, a unique key value of the client object.
// The client can check the information about the player object added by set by adding the add_OnAdd event to the players object.
this.state.players.set(client.sessionId, player);
}
async onLeave(client: SandboxPlayer, consented?: boolean) {
this.state.players.delete(client.sessionId);
}
}
STEP 2-2 : Basic Client Script
When creating a multiplay world, a client script is needed to communicate with the server.
Below is an example of a fundamental client script for multiplayer play.
On the client side, we manage the player data to be displayed in our client using the currentPlayers
map data structure.
The pivotal line of code is illustrated below:
ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
When the client receives player information from the server's Room State and a new player joins the Room, a session ID is assigned. If the session ID of the player being created matches our own session ID, the player is considered local. In this case, the player will be instantiated with isLocal = true
, indicating that it's the local player.
Subsequently, players who aren't local are created with isLocal = false
. This ensures that the appearances of all players, both local and non-local, are rendered on the screen.
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) {
// When the first OnStateChange event is received, a full state snapshot is recorded.
if (isFirst) {
// [CharacterController] (Local) Called when the Player instance is fully loaded in Scene
ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
this.zepetoPlayer = myPlayer;
});
// [CharacterController] (Local) Called when the Player instance is fully loaded in Scene
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] Create a player instance for players that enter the Room
join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
// [RoomState] Remove the player instance for players that exit the room
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);
// Create 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);
}
}
Now, when a Player enters, you can confirm that the ZEPETO character is created on your screen.
But, the movement of the Player isn't yet reflected on the screen.
Let's move on to synchronizing the position next.
STEP 3 : Synchronizing by Fetching Other Player's Information
For synchronization, every time a Player moves or takes some action, they must send their status change to the server.
Sending a message containing their status change to the server is done through Room Message communication.
When the server receives a message about a Player's status change, it updates the Room State.
- Please refer to the following guide. [Multiplay Room Message]
For instance, let's consider that your local player is named B.
When a Network Player A joins the Room, they are instantiated at the coordinates x:0, y:0, z:0.
If Player A moves to the position x: -10, y: 0, z: 0, B has no actual way of knowing that A is moving.
This is because both are being operated locally on separate devices, hence on separate clients.
Thus, the person controlling A needs to communicate their movement to the server via a Room Message.
Once the server receives this information, it informs everyone present in the Room about A's real-time position. This way, B is finally aware that A is moving.
For the server to notify other players, there are two methods:
- Using Room Message broadcast.
- Updating the Room State, then having clients fetch and apply the Room State.
This guide utilizes the second method of updating the Room State.
Given that a Zepeto Player loaded into the Scene possesses Zepeto character attributes, you can employ Zepeto character functions to command movements to specific locations or initiate jumps.
For B's client to visualize A's movement to x:-10, y:0, z:0, the most intuitive approach is to use MoveToPosition(). This function will move the player to A's most recent position as received from the server.
Not just position changes, but gestures, skill usage, item collection, and all state changes necessitate server-client communication for synchronization.
You'll need to implement synchronization to keep every action in harmony across the network.
Synchronization Concept Summary
- When a Local Player has a status change, they send it to the server using Room Message.
- The server notifies all other players except the local player about the status change.
- Upon receiving the status change message, the client code updates the status of the Player who sent the message.
STEP 3-1 : Server Script with Completed Position Synchronization
In the basic server script, additional implementation is needed to update the Room State every time a message about a status change from the Local Player's client is received.
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) {
// Called when the Room object is created.
// Handle the state or data initialization of the Room object.
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; // Character Controller V2
}
});
}
async onJoin(client: SandboxPlayer) {
// Create the player object defined in schemas.json and set the initial value.
console.log(`[OnJoin] sessionId : ${client.sessionId}, userId : ${client.userId}`)
const player = new Player();
player.sessionId = client.sessionId;
if (client.userId) {
player.zepetoUserId = client.userId;
}
// Manage the Player object using sessionId, a unique key value of the client object.
// The client can check the information about the player object added by set by adding the add_OnAdd event to the players object.
this.state.players.set(client.sessionId, player);
}
async onLeave(client: SandboxPlayer, consented?: boolean) {
// By setting allowReconnection, it is possible to maintain connection for the circuit, but clean up immediately in the basic guide.
// The client can check the information about the deleted player object by adding the add_OnRemove event to the players object.
this.state.players.delete(client.sessionId);
}
}
STEP 3-2 : Client Script with Completed Position Synchronization
Key implementations in the basic client script are:
- Automatically call OnStateChange when the server's Room State changes.
- Using the
SendMessageLoop(0.04)
function, send the local player's position and character jump status information to the server every 0.04 seconds.
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));
}
// Send the local character transform to the server at the scheduled Interval Time.
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) {
// When the first OnStateChange event is received, a full state snapshot is recorded.
if (isFirst) {
// [CharacterController] (Local) Called when the Player instance is fully loaded in Scene
ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
this.zepetoPlayer = myPlayer;
});
// [CharacterController] (Local) Called when the Player instance is fully loaded in Scene
ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
const isLocal = this.room.SessionId === sessionId;
if (!isLocal) {
const player: Player = this.currentPlayers.get(sessionId);
// [RoomState] Called whenever the state of the player instance is updated.
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] Create a player instance for players that enter the Room
join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
// [RoomState] Remove the player instance for players that exit the room
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
);
}
}
Tips
- This guide only implements position synchronization. Gesture synchronization, object synchronization, etc., haven't been implemented.
- The principle is the same for all, but the process of sending and receiving Room Messages at every required moment is necessary.
- If you want to implement synchronization more conveniently, consider using a multiplay synchronization module.