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