みなさんゲームクライアント開発で値をプレイヤー同士でどのように共有していますか?今回はPhoton PUNのカスタムプロパティについて紹介する.Photonは, Unity等でマルチプレイゲーム開発を行う際に広く使われているネットワークエンジンである. 特にUnity向けのPhoton PUN (Photon Unity Networking)は, 少ないコードでリアルタイム通信を実現できる強力なライブラリである.
カスタムプロパティは, Photonの便利な機能の一つで, ゲームのデータを簡単に全プレイヤーと共有できる. この記事では, 実際のプロジェクト例を使って, カスタムプロパティの基本から応用までを分かりやすく解説する.
カスタムプロパティとは何か?
カスタムプロパティは簡単に言うと「自動的に同期されるデータ」である. 具体的には:
- キーと値のペアで構成されるデータ (例: "health" = 100)
- ルーム, プレイヤー, オブジェクトにそれぞれ設定できる
- 設定すると自動的に全プレイヤーに同期される
- データが更新されたときに通知を受け取れる
これにより, サーバープログラミングなしでゲームの状態を簡単に共有できる.
カスタムプロパティの主な特徴
- 多様なデータ型をサポート: 数値, 文字列, 真偽値, 配列などを扱える
- 自動同期: 手動で同期処理を書く必要がない
- 簡単なAPI: 設定と取得が簡単
- 更新通知: データが変わったときのイベントを受け取れる
- 柔軟な使用方法: シンプルな値から複雑なデータ構造まで対応
実践的な実装例: GameManagerクラス
実際のゲーム開発でどのようにカスタムプロパティを使うか見ていこう. 以下の例は実際のプロジェクトで使われたGameManagerクラスを簡略化したものである.
基本的な実装構造
using Photon.Pun; // Photon PUN用の名前空間
using Photon.Realtime; // Photonの基本機能用の名前空間
using ExitGames.Client.Photon; // 低レベルAPIのための名前空間
using UnityEngine;
// カスタムプロパティを扱うクラスはMonoBehaviourPunCallbacksを継承するとよい
// これによりPhotonの各種コールバックを簡単に受け取れる
public class GameManager : MonoBehaviourPunCallbacks
{
// このクラスでゲームの状態を管理する
}
ゲーム状態の同期: 列挙型をカスタムプロパティとして使う
ゲームの進行状態をすべてのプレイヤーで同期するのはマルチプレイゲームでは重要である. カスタムプロパティを使えば簡単に実現できる.
// ゲームの状態を表す列挙型
public enum GameState
{
START, // ゲーム開始前
PLAY, // プレイ中
END // ゲーム終了
}
// ローカルでの状態管理変数
[Header("ゲームの状態管理用変数")]
private GameState state = GameState.START;
// 現在のゲーム状態を取得するメソッド
public GameState GetGameState()
{
// ルームにいる場合は、ルームのカスタムプロパティから取得
if (PhotonNetwork.InRoom
&& PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue("gameState", out object gs))
{
// objをGameState型にキャスト
return (GameState)(int)gs;
}
// ルームにいない、またはプロパティがなければローカル変数を返す
return state;
}
// ゲーム状態を設定し、全プレイヤーに共有するメソッド
public void SetGameState(GameState newState)
{
// まずローカル変数を更新
state = newState;
// ルームにいる場合は、カスタムプロパティとして設定して全プレイヤーに共有
if (PhotonNetwork.InRoom)
{
// Hashtableオブジェクトを作成し、キー"gameState"に値を設定
var props = new ExitGames.Client.Photon.Hashtable { ["gameState"] = (int)state };
// ルームのカスタムプロパティとして設定(これで自動的に同期される)
PhotonNetwork.CurrentRoom.SetCustomProperties(props);
}
}
文字列配列の同期: プレイヤー名のリスト管理
プレイヤー名のような文字列の配列も同期できる. 以下はプレイヤー名リストを管理する例である.
// ローカルで管理するプレイヤー名の配列
private string[] localPlayerNames = new string[0];
// 新しいプレイヤー名を追加するメソッド
public void AddLocalPlayerName(string name)
{
// 現在の配列から一時的なリストを作成
var list = new List<string>(localPlayerNames);
// 新しい名前を追加
list.Add(name);
// リストを配列に戻す
localPlayerNames = list.ToArray();
// カスタムプロパティとして更新して同期
UpdatePlayerNameListProperty();
}
// プレイヤー名リストをカスタムプロパティとして設定
void UpdatePlayerNameListProperty()
{
// ルームにいる場合のみ実行
if (PhotonNetwork.InRoom)
{
// プレイヤー名リストをカスタムプロパティとして設定
PhotonNetwork.CurrentRoom.SetCustomProperties(
new ExitGames.Client.Photon.Hashtable { ["playerNameList"] = localPlayerNames }
);
}
}
// すべてのプレイヤー名を取得するメソッド
public string[] GetAllPlayerNames()
{
// ルームのカスタムプロパティからプレイヤー名リストを取得
if (PhotonNetwork.InRoom
&& PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue("playerNameList", out object obj))
{
// 型に応じた処理
if (obj is string[] names)
{
// すでに文字列配列の場合
return names;
}
else if (obj is object[] objArray)
{
// object[]の場合は文字列に変換
string[] namesFromRoom = new string[objArray.Length];
for (int i = 0; i < objArray.Length; i++)
{
namesFromRoom[i] = objArray[i].ToString();
}
return namesFromRoom;
}
}
// 取得できなければローカルの配列を返す
return localPlayerNames;
}
ブール配列の同期: プレイヤーの状態管理
プレイヤーの生存状態のようなブール値の配列も同期できる.
// プレイヤーの死亡状態を管理するブール配列
private bool[] playerDeadStatus;
// プレイヤー死亡状態配列を初期化するメソッド
private void InitializePlayerDeadStatusArray()
{
// プレイヤー名のリストからプレイヤー数を取得
var names = GetAllPlayerNames();
// プレイヤー数分の配列を作成
playerDeadStatus = new bool[names.Length];
// すべて「生存中(false)」で初期化
for (int i = 0; i < playerDeadStatus.Length; i++)
playerDeadStatus[i] = false;
// カスタムプロパティとして設定して同期
UpdatePlayerDeadStatusProperty();
}
// 指定プレイヤーを死亡状態に設定するメソッド
public void SetPlayerDeadStatusTrue(int index)
{
// 配列が初期化済みで、インデックスが有効な範囲か確認
if (playerDeadStatus != null
&& index >= 0
&& index < playerDeadStatus.Length)
{
// 死亡状態に設定
playerDeadStatus[index] = true;
// カスタムプロパティとして更新して同期
UpdatePlayerDeadStatusProperty();
}
else
{
Debug.LogError($"Invalid index or not initialized: {index}");
}
}
// 死亡状態配列をカスタムプロパティとして更新
private void UpdatePlayerDeadStatusProperty()
{
if (PhotonNetwork.InRoom)
{
PhotonNetwork.CurrentRoom.SetCustomProperties(
new ExitGames.Client.Photon.Hashtable { ["playerDeadStatus"] = playerDeadStatus }
);
}
}
定期的な同期の実装
重要なデータは定期的に同期すると安全である. 以下はコルーチンを使った定期同期の例である.
// ローカルデータを定期的にルームに同期するコルーチン
private IEnumerator SyncCustomPropertiesCoroutine()
{
while (true) // 無限ループ
{
// 各種データをルームのカスタムプロパティとして更新
UpdatePlayerNameListProperty();
UpdatePlayerDeadStatusProperty();
UpdatePlayerScoreListProperty();
// 1秒待機
yield return new WaitForSeconds(1f);
}
}
private void Start()
{
// ゲーム開始時にコルーチンを起動
StartCoroutine(SyncCustomPropertiesCoroutine());
}
プロパティ更新通知のハンドリング
カスタムプロパティが変更されると、自動的に通知を受け取ることができる. MonoBehaviourPunCallbacks
を継承したクラスでは、OnRoomPropertiesUpdate
メソッドをオーバーライドして処理を実装する。
// ルームのカスタムプロパティが更新されたときに呼ばれるコールバック
public override void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable props)
{
// 更新された各プロパティに対する処理
// 生存プレイヤー数が更新された場合
if (props.ContainsKey("aliveCount"))
{
aliveCount = (int)props["aliveCount"];
Debug.Log("生存プレイヤー数が更新された: " + aliveCount);
}
// ゲーム状態が更新された場合
if (props.ContainsKey("gameState"))
{
state = (GameState)(int)props["gameState"];
Debug.Log("ゲーム状態が更新された: " + state);
}
// プレイヤー死亡状態が更新された場合
if (props.ContainsKey("playerDeadStatus"))
{
playerDeadStatus = props["playerDeadStatus"] as bool[];
Debug.Log("プレイヤー死亡状態が更新された: " +
string.Join(", ", playerDeadStatus ?? new bool[0]));
}
// プレイヤー名リストが更新された場合
if (props.ContainsKey("playerNameList"))
{
object propValue = props["playerNameList"];
// 型に応じた処理
if (propValue is string[] names)
{
localPlayerNames = names;
}
else if (propValue is object[] objArray)
{
// object[]から文字列配列に変換
localPlayerNames = new string[objArray.Length];
for (int i = 0; i < objArray.Length; i++)
{
localPlayerNames[i] = objArray[i].ToString();
}
}
Debug.Log("プレイヤー名リストが更新された: " + string.Join(", ", localPlayerNames));
}
// スコアリストが更新された場合
if (props.ContainsKey("playerScoreList"))
{
localPlayerScores = props["playerScoreList"] as float[];
Debug.Log("スコアリストが更新された: " +
string.Join(", ", localPlayerScores ?? new float[0]));
}
}
数値データの管理: スコアシステムの実装例
プレイヤーのスコアを管理する例である. 配列のサイズを動的に変更する方法も含まれている.
// ローカルで管理するスコア配列
private float[] localPlayerScores = new float[0];
// プレイヤーのスコアを設定するメソッド
public void SetLocalPlayerScore(int index, float score)
{
// index = -1 の場合は末尾に追加
if (index == -1)
{
index = localPlayerScores.Length;
}
// 配列サイズが足りない場合は拡張
if (index >= localPlayerScores.Length)
{
// 配列のサイズを変更
Array.Resize(ref localPlayerScores, index + 1);
}
// スコアを設定
localPlayerScores[index] = score;
// カスタムプロパティとして更新して同期
UpdatePlayerScoreListProperty();
}
// スコア配列をカスタムプロパティとして更新
private void UpdatePlayerScoreListProperty()
{
if (PhotonNetwork.InRoom)
{
PhotonNetwork.CurrentRoom.SetCustomProperties(
new ExitGames.Client.Photon.Hashtable { ["playerScoreList"] = localPlayerScores }
);
}
}
カスタムプロパティを活用したゲームロジック
カスタムプロパティの変更を監視することで、さまざまなゲームロジックを実装できる。以下はゲーム状態に応じた処理の例である。
private void Update()
{
// 【ゲーム開始処理】
// GODプレイヤーがスペースキーを押すとゲームスタート
if (GetGameState() == GameState.START // 現在は開始前状態
&& Input.GetKey(KeyCode.Space) // スペースキーが押された
&& !hasPlayerNameCreated) // 初期化済みでない
{
// ゲーム状態をPLAYに変更(全プレイヤーに通知される)
SetGameState(GameState.PLAY);
}
// 【ゲーム中の初期化処理】
// 全プレイヤーで実行される
if (GetGameState() == GameState.PLAY // プレイ状態になった
&& !hasPlayerNameCreated) // まだ初期化していない
{
// 生存プレイヤー数を設定
SetAliveCount(GetAllPlayerNames().Length);
// UIのセットアップ
SetupUI();
// プレイヤー死亡状態の初期化
InitializePlayerDeadStatusArray();
// 初期化済みフラグを立てる
hasPlayerNameCreated = true;
}
// 【ゲーム終了時の処理】
if (GetGameState() == GameState.END)
{
// 結果画面に遷移
LoadResultScene();
}
}
ルームへの接続時のデータ取得
プレイヤーがルームに参加したとき、既にルームにあるカスタムプロパティを取得することも重要である。
private void Start()
{
// ゲーム開始時の初期化
// Room に入室済みなら既存のプレイヤー名リストを取得
FetchPlayerNameListFromRoom();
// Room に入室済みなら既存のスコアリストを取得
FetchPlayerScoreListFromRoom();
}
// ルームからプレイヤー名リストを取得するメソッド
private void FetchPlayerNameListFromRoom()
{
// ルームに入室済みで、playerNameListプロパティが存在する場合
if (PhotonNetwork.InRoom
&& PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue("playerNameList", out object obj))
{
// 型に応じた処理
if (obj is string[] names)
{
// string[]型の場合
localPlayerNames = names;
}
else if (obj is object[] objArray)
{
// object[]型の場合は変換
localPlayerNames = new string[objArray.Length];
for (int i = 0; i < objArray.Length; i++)
{
localPlayerNames[i] = objArray[i].ToString();
}
}
Debug.Log("ルームからプレイヤー名リストを取得した: " + string.Join(", ", localPlayerNames));
}
}
Photonでよくある型変換の問題と解決法
Photonのカスタムプロパティでは、データの送受信時に型が変わることがある。特に配列はobject[]
として受け取られることが多いため、適切な型変換が必要である。
// カスタムプロパティから取得したデータの型変換例
if (obj is string[] names)
{
// すでに正しい型の場合はそのまま使用
localPlayerNames = names;
}
else if (obj is object[] objArray)
{
// object[]の場合は手動で変換
localPlayerNames = new string[objArray.Length];
for (int i = 0; i < objArray.Length; i++)
{
localPlayerNames[i] = objArray[i].ToString();
}
}
カスタムプロパティの実用例
実際のゲーム開発では、以下のようなシーンでカスタムプロパティが役立つ:
1. ゲーム状態管理
全プレイヤーでゲームの進行状況を同期する。
// ゲーム状態を変更し、全プレイヤーに通知
SetGameState(GameState.PLAY); // これでゲーム開始を全員に通知
// ゲーム状態を取得して条件分岐
if (GetGameState() == GameState.END) {
// ゲーム終了時の処理
}
2. プレイヤー情報管理
各プレイヤーの情報を共有する。
// プレイヤーのスコアを更新して全員に通知
SetLocalPlayerScore(playerIndex, newScore);
// プレイヤーの死亡をマークして全員に通知
SetPlayerDeadStatusTrue(playerIndex);
3. ゲームロジック実装
ゲームのルールに関わる処理を実装できる。
// プレイヤーを倒した時の処理
public void SetDecrementAliveCount()
{
// 生存者数を減らす
aliveCount--;
// 更新を全プレイヤーに通知
UpdateAliveCountProperty();
// 全員倒されたらゲーム終了
if (aliveCount <= 0)
{
SetGameState(GameState.END); // ゲーム終了を全員に通知
}
}
4. UIの同期更新
UI要素をゲームの状態に合わせて更新できる。
// プレイヤーの死亡状態に応じてUIを更新
private void SetupDeadUI()
{
// 死亡状態配列を取得
var deadStatus = GetPlayerDeadStatus();
// 各プレイヤーの状態に応じて処理
for (int i = 0; i < deadStatus.Length; i++)
{
if (deadStatus[i]) // 死亡している場合
{
// 対応するUIを死亡表示に
switch (i)
{
case 0: mrAttach.SetFirstPlayerDead(); break;
case 1: mrAttach.SetSecondPlayerDead(); break;
case 2: mrAttach.SetThirdPlayerDead(); break;
}
}
}
}
Photonのカスタムプロパティは、マルチプレイヤーゲーム開発において非常に便利なツールである。主な利点は:
- 簡単なデータ共有: サーバーサイドコードを書かずに済む
- 自動同期: 手動で送受信を管理する必要がない
- 柔軟なデータ型: 数値からオブジェクト配列まで様々なデータを扱える
- 拡張性: 必要に応じて複雑なシステムも構築できる
効果的な使い方は:
- ゲーム状態はルームのカスタムプロパティとして管理
- プレイヤー固有の情報はプレイヤーのカスタムプロパティで管理
- オブジェクト情報は必要に応じてPhotonViewのカスタムプロパティを使用
- 更新通知を適切に処理してUIやゲームロジックを更新
これらの概念を理解して実装することで,スムーズなマルチプレイヤーゲーム体験を実現できる.