始めに
新年初投稿を務めさせていただきます、情熱開発部プログラム一課の辻と申します。
皆さん年末年始はいかがお過ごしになられましたか。私は友人と共にオンラインゲームで遊んでおりました。最近のゲームではオンラインで協力したり、対戦するのが当たり前になっていますね。
私、個人としては、ネットワークプログラムは複雑な印象を持っております。今回使用するPUN2は、そんな私でも比較的分かりやすいアセットでした。本記事では、PUN2を運用する上で使いこなすと便利な、カスタムプロパティを使ったデータの同期について紹介していきたいと思います。それでは、よろしくお願いいたします。
- 使用するUnityのversion :2021.3.29f1
- 使用するPUN2のversion:2.44
PUN2とカスタムプロパティについての説明
まずはPUN2とカスタムプロパティについて、ざっくり解説していきます。本記事はPUN2の導入方法を省略させていただきますが、ご了承ください。
PUN2とは
「Photon Unity Networking 2」のことです。始めに軽く触れてはいますが、Unityでオンライン機能を実装できる無料アセットとなっており、ネットワーク初心者の方でも問題なく使用できます。さらに詳しく知りたい場合は、PUN2の公式サイトの「はじめに」をご覧ください。
カスタムプロパティとは
PUN2で使用できる同期機能の一種です。PUN2ではカスタムプロパティ以外に、オブジェクト同期やRPC同期があります。これらとの違いについて、特徴を踏まえて次の章で触れていきたいと思います。
カスタムプロパティと他の同期方法との違い
この章では、カスタムプロパティはオブジェクト同期やRPC同期と何が違うのか、注意点を踏まえて説明していきます。
① オブジェクト同期
主にプレイヤーの位置や、カウントの同期といった高頻度な更新で使われる同期方法です。PUN2では主にPhotonViewTransformやPhotonAnimatorViewといったコンポーネントで使われています。独自実装をする際は以下の点に気を付ける必要が有ります。
注意点
- 情報が抜け落ちる可能性がある
- 通信量が増えるため、処理が重くなる
- 必要なタイミングのみ通信することは出来ない
② RPC同期
Remote Procedure Calls (リモートプロシージャルコール)の略です。同ルームのリモートクライアントに対してメソッド呼び出しをおこないます。もっとも汎用的な同期方法で、これだけで何とかなる場合があります。
注意点
- 途中参加のプレイヤーとの同期は苦手
- ゲームオブジェクトにPhotonViewコンポーネントをアタッチして使う
- 引数は特定の型でしか値を渡せない
defaultのRPCで使用できる型については、PhotonEngine公式サイトの「Photonでの直列化」をご覧ください。例として、Color型やVector4は引数で渡すことが出来ません。どうしてもRPCで使いたい場合は、独自でシリアライズを実装する必要が有ります。
③ カスタムプロパティ
ルームやプレイヤーに紐付けできるハッシュテーブル形式の一時的なパラメータです。途中参加のプレイヤーとも同期が可能であり、必要なタイミングで情報を取得したい場合に使用します。例としては、スコアの加算やサーバーの時間同期等が当てはまります。
注意点
- 文字列キーを使ってデータを紐づけているため、データ量がおおきくなりがち
- 複数プレイヤーが同時に値を変更しようとすると、並列処理周りのエラーが発生する
- プレイヤーとルームのどちらにパラメータを作成するかで、使用するクラスが変わる
プレイヤーかルームかの違いは、プレイヤー固有の値か、ルーム固有の値かで判別すると良いと思います。
プレイヤー固有の例 | プレイヤー名、HP、スコア…等 |
ルーム固有の例 | ゲーム時間、現在のプレイヤー数、ランキング…等 |
簡潔にまとめると以下のようになります。
オブジェクト同期 | 高頻度なやり取りが必要な情報で有効 |
RPC同期 | 特定のタイミングで同期でき、途中参加のプレイヤーと同期する必要が無い情報で有効 |
カスタムプロパティ | 特定のタイミングで同期でき、途中参加のプレイヤーと同期する必要が有る情報で有効 |
PUN2では、これらの特徴を把握したうえで、適切な同期方法を取捨選択していくこととなります。今回はルームのカスタムプロパティを使って、ゲーム画面を一斉に停止させる処理を組んでいこうと思います。
カスタムプロパティを使って同期してみる
事前準備
処理を組む前に、まずはカスタムプロパティの書式についてコードを見ながら触れていきます。本記事では、サーバへの接続やルームへの参加処理といった基礎的な処理は、都合上省かせていただきます。
public static class PhotonCustomProperties
{
private static Hashtable propHash = new Hashtable();
// ルームのカスタムプロパティ
public static bool SetRoom<T>(string keyName, T valus)
{
if (PhotonNetwork.InRoom) // ルームにいるかどうか
{
var room = PhotonNetwork.CurrentRoom;
if (room == null) return false; // ルームが存在していなかったらプロパティの作成 or 値の更新はしない
// ルームのカスタムプロパティを作成 or 値の更新
propHash[keyName] = valus;
room.SetCustomProperties(propHash);
propHash.Clear();
return true;
}
return false;
}
public static object GetRoom(string keyName)
{
if (PhotonNetwork.InRoom)
{
var room = PhotonNetwork.CurrentRoom;
if (room == null) return null;
// ルームのカスタムプロパティを取得
return room.CustomProperties[keyName];
}
return null;
}
//==========
// 以下省略
//==========
}
上記コードは、カスタムプロパティ周りをまとめたクラスとなってます。今回カスタムプロパティで作成する値は、サーバーの開始時間と、停止フラグの二種類です。どちらもルームのカスタムプロパティで実装した方が良いので、プレイヤー部分は省略しております。
3行目にあるHashtable
は、ExitGames.Client.Photon
の中にあるクラスで、Dictionary<object , object>
がベースとなっています。SetCustomProperties()
を使ってカスタムプロパティを新規作成 or 更新を行います。
値を取得したい場合は、32行目のように、取得したい値のkeyName
を指定して、CustomProperties[keyName]
で取得します。取得した値は必ずobject型で返ってくるため、任意の方にキャストをするか、is演算子の宣言パターン等で記載する必要が有ります。
また、作成 & 更新は重いため、同フレームで値を取得しようとすると、nullが入る可能性があります。必須ではないですが、nullチェックを忘れずに行うと不要なエラーを出さずに済みます。
実装
それでは実装に移ります。現在のゲーム画面は以下になっています。
ルームに接続するとタイマーが開始されます。ですが、あとから接続したプレイヤーは値が同期されていないので、このままではゲームになりません。
① 親がサーバーの開始時間を取得し、カスタムプロパティを作成する
// ルームに接続時
public override void OnJoinedRoom()
{
// ゲームの開始時間を記録
if (PhotonNetwork.IsMasterClient) // 親だった場合
{
SetRoomProperties(KeyName.startTime.ToString() , PhotonNetwork.ServerTimestamp);
SetRoomProperties(KeyName.timeFlag .ToString() , PhotonTimer .isTimerStart);
}
}
ルームのカスタムプロパティは一つだけ存在すればOKなので、上記のようにPhotonNetwork.IsMasterClient
を使って、親(マスタークライアント)が作成すれば大丈夫です。またPhotonNetwork.ServerTimestamp
は、サーバー内の現在の時刻を取得できるメソッドです。ついでに、一時停止を管理するフラグ用のカスタムプロパティも作成しておきます。
② 値を取得し、時間を計算
void Update()
{
currentTime = (int)GetRoomProperties(KeyName.startTime.ToString()); //開始時刻を取得
// 経過時間の計算
var elapsedTime = Mathf.Max(0f , (PhotonNetwork.ServerTimestamp - currentTime) / 1000f);
timeUI.text = elapsedTime.ToString("f1"); // UIに表示
}
先ほど作成したカスタムプロパティを取得し、経過時間を計算します。これらは各クライアントごとに行います。
なんとこれだけで、時間の同期が出来てしまいます。
③ 一斉停止処理の実装
void Update()
{
if ((bool)GetRoomProperties(KeyName.timeFlag.ToString()) == false) return; // 追加
currentTime = (int)GetRoomProperties(KeyName.startTime.ToString()); //開始時刻を取得
// 経過時間の計算
var elapsedTime = Mathf.Max(0f , (PhotonNetwork.ServerTimestamp - currentTime) / 1000f);
timeUI.text = elapsedTime.ToString("f1"); // UIに表示
}
public void OnPushButton()
{
if (!PhotonNetwork.IsMasterClient) return;
if (isTimerStart)
{
isTimerStart = false;
SetRoomProperties(KeyName.timeFlag.ToString() , isTimerStart); // 停止
}
else
{
isTimerStart = true;
SetRoomProperties(KeyName.timeFlag.ToString() , isTimerStart); // 再開
}
}
①で作成したフラグのカスタムプロパティを使って、Update内にチェックを仕込むだけです。今回はボタンで停止させるために、OnPushButton関数でSetRoomPropertiesを実行しています。上記コードでは時間だけ停止しますが、キャラクターが動く処理にも挟めば、以下gifのように一斉停止が可能です。
まとめ
いかがだったでしょうか。一時停止処理の実装はPunRPCでも実装できてしまうのですが、カスタムプロパティを使って実装した方が、コードが短く済みます。また、プレイヤーのカスタムプロパティは割と書かれている印象ですが、ルームのカスタムプロパティの記事は少ないように感じましたので、今回書かせていただきました。この記事が少しでも何方かのお力になれたら幸いです。
参考サイト
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。