Sample Project
Multiplay Sample
Summary
Summary | From creating a Multiplay server to a client, set up the environment needed to develop a Multiplay World. |
Difficulty | Intermediate |
Time Required | 1 Hour |
Part 1. Installing Multiplay
- From creating a Multiplay server to a client, set up the environment needed to develop a Multiplay World.
Part 2. Writing World Logic
- Learn about the Schema needed for communication between the server and client, and define Schema Types and Room State.
Caution
- Please note that the server script used in the video, which includes content related to hashCode, is no longer supported.
- Therefore, exclude the following code when writing.
if (client.hashCode) { player.zepetoHash = client.hashCode; }
Part 3. Writing World Logic 2
- Write World logic needed to sync from the player's location to the player's exit.
Part 4. Running the Server and Connecting to Multiplay
- Run the server and connect to Multiplay.
Part 5. Scripts
Schemas.json
{
"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"}
}
Server 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>();
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;
}
// [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);
}
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);
}
}
Client Code
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
);
}
}