Studio GuideWorld SDK Guide
Log In

Bot Player Creation Guide

Bot players are used to fill in when there are not enough people to start a multiplayer world, or when a player leaves during a world.

How the bot player behaves needs to be implemented for each content.

This guide describes the general method of creating a Bot player.

📘

The Bot Player creation guide is based on the multiplayer guide. [Multiplay Tutorial]


STEP 1: Create a Bot Player

1-1. Add a boolean value called IsBot to the multiplayer schema.


1-2. Define the function below to create a Bot player in the server script index.ts and call it at the desired point.

// The `CreateBot()` method is used to create a bot player with the given `userId`.
CreateBot(userId: string) {

    // Generate a session ID for the bot player using the provided `userId`.
    const sessionId = "Bot_" + userId;

  	// Check if a bot player with the same session ID already exists. If so, return without creating a duplicate.
    if (this.state.players.has(sessionId)) {
        return;
    }

    // Create a new `Player` object for the bot player.
    const player: Player = new Player();
    player.sessionId = sessionId;
    if (userId) {
        player.zepetoUserId = userId;
    }
    player.isBot = true;

  	// Add the bot player to the state's players map using the session ID as the key.
    this.state.players.set(player.sessionId, player);
    this._botMap.set(sessionId, player);
}

👍

Tips

  • The userId of a specific user is stored in advance for the Bot player character to be created.
  • You can check the UserId of a specific user by checking the userId of the client connecting to OnJoin on the server. After writing the script below in the server script, connect from the relevant world.
onJoin(client: SandboxPlayer) {
	console.log(client.userId);
}

STEP 2: Create a Bot player on the client

2-1. If you have the server create a Bot player at some point, the client will recognize it as a new player in OnJoinPlayer().

  • Create Project > Create > ZEPETO > TypeScript and rename it to BotPlayerManager.
  • Add logic in OnAddedPlayer() to create each player, and add logic to distinguish bot players and create their ZEPETO characters.
import { ZepetoCharacter, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { Room } from 'ZEPETO.Multiplay';
import { ZepetoScriptBehaviour } from 'ZEPETO.Script'
import { ZepetoWorldMultiplay } from 'ZEPETO.World';

export default class BotPlayerManager extends ZepetoScriptBehaviour {

    public zepetoWorldMultiplay: ZepetoWorldMultiplay;

 	// Private variables to store the current room and bot player data.
    private _room: Room;
    private _botMapData: Map<string, ZepetoCharacter> = new Map<string, ZepetoCharacter>();

    Start() {
		
	 	// Listen for the `RoomJoined` event from the `ZepetoWorldMultiplay` component.
        this.zepetoWorldMultiplay.RoomJoined += (room: Room) => {
        	this._room = room;
        }

	 	// Listen for the `OnAddedPlayer` event from `ZepetoPlayers.instance` to handle newly added players.
        ZepetoPlayers.instance.OnAddedPlayer.AddListener((userId: string) => {

		 	// Get the current player data from the room state using the `userId`.
            const currentPlayer = this._room.State.players.get_Item(userId);

		 	// Check if the player is a bot, and if so, set them as a bot player.
            if (currentPlayer.isBot) {
                this.SetBotPlayer(currentPlayer.sessionId);
            }
        });
    }
}

2-2. Write the SetBotPlayer function to add tags and synchronization components to bot players and create scripts to control them.

  • Set _botMapData to save bot player data in Map format to manage bot players.
// The `SetBotPlayer()` method is used to set a player as a bot.
SetBotPlayer(userId: string) {

    // Get the ZEPETO character associated with the bot player using their `userId`.
    const bot = ZepetoPlayers.instance.GetPlayer(userId).character;

    // Set the name of the character to the `userId` for identification.
    bot.gameObject.name = userId;

    // Store the bot player's data in the `_botMapData` map using their `userId` as the key.
		this._botMapData.set(userId, bot);
}

👍

Tips

You can add additional scripts or settings to SetBotPlayer() to control the behavior of bot players.


STEP 3: Create a Bot player button on the client

For worlds that require a certain number of players to start, sometimes there are not enough players and you have to wait a long time for the world to start.

In this case, you can start the world by adding a Bot player.

3-1. Register a function to execute CreateBot() when the server receives a message from the client in index.ts.

async OnCreate() {

	 	// Handle the "CreateBot" message, which creates a bot player with the given userId.
    this.onMessage("CreateBot", (client, message) => {
        this.CreateBot(message);
    });
}

3-2. In the client script BotPlayerManager.ts, write a function to send the "CreateBot" message to the server.

  • The way to execute a function is to send a message by pressing a button.
  • Send the user ID of the Bot player to be created as a string through the message.
public buttonCreateBot: Button;
public botPlayerId: string;

Start() {	

	 	// Add a click listener to the "Create Bot" button to send a message to create a bot player.
	 	this.buttonCreateBot.onClick.AddListener(() => {
            this._room.Send("CreateBot", this.botPlayerId);
    });
}

3-3. Now, when you run the server and the runtime, you can see that bot players are created when you press the button.


STEP 4: Start the world by adding a bot player

When there are not enough players to start the world, you can add bot players to initiate the world.

4-1. In the server script, add the following code during OnJoin to check the number of players and start the world when there are at least four players.

  • Add a function to check the number of players in CreateBot().
  • Add a counter for the number of plays in the StartWorld() function.
 async onJoin(client: SandboxPlayer) {

 	// Check the number of players in the room after a player joins.
	this.CheckPlayerNumber();
}

// The `CheckPlayerNumber()` method checks the number of players in the room and starts the world if there are at least four players.
CheckPlayerNumber() {

	// Print the current number of players in the room to the console.
	console.log(`player Number, ${this.state.players.size}`);

	// If there are at least four players in the room, start the world.
	if (this.state.players.size >= 4) {
 	this.StartWorld();
	}
}

// The `CreateBot()` method is used to create a bot player with the given `userId`.
CreateBot(userId: string) {

	// Generate a session ID for the bot player using the provided `userId`.
	const sessionId = "Bot_" + userId;

	// Check if a bot player with the same session ID already exists. If so, return without creating a duplicate.
 	if (this.state.players.has(sessionId)) {
	return;
	}

	// Create a new `Player` object for the bot player.
	const player: Player = new Player();
 	player.sessionId = sessionId;
	if (userId) {
		player.zepetoUserId = userId;
	}
	player.isBot = true;

	// Add the bot player to the state's players map using the session ID as the key.
	this.state.players.set(player.sessionId, player);
	this._botMap.set(sessionId, player);

	// Check the number of players in the room after adding the bot player.
	this.CheckPlayerNumber();
}


private playTime: number = 0;

// The `StartWorld()` method increments the play time and broadcasts the "StartWorld" message to all clients.
StartWorld() {
		this.playTime += 1;

    // Print a message indicating the start of the world and the current play time.
    console.log("Start World!");
    this.broadcast("StartWorld", this.playTime);
} 

👍

Tips

  • On the server, OnJoin is executed when a real player joins the room. So, when a Bot player is created via CreateBot and when a player enters via OnJoin, checkPlayerNumber() adds the number of people.

4-2. In the client script, BotPlayerManager.ts, write StartWorld(), which is executed when the message StartWorld is received from the server.

Start() {

	// Listen for the `RoomJoined` event from the `ZepetoWorldMultiplay` component.
	this.zepetoWorldMultiplay.RoomJoined += (room: Room) => {

 	// Add a message handler for the "StartWorld" message type.    
	this._room.AddMessageHandler("StartWorld", (playTime: number) => {
    	this.StartWorld(playTime);
  });   
}

// The `StartWorld()` method is called when the world starts with the provided `playTime`.
StartWorld(playTime: number) {

	// Print the world start message along with the `playTime`.
  console.log(`Start World : ${playTime}`);
}

4-3. At runtime, when there are more than 4 players, including Bot players, you can see a log called World Start pops up on the server console and on the client's console.


STEP 5: Synchronize Bot Player Position

Below is sample code that moves the added Bot players to the local player position and synchronizes the movement position.

5-1. First, write the code to move the Bot players when the message MoveBot is received from the client in index.ts of the server.

async OnCreate() {

 	// Handle the "MoveBot" message, which moves the bot player to the specified position.
	this.onMessage("MoveBot", (client, message) => {
        this.MoveBot(client, message);
 	});
}  

// The `MoveBot()` method moves the bot player to the specified position based on the received message.
MoveBot(client: SandboxPlayer, message: string) {
    // Parse the JSON message received from the client to extract the position information.
    const position = JSON.parse(message);

    // Create a new message object with the user's session ID and the parsed position data.
    const newMessage = {
        user: client.sessionId,
        positionX: position.x,
        positionY: position.y,
        positionZ: position.z
    }

    // Broadcast the "MoveBotToPosition" message to all clients with the new message data as a JSON string.
    this.broadcast("MoveBotToPosition", JSON.stringify(newMessage));
}

5-2. In the client script, BotPlayerManager.ts, write SendBotPosition() that sends the local player position to the server when buttonCallBot is pressed.

  • Then write code to move all Bot players to the location information included in the message when the message MoveBotToPosition is received from the server.
public buttonCallBot: Button;

Start(){    

	// Listen for the `RoomJoined` event from the `ZepetoWorldMultiplay` component.
    this.zepetoWorldMultiplay.RoomJoined += (room: Room) => {
    		this._room = room;

        // Add a message handler for the "StartWorld" message type.
        this._room.AddMessageHandler("StartWorld", (playTime: number) => {
            this.StartWorld(playTime);
        });
       
	// Add a click listener to the "Call Bot" button to send a message to send a bot player position.
	this.buttonCallBot.onClick.AddListener(() => {
				this.SendBotPosition();
	});
}  

// This method sends the position of the local player's character to the server for bot movement synchronization.
SendBotPosition() {

    // Get the position of the local player's character.
    const localPlayerPosition = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.transform.position;

    // Create a position object containing the x, y, and z coordinates of the local player's character.
    const position = {
        x: localPlayerPosition.x,
        y: localPlayerPosition.y,
        z: localPlayerPosition.z
    }

    // Convert the position object to a JSON string and send it to the server with the "MoveBot" message type.
    this._room.Send("MoveBot", JSON.stringify(position));
    }   

MoveBotToPosition(message) {

    // Parse the JSON message received from the client to extract the position information.
    const jsonMessage = JSON.parse(message);
    const position = new Vector3(jsonMessage.positionX, jsonMessage.positionY, jsonMessage.positionZ);

    // Move each bot character in the `_botMapData` map to the specified position with a small random offset on the x and z axes.
    this._botMapData.forEach((character: ZepetoCharacter) => {
        // The `MoveToPosition()` method is used to move the character to the specified position.
        // Here, a small random offset is added to the target position to create a natural-looking movement for the bots.
        character.MoveToPosition(position + new Vector3(Random.Range(0.5, 1), 0, Random.Range(0.5, 1)));
    });
}

5-3. Now if you create a Bot player at runtime and press the buttonCallBot button you should see the created Bot player move to the local player character position.


BotPlayerManager.ts full code

import { Random, Vector3 } from 'UnityEngine';
import { Button } from 'UnityEngine.UI';
import { ZepetoCharacter, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { Room } from 'ZEPETO.Multiplay';
import { ZepetoScriptBehaviour } from 'ZEPETO.Script'
import { ZepetoWorldMultiplay } from 'ZEPETO.World';

export default class BotPlayerManager extends ZepetoScriptBehaviour {

    // Public properties to reference necessary components and settings from the Inspector.
    public zepetoWorldMultiplay: ZepetoWorldMultiplay;
    public buttonCreateBot: Button;
    public buttonCallBot: Button;
    public botPlayerId: string;

    // Private variables to store the current room and bot player data.
    private _room: Room;
    private _botMapData: Map<string, ZepetoCharacter> = new Map<string, ZepetoCharacter>();

    Start() {

        // Listen for the `RoomJoined` event from the `ZepetoWorldMultiplay` component.
        this.zepetoWorldMultiplay.RoomJoined += (room: Room) => {
            this._room = room;

            // Add a message handler for the "StartWorld" message type.
            this._room.AddMessageHandler("StartWorld", (playTime: number) => {
                this.StartWorld(playTime);
            });
        }

        // Listen for the `OnAddedPlayer` event from `ZepetoPlayers.instance` to handle newly added players.
        ZepetoPlayers.instance.OnAddedPlayer.AddListener((userId: string) => {

            // Get the current player data from the room state using the `userId`.
            const currentPlayer = this._room.State.players.get_Item(userId);

            // Check if the player is a bot, and if so, set them as a bot player.
            if (currentPlayer.isBot) {
                this.SetBotPlayer(currentPlayer.sessionId);
            }
        });

        // Add a click listener to the "Create Bot" button to send a message to create a bot player.
        this.buttonCreateBot.onClick.AddListener(() => {
            this._room.Send("CreateBot", this.botPlayerId);
        });

        this.zepetoWorldMultiplay.RoomJoined += (room: Room) => {
            this._room = room;

            this._room.AddMessageHandler("MoveBotToPosition", (message: string) => {
                this.MoveBotToPosition(message);
            });
        }

        // Add a click listener to the "Call Bot" button to send a message to send a bot player position.
        this.buttonCallBot.onClick.AddListener(() => {
            this.SendBotPosition();
        });
    }

    // The `SetBotPlayer()` method is used to set a player as a bot.
    SetBotPlayer(userId: string) {

        // Get the ZEPETO character associated with the bot player using their `userId`.
        const bot = ZepetoPlayers.instance.GetPlayer(userId).character;

        // Set the name of the character to the `userId` for identification.
        bot.gameObject.name = userId;

        // Store the bot player's data in the `_botMapData` map using their `userId` as the key.
        this._botMapData.set(userId, bot);
    }

    // The `StartWorld()` method is called when the world starts with the provided `playTime`.
    StartWorld(playTime: number) {

        // Print the world start message along with the `playTime`.
        console.log(`Start World : ${playTime}`);
    }

    // This method sends the position of the local player's character to the server for bot movement synchronization.
    SendBotPosition() {

        // Get the position of the local player's character.
        const localPlayerPosition = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character.transform.position;

        // Create a position object containing the x, y, and z coordinates of the local player's character.
        const position = {
            x: localPlayerPosition.x,
            y: localPlayerPosition.y,
            z: localPlayerPosition.z
        }

        // Convert the position object to a JSON string and send it to the server with the "MoveBot" message type.
        this._room.Send("MoveBot", JSON.stringify(position));
    }

    // This method is called when the server receives the "MoveBot" message from a client and moves the bot characters to the specified position.
    MoveBotToPosition(message) {

        // Parse the JSON message received from the client to extract the position information.
        const jsonMessage = JSON.parse(message);
        const position = new Vector3(jsonMessage.positionX, jsonMessage.positionY, jsonMessage.positionZ);

        // Move each bot character in the `_botMapData` map to the specified position with a small random offset on the x and z axes.
        this._botMapData.forEach((character: ZepetoCharacter) => {
            // The `MoveToPosition()` method is used to move the character to the specified position.
            // Here, a small random offset is added to the target position to create a natural-looking movement for the bots.
            character.MoveToPosition(position + new Vector3(Random.Range(0.5, 1), 0, Random.Range(0.5, 1)));
        });
    }
}

index.ts server full code

import { Sandbox, SandboxOptions, SandboxPlayer } from "ZEPETO.Multiplay";
import { DataStorage } from "ZEPETO.Multiplay.DataStorage";
import { Player, Transform, Vector3 } from "ZEPETO.Multiplay.Schema";

export default class extends Sandbox {

    storageMap: Map<string, DataStorage> = new Map<string, DataStorage>();

    // Save the bot player's map data as _botMap
    private _botMap: Map<string, Player> = new Map<string, Player>();
    private playTime: number = 0;

    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) => {

            this.state.players.get(client.sessionId);

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

        // Handle the "CreateBot" message, which creates a bot player with the given userId.
        this.onMessage("CreateBot", (client, message) => {
            this.CreateBot(message);
        });

        // Handle the "MoveBot" message, which moves the bot player to the specified position.
        this.onMessage("MoveBot", (client, message) => {
            this.MoveBot(client, message);
        });
    }

    // The `CreateBot()` method is used to create a bot player with the given `userId`.
    CreateBot(userId: string) {

        // Generate a session ID for the bot player using the provided `userId`.
        const sessionId = "Bot_" + userId;

        // Check if a bot player with the same session ID already exists. If so, return without creating a duplicate.
        if (this.state.players.has(sessionId)) {
            return;
        }

        // Create a new `Player` object for the bot player.
        const player: Player = new Player();
        player.sessionId = sessionId;
        if (userId) {
            player.zepetoUserId = userId;
        }
        player.isBot = true;

        // Add the bot player to the state's players map using the session ID as the key.
        this.state.players.set(player.sessionId, player);
        this._botMap.set(sessionId, player);

        // Check the number of players in the room after adding the bot player.
        this.CheckPlayerNumber();
    }

    // The `CheckPlayerNumber()` method checks the number of players in the room and starts the world if there are at least four players.
    CheckPlayerNumber() {

        // Print the current number of players in the room to the console.
        console.log(`player Number, ${this.state.players.size}`);

        // If there are at least four players in the room, start the world.
        if (this.state.players.size >= 4) {
            this.StartWorld();
        }
    }

    // The `StartWorld()` method increments the play time and broadcasts the "StartWorld" message to all clients.
    StartWorld() {
        this.playTime += 1;

        // Print a message indicating the start of the world and the current play time.
        console.log("Start World!");
        this.broadcast("StartWorld", this.playTime);
    }

    // The `MoveBot()` method moves the bot player to the specified position based on the received message.
    MoveBot(client: SandboxPlayer, message: string) {
        // Parse the JSON message received from the client to extract the position information.
        const position = JSON.parse(message);

        // Create a new message object with the user's session ID and the parsed position data.
        const newMessage = {
            user: client.sessionId,
            positionX: position.x,
            positionY: position.y,
            positionZ: position.z
        }

        // Broadcast the "MoveBotToPosition" message to all clients with the new message data as a JSON string.
        this.broadcast("MoveBotToPosition", JSON.stringify(newMessage));
    }

    async onJoin(client: SandboxPlayer) {

        // Create the player object defined in schemas.json and set the initial value.
        console.log(`[OnJoin] sessionId : ${client.sessionId}, HashCode : ${client.hashCode}, userId : ${client.userId}`)

        const player = new Player();
        player.sessionId = client.sessionId;

        if (client.hashCode) {
            player.zepetoHash = client.hashCode;
        }
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }

        // [DataStorage] DataStorage Load of the entered Player
        const storage: DataStorage = client.loadDataStorage();

        this.storageMap.set(client.sessionId, storage);

        let visit_cnt = await storage.get("VisitCount") as number;
        if (visit_cnt == null) visit_cnt = 0;

        console.log(`[OnJoin] ${client.sessionId}'s visiting count : ${visit_cnt}`)

        // [DataStorage] Update Player's visit count and then Storage Save
        await storage.set("VisitCount", ++visit_cnt);

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

        // Check the number of players in the room after a player joins.
        this.CheckPlayerNumber();
    }

    onTick(deltaTime: number): void {
        //  It is repeatedly called at each set time in the server, and a certain interval event can be managed using deltaTime.
    }

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