Studio GuideWorld SDK Guide
Log In

ZEPETO Players

  • 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:
ZepetoPlayerDescription
Local PlayerRepresents 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 PlayerA 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.

📘


  • 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.

📘


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:

📘


For a sample on how to display a Zepeto player's name and profile picture, check out the guide:

📘


ZEPETO Players API

If you're interested in the ZepetoPlayers API, refer to the documentation:

📘


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.

멀티플레이 세팅만 하고 캐릭터 생성 및 동기화 스크립트를 구현하지 않았을 때의 모습

Appearance when only multiplay settings are applied without character creation and synchronization scripts


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.

📘


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"}
}

📘


👍

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.

내 화면에서는 자신의 로컬 플레이어만 움직이고, 다른 플레이어는 실제로 움직여도 반영되지 않는 모습

On my screen, only my local player moves, and other players appear static even if they are actually moving.


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.

📘


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
        );
    }
}

동기화 코드가 적용되어 내 로컬 플레이어 뿐 아니라 다른 플레이어의 움직임도 보여지는 모습

With the position synchronization code applied, not only my local player but also the movements of other players are displayed.


👍

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.