ZEPETO Players

- ZepetoPlayers は、ZEPETOプレイヤーとZEPETOキャラクターの両方を制御するために設計されたマネージャー(シングルトン)クラスです。
- ZepetoPlayersをシーンに追加することで、ZEPETOプレイヤーを作成、削除、操作できます。
- ZepetoPlayer は、マルチプレイヤーの世界で直接制御するプレイヤーと他のプレイヤーを管理するために使用されるZEPETOキャラクターの個別のインスタンスを表します。
- マルチプレイヤーの世界で作成されたすべてのZepetoPlayerには一意のセッションIDが割り当てられ、このセッションIDを使用して管理されます。
- ZepetoPlayerはZepetoCharacterの属性を持っているため、ZepetoCharacterに関連する関数を使用して制御できます。
- ZepetoPlayerには3種類があります:
ZepetoPlayer | 説明 |
|---|---|
ローカルプレイヤー | ローカルユーザーによって直接制御されるZEPETOキャラクターインスタンスです。 - キャラクターコントローラー/Zepetoカメラコンポーネントが付属しています。 |
ネットワークプレイヤー(リモートプレイヤー) | マルチプレイコンテンツで読み込んで利用できるZEPETOキャラクターインスタンスです。 - キャラクターコントローラー/Zepetoカメラコンポーネントは付属していません。 |
ボットプレイヤー | マルチプレイコンテンツ用のZEPETOキャラクターインスタンスですが、実際のユーザーではなくボットによって制御されます。 プレイヤーが不足している状態でマルチプレイヤーワールドを開始する際や、プレイヤーがプレイ中に退出した場合の代替として使用されます。 - キャラクターコントローラー/Zepetoカメラコンポーネントは付属していません。 |
- 以下のガイドを参照してください。
- ZepetoCharacter は、World Sceneで読み込んで制御できるZEPETOキャラクターの基本インスタンスユニットです。
- ZepetoCharacterは、ZEPETOアプリを通じて作成されたアバターの外見を持っています。
ZepetoPlayersの追加
Hierarchyウィンドウで、ZEPETO → ZepetoPlayersタブを選択します。

シーンに追加できるようになりました。

ZepetoPlayersを追加するだけでは、Zepeto Playerをシーンに持ち込むことはできません。ZepetoPlayersのキャラクター作成APIを使用してスクリプトを実装する必要があります。
シーンでローカルプレイヤーだけを迅速に作成して試したい場合は、ガイドを参照してください:
Zepetoプレイヤーの名前とプロフィール写真を表示する方法のサンプルについては、ガイドを参照してください:
ZEPETOプレイヤーAPI
ZepetoPlayers APIに興味がある場合は、ドキュメントを参照してください:
このガイドは、主にマルチプレイヤーシナリオでのZEPETOプレイヤーの使用例をカバーしています。
ZEPETOプレイヤーを使用したマルチプレイ位置同期の実装
シングルプレイヤーの世界では、ローカルプレイヤーを作成する必要があり、ローカルプレイヤーのみが画面に表示されるため、同期は不要です。
しかし、マルチプレイの世界では、直接操作するローカルプレイヤーだけでなく、ネットワークプレイヤーと呼ばれる他のプレイヤーも画面に表示する必要があります。
ネットワークプレイヤーのすべてのアクション - 移動、ジャンプ、特定のジェスチャーを行う - は、あなたの画面に同じように表示される必要があります。
このプロセスは同期と呼ばれます。
同期スクリプトを実装しないと、ネットワークプレイヤーの外見や動きを見ることができません。
同期なしのマルチプレイワールドでは、他のクライアントがルームに入ったかどうかを知る唯一の方法はホームボタンを通じてです。

ステップ 1 : マルチプレイ環境の設定
マルチプレイチュートリアルビデオを通じて、基本設定と概念を理解することから始めることをお勧めします。
ステップ 2 : 他のプレイヤーを画面に表示する
あなたのローカルプレイヤーは、他の誰かのデバイス上でネットワークプレイヤーとして扱われます。
これは、あなたのローカルプレイヤーも同期のためにサーバーに情報を送信しなければならないことを意味します。
マルチプレイワールドに接続されているすべてのプレイヤーは、自分の情報を共有する必要があります。
マルチプレイルームに接続されているすべてのクライアントは、マルチプレイルームの状態データを共有します。
このルーム状態データは、Schemas.jsonで定義されたスキーマに従います。
(スキーマをデータ構造として考慮してください)
このガイドでは、ルームステートデータを通じてプレイヤーの位置を同期します。したがって、各プレイヤーの位置データを表すことができるスキーマを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"}
}
👍 ヒント
- サーバーとすべてのクライアントが共有すべき情報をすべて、マルチプレイルームステートに保存してください。
- レベル、経験値、スコアなど、各プレイヤーの個別データを保存するには、データストレージを使用してください。
サーバースクリプトは、別のプレイヤーがルームに入ったときにそれを認識し、その情報をクライアントに送信して、そのプレイヤーをシーンにロードすることができます。
ステップ 2-1 : 基本サーバースクリプト
プレイヤーがマルチプレイワールドのルームに入ると、onJoin()が呼び出されます。
サーバースクリプトでは、接続されたプレイヤーの情報をルームステートのプレイヤーに追加します。
また、プレイヤーがルームを退出すると、onLeave()が呼び出されます。
サーバースクリプトは、ルームステートのプレイヤーから退出したプレイヤーの情報を削除します。
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;
}
// セッションIDを使用してプレイヤーオブジェクトを管理します。これはクライアントオブジェクトのユニークなキー値です。
// クライアントは、プレイヤーオブジェクトに追加された情報を、プレイヤーオブジェクトにadd_OnAddイベントを追加することで確認できます。
this.state.players.set(client.sessionId, player);
}
async onLeave(client: SandboxPlayer, consented?: boolean) {
this.state.players.delete(client.sessionId);
}
}
ステップ 2-2 : 基本クライアントスクリプト
マルチプレイワールドを作成する際には、サーバーと通信するためのクライアントスクリプトが必要です。
以下は、マルチプレイ用の基本的なクライアントスクリプトの例です。
クライアント側では、currentPlayersマップデータ構造を使用して、表示するプレイヤーデータを管理します。
以下に重要なコードの行を示します:
ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
クライアントがサーバーのルームステートからプレイヤー情報を受信し、新しいプレイヤーがルームに参加すると、セッションIDが割り当てられます。作成されるプレイヤーのセッションIDが自分のセッションIDと一致する場合、そのプレイヤーはローカルと見なされます。この場合、プレイヤーは次のようにインスタンス化されます:isLocal = true, これはローカルプレイヤーであることを示しています。
その後、ローカルでないプレイヤーは isLocal = false. これにより、ローカルプレイヤーと非ローカルプレイヤーのすべての外観が画面に表示されることが保証されます。
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) {
// 最初のOnStateChangeイベントが受信されたとき、完全な状態スナップショットが記録されます。
if (isFirst) {
// [CharacterController] (ローカル) プレイヤーインスタンスがシーンに完全にロードされたときに呼び出されます
ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
this.zepetoPlayer = myPlayer;
});
// [CharacterController] (ローカル) プレイヤーインスタンスがシーンに完全にロードされたときに呼び出されます
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] ルームに入るプレイヤーのインスタンスを作成します
join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
// [RoomState] ルームを退出するプレイヤーのインスタンスを削除します
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 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);
}
}
今、プレイヤーが入ると、ZEPETOキャラクターが画面に作成されたことを確認できます。
しかし、プレイヤーの動きはまだ画面に反映されていません。

次に、位置の同期に進みましょう。
ステップ3:他のプレイヤーの情報を取得して同期する
同期のために、プレイヤーが動いたりアクションを取ったりするたびに、彼らはサーバーに状態の変化を送信しなければなりません。
状態の変化を含むメッセージをサーバーに送信するのは、ルームメッセージ通信を通じて行われます。
サーバーがプレイヤーの状態変化に関するメッセージを受信すると、ルームの状態が更新されます。
例えば、あなたのローカルプレイヤーがBという名前だとしましょう。
ネットワークプレイヤーAがルームに参加すると、彼らは座標x:0, y:0, z:0でインスタンス化されます。
プレイヤーAが位置x: -10, y: 0, z: 0に移動すると、BはAが移動していることを知る方法が実際にはありません。
これは、両者が別々のデバイスでローカルに操作されているため、別々のクライアントであるからです。
したがって、Aを操作している人は、ルームメッセージを介してサーバーに自分の動きを伝える必要があります。
サーバーがこの情報を受け取ると、ルーム内の全員にAのリアルタイムの位置を通知します。これにより、BはついにAが移動していることを認識します。
サーバーが他のプレイヤーに通知するためには、2つの方法があります:
- ルームメッセージのブロードキャストを使用する。
- ルームの状態を更新し、その後クライアントがルームの状態を取得して適用する。
このガイドでは、ルームの状態を更新する2番目の方法を利用します。

シーンにロードされたZepetoプレイヤーがZepetoキャラクター属性を持っているため、特定の場所への移動やジャンプを指示するためにZepetoキャラクター機能を使用できます。
BのクライアントがAの移動をx:-10, y:0, z:0に視覚化するための最も直感的なアプローチは、MoveToPosition()を使用することです。この関数は、サーバーから受信したAの最新の位置にプレイヤーを移動させます。
位置の変更だけでなく、ジェスチャー、スキルの使用、アイテムの収集、すべての状態変更は、同期のためにサーバーとクライアントの通信を必要とします。
ネットワーク全体で全てのアクションを調和させるために、同期を実装する必要があります。
👍 同期の概念の概要
- ローカルプレイヤーにステータス変更があると、Room Messageを使用してサーバーに送信します。
- サーバーは、ローカルプレイヤーを除くすべての他のプレイヤーにステータス変更を通知します。
- ステータス変更メッセージを受信すると、クライアントコードはメッセージを送信したプレイヤーのステータスを更新します。
ステップ 3-1 : 完全な位置同期を持つサーバースクリプト
基本的なサーバースクリプトでは、ローカルプレイヤーのクライアントからのステータス変更に関するメッセージを受信するたびに、ルームの状態を更新するための追加の実装が必要です。
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) {
// ルームオブジェクトが作成されたときに呼び出されます。
// ルームオブジェクトの状態またはデータの初期化を処理します。
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; // キャラクターコントローラー V2
}
});
}
async onJoin(client: SandboxPlayer) {
// schemas.jsonで定義されたプレイヤーオブジェクトを作成し、初期値を設定します。
console.log(`[OnJoin] sessionId : ${client.sessionId}, userId : ${client.userId}`)
const player = new Player();
player.sessionId = client.sessionId;
if (client.userId) {
player.zepetoUserId = client.userId;
}
// セッションIDを使用してプレイヤーオブジェクトを管理します。これはクライアントオブジェクトの一意のキー値です。
// クライアントは、プレイヤーオブジェクトに追加された情報をplayersオブジェクトにadd_OnAddイベントを追加することで確認できます。
this.state.players.set(client.sessionId, player);
}
async onLeave(client: SandboxPlayer, consented?: boolean) {
// allowReconnectionを設定することで、回路の接続を維持できますが、基本的なガイドではすぐにクリーンアップします。
// クライアントは、削除されたプレイヤーオブジェクトに関する情報をplayersオブジェクトにadd_OnRemoveイベントを追加することで確認できます。
this.state.players.delete(client.sessionId);
}
}
ステップ 3-2 : 完全な位置同期を持つクライアントスクリプト
基本的なクライアントスクリプトにおける重要な実装は次のとおりです:
- サーバーのルームステートが変更されたときにOnStateChangeを自動的に呼び出します。
- を使用して、SendMessageLoop(0.04)関数を使用して、ローカルプレイヤーの位置とキャラクターのジャンプ状態情報をサーバーに0.04秒ごとに送信します。
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
);
}
}

👍 ヒント
- このガイドは位置の同期のみを実装しています。ジェスチャーの同期、オブジェクトの同期などは実装されていません。
- 原則はすべて同じですが、必要な瞬間にルームメッセージを送受信するプロセスが必要です。
- 同期をより便利に実装したい場合は、マルチプレイ同期モジュールの使用を検討してください。