2022年10月11日6 分

UnityでZPETOのワールドを作ってみる Vol.04 マルチプレイ編

最終更新: 2022年11月22日

前回は単独で動くプレイヤーを生成するところまで行きました。ZEPETOに投稿するには複数のユーザーが参加できる「マルチプレイ」の環境を構築する必要があります。今回はその基本的なつくり方のご紹介になります。

今回の完成イメージはこんな感じです。

ZEPETOワールド登録画面

5,Muiliplay(マルチプレイ)

ZepetoPlayer は Multiplay ワールドコンテンツでアバターを作成、削除、管理できるキャラクターインスタンスユニットです。各Player の UserId値 を通じて、アバターのインスタンスを Scene に Load および Unload することができます。

5.1,Multiplay Serverのインストール

まずは「Multiplay Server」を取得します。
 
projectのAsset内で Create ⇒ ZEPETO ⇒ Multiplay Server を選択。
 
Assetフォルダ内に「World.MultiPlay」というフォルダ生成されています。中にはいくつかのスクリプトファイルやフォルダが配置されているのが確認できると思います。
 
この中の index というスクリプトファイルと schemas という jsonファイルを変更します。

index.ts はサーバーのメインロジックコードで、マルチプレイの肝となる部分です。
 
index.ts ファイルを以下のように修正します。

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) {
 
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;
 
player.transform = transform;
 
});
 
this.onMessage("onChangedState", (client, message) => {
 
const player = this.state.players.get(client.sessionId);
 
player.state = message.state;
 
player.subState = message.subState;
 
});
 
}
 

 
async onJoin(client: SandboxPlayer) {
 
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;
 
}
 
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}`)
 
await storage.set("VisitCount", ++visit_cnt);
 
this.state.players.set(client.sessionId, player);
 
}
 

 
onTick(deltaTime: number): void {
 
}
 

 
async onLeave(client: SandboxPlayer, consented?: boolean) {
 
this.state.players.delete(client.sessionId);
 
}
 
}

もう一つ「schemas」これはサーバーとクライアントの通信用のデータ構造(jsonファイル)の様です。
 
以下のように変更します。

{
 
"State" : {"players" : {"map" : "Player"}},
 
"Player" : {"sessionId" : "string","zepetoHash" : "string","zepetoUserId" : "string","transform" : "Transform","state" : "number","subState" : "number"},
 
"Transform" : {"position" : "Vector3","rotation" : "Vector3"},
 
"Vector3" : {"x" : "number","y" : "number","z" : "number"}
 
}
 

「schemas」をInspectorで確認すると以下のような状態です。

5.2,テストサーバーの実行

作成者が開発中にサーバー/クライアントをテストするには、ローカルサーバー環境をの構築が必要です。その環境の動作を確認するには「Multiplay Server」を呼び出します。
 
上部メニューのWindow から ZEPETO ⇒ Multiplay Servet を選択し、状態ウィンドウを表示させます。

Unityエディタ上部のゼペットアイコンの右側のボタンを押すことで、状態ウィンドウの情報が更新されます。

さらに、Publishの隣の▼ボタンから「Play with Multiplay Server」が有効にします。チェックが入っていればOK、入っていなければクリックしてチェックを入れておきます。

5.3,クライアント接続

Hierachy で GameObject を作成、「MultiPlay」という名前を付けておきます。
 
さらにInspectorから AddCompornent で「ZepetoWorldMultiplay」コンポーネントを追加します。
 
追加されたZepetoWorldMultiplayコンポーネントは、Multiplay Packageに自動的に接続されます。

この状態でプレイボタンをクリックすると、情報ウィンドウからクライアントアクセスログを確認することができます。

5.4,MultiPlay 呼び出し

MultiPlayの準備ができましたが、実際のプレイの前にこの環境を呼び出す必要があります。
 
Hierarchyで空の GameObject を生成し、名前を「ClientStarter」に変更します。
 
さらにInspectorから AddCompornent で「ZepetScript」をセットします。

続いてAssetフォルダの内部で Create から ZEPETO ⇒ TypeScript を作成し、名前を「ClientStarter」に指定しておきます。

ClientStarter にはPlayerの呼び出し、削除などのコードを追記します。サンプルコードを載せておきますが、必要に応じ使ってみてください。

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} from 'ZEPETO.Character.Controller'
 
import * as UnityEngine from "UnityEngine";
 

 
export default class Starter extends ZepetoScriptBehaviour {
 

 
public multiplay: ZepetoWorldMultiplay;
 
private room: Room;
 
private currentPlayers: Map<string, Player> = new Map<string, Player>();
 
public playerPos: Vector3;
 

 
private Start() {
 
this.multiplay.RoomCreated += (room: Room) => {
 
this.room = room;
 
};
 

 
this.multiplay.RoomJoined += (room: Room) => {
 
room.OnStateChange += this.OnStateChange;
 
};
 

 
this.StartCoroutine(this.SendMessageLoop(0.1));
 
}
 

 
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 myPlayer = ZepetoPlayers.instance.GetPlayer(this.room.SessionId);
 
if (myPlayer.character.CurrentState != CharacterState.Idle)
 
this.SendTransform(myPlayer.character.transform);
 
}
 
}
 
}
 
}
 

 
private OnStateChange(state: State, isFirst: boolean) {
 
if (isFirst) {
 
ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
 
const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
 
myPlayer.character.OnChangedState.AddListener((cur, prev) => {
 
this.SendState(cur);
 
});
 
});
 

 
ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
 
const isLocal = this.room.SessionId === sessionId;
 
if (!isLocal) {
 
const player: Player = this.currentPlayers.get(sessionId);
 
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);
 
});
 

 
join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
 
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);
 
zepetoPlayer.character.MoveToPosition(position);
 
if (player.state === CharacterState.JumpIdle || player.state === CharacterState.JumpMove)
 
zepetoPlayer.character.Jump();
 
}
 

 
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);
 
this.room.Send("onChangedState", data.GetObject());
 
}
 

 
private ParseVector3(vector3: Vector3): UnityEngine.Vector3 {
 
return new UnityEngine.Vector3
 
(
 
vector3.x,
 
vector3.y,
 
vector3.z
 
);
 
}
 
}
 

作成したClientStarter.ts を ClientStarter の ZepetScript にセットすると、Public変数で宣言したMultiPlay をセットするスペースが生まれます。ここには事前にHierarchyに作成した「MultiPlay」をassignしておきます。

これで準備が整いました。最後にプロジェクトを起動させてみましょう。

下の図のように、事前に作ったスキンヘッドのアバターとご自身のアバターが同時に表示されればOKです。実際に動くのはご自身のアバターです。

最後は、不要なスキンヘッドアバターを非表示にしたいので、Hierarchy上にのGameObject の Inspectorからアクティブ状態のチェックをはずし、無効化しておきます。

これで下のようなワールドができました。
 
ご興味がありましたら「StoneCircle on Island」で検索してみて遊んでいってください!

さて、ここまでの動きでうまく動かなかった場合、ファイブボックスではオンライン等でもサポート致します。お気軽にお問い合わせください。お問い合わせは こちら から。

次回はカメラについてご説明いたします。

ファイブボックスでは実際に通って頂いての授業の他、ちょっとしたお困りごとに対するオンライン授業やオンラインサポートも行っております。
 
お困りごとのある方、ご興味がある方は、ぜひお問い合わせください。
 
お問い合わせは こちら から。
 
体験授業のお申込みは こちら から。

    5