Unity でマルチプレイヤーゲーム開発を始めるためのガイド
オンラインマルチプレイヤーゲームを作ってみたいけど、ネットワークの部分って難しそう… 🤔 と思っていませんか? そんなあなたにおすすめなのが Photon Unity Networking (PUN) です! PUNはUnity向けのマルチプレイヤーゲーム開発パッケージで、サーバーの管理や複雑なネットワーク処理を意識することなく、比較的簡単にオンライン機能を実装できます。
Photonにはいくつかの製品がありますが、この記事では特にUnity開発者にとって馴染み深く、多くのチュートリアルや情報が存在する PUN2 (Photon Unity Networking 2) の基本的な使い方に焦点を当てて解説していきます。PUN2は無料枠でも同時に20人まで接続可能なので、個人開発や学習用途にも最適です!🎉
※注意: Photonの開発元であるExit Gamesは、2023年以降、PUN2を「メンテナンスモード」とし、新機能の追加やUnity 2023以降へのメジャーアップデートは計画していないことを発表しています。新規プロジェクトでは後継の Photon Fusion の利用が推奨されていますが、PUN2も引き続き利用可能であり、既存プロジェクトや学習目的では依然として有用な選択肢です。
1. 準備編:Photon を始めよう!
まずはPhotonを使うための準備をしましょう。以下のステップで進めます。
- Photonアカウントの作成
- アプリケーションの登録とApp IDの取得
- UnityプロジェクトへのPUN2パッケージのインポート
- PhotonServerSettingsの設定
1.1. アカウント作成とApp ID取得
Photon Cloudを利用するには、まずPhoton Engineの公式サイトでアカウントを作成する必要があります。
- Photon Engineの公式サイト (https://www.photonengine.com/) へアクセスし、サインアップ(アカウント登録)を行います。
- サインイン後、ダッシュボードに移動し、「新しいアプリを作成する」または「Create a New App」ボタンをクリックします。
- Photonの種別(Photon Type)で「Photon PUN」を選択します。
- アプリケーション名(App Name)を任意で入力します。(例: MyFirstPhotonGame)
- 必要に応じて説明(Description)やURLを入力し、「作成する」または「Create」ボタンをクリックします。
- 作成が完了すると、ダッシュボードのアプリケーションリストに表示されます。
- 表示されたアプリケーションの「アプリID (App ID)」をコピーしておきます。これは後ほどUnityで設定するために必要になります。
このApp IDは、あなたのUnityアプリケーションをPhoton Cloudサーバーに接続するための鍵🔑となります。
1.2. UnityプロジェクトへのPUN2インポート
次に、作成したUnityプロジェクトにPUN2パッケージをインポートします。
- Unityエディタを開き、対象のプロジェクトを開きます。
- メニューバーから「Window」>「Asset Store」を選択し、アセットストアを開きます。
- 検索バーで「PUN 2 – FREE」と検索します。
- 「PUN 2 – FREE」アセットを見つけ、「Download」または「Import」ボタンをクリックしてプロジェクトにインポートします。
- インポートが完了すると、「PUN Wizard」というウィンドウが表示されることがあります。
1.3. PhotonServerSettingsの設定
PUN2をインポートすると、プロジェクトとPhoton Cloudを接続するための設定を行います。
- PUN Wizardウィンドウが表示された場合、「AppId or Email」の欄に、先ほどコピーしたアプリID (App ID) を貼り付け、「Setup Project」ボタンをクリックします。
- Wizardウィンドウが表示されない、または閉じてしまった場合は、メニューバーから「Window」>「Photon Unity Networking」>「Highlight Server Settings」を選択します。
- Projectウィンドウで「PhotonServerSettings」アセットが選択されるので、Inspectorウィンドウを確認します。
- 「Pun Logging」を「Full」に設定すると、開発中に詳細なログが表示されデバッグに役立ちます。
- 「App Settings」セクションの「App Id Pun」フィールドに、コピーしたアプリIDを貼り付けます。
- 「App Version」は、異なるバージョンのゲーム間でマッチメイキングを分離するために使用します。開発中は「1」のままで構いません。
- 「Fixed Region」は通常空欄のままで、プレイヤーに最も近いリージョンのサーバーへ自動接続されます。特定の地域でのみテストしたい場合は、リージョンコード(例: “jp”)を入力します。開発中にエディタとビルド版で接続するリージョンが異なり問題が発生する場合、一時的にリージョンを固定すると解決することがあります。
これで、UnityプロジェクトでPhoton PUN2を使用する準備が整いました!✨
2. 基本的な使い方:接続とルームへの参加
Photonを使ってマルチプレイヤー機能を実現するための最初のステップは、Photon Cloudサーバーへの接続と、「ルーム」への参加です。ルームは、プレイヤーが集まって一緒にゲームをプレイするための仮想的な空間です。
2.1. サーバーへの接続
Photon Cloudに接続するには、 `PhotonNetwork.ConnectUsingSettings()` メソッドを使用します。これは、先ほど設定した `PhotonServerSettings` の情報(App ID、App Version、リージョン設定など)を使って接続を試みます。
接続処理や切断、エラーなどのイベントに対応するために、Photonはコールバックという仕組みを提供しています。`MonoBehaviourPunCallbacks` クラスを継承すると、これらのコールバックメソッドを簡単にオーバーライドして実装できます。
以下は、サーバーへの接続と接続成功/失敗時のコールバックを実装する簡単な例です(C#)。
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
// MonoBehaviourPunCallbacksを継承してPhotonのコールバックを受け取る
public class NetworkManager : MonoBehaviourPunCallbacks
{
void Start()
{
Debug.Log("Connecting to Photon Network...");
// PhotonServerSettingsの設定を使ってPhoton Cloudに接続する
PhotonNetwork.ConnectUsingSettings();
}
// マスターサーバーへの接続成功時に呼ばれるコールバック
public override void OnConnectedToMaster()
{
Debug.Log("Connected to Master Server!");
// ここでロビーへの参加やルームへの参加処理を行うことが多い
// 例: PhotonNetwork.JoinLobby();
}
// 接続失敗時や切断時に呼ばれるコールバック
public override void OnDisconnected(DisconnectCause cause)
{
Debug.LogWarningFormat("Disconnected from Photon Network. Cause: {0}", cause);
// 必要に応じて再接続処理などを実装
}
}
このスクリプトをシーン内の適当なGameObject(例: “NetworkManager”)にアタッチしておくと、ゲーム開始時に自動的にPhotonへの接続が試みられます。
2.2. ロビーとルーム
マスターサーバーに接続できたら、次は他のプレイヤーとマッチングするための「ロビー」に参加したり、実際にゲームをプレイするための「ルーム」を作成または参加したりします。
- ロビー (Lobby): ルームを探したり、待機したりする場所です。デフォルトロビーや、特定の種類のゲーム用ロビーなどがあります。`PhotonNetwork.JoinLobby()` でデフォルトロビーに参加できます。
- ルーム (Room): 実際にプレイヤーが集まってゲームをプレイする仮想空間です。ルームには名前、最大プレイヤー数、パスワードなどの設定が可能です。
ルームへの参加/作成
ルームの操作には主に以下のメソッドを使用します。
メソッド | 説明 |
---|---|
PhotonNetwork.CreateRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby) |
指定した名前とオプションで新しいルームを作成します。同じ名前のルームが存在すると失敗します。 |
PhotonNetwork.JoinRoom(string roomName) |
指定した名前の既存のルームに参加します。ルームが存在しない、または満員の場合は失敗します。 |
PhotonNetwork.JoinRandomRoom() |
ロビー内で参加可能なルームにランダムに参加します。適切なルームが見つからない場合は失敗します。 |
PhotonNetwork.JoinOrCreateRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby) |
指定した名前のルームに参加しようと試み、存在しない場合は同じ名前とオプションで新しいルームを作成します。 |
ルームオプション (`RoomOptions`) では、ルームの公開/非公開、最大プレイヤー数 (`MaxPlayers`)、カスタムプロパティなどを設定できます。
ルーム関連のコールバック
ルームへの参加成功/失敗、他のプレイヤーの入退室などに対応するためのコールバックも用意されています。`MonoBehaviourPunCallbacks` を継承していれば、これらもオーバーライドして利用できます。
コールバックメソッド | 説明 |
---|---|
OnJoinedRoom() |
ルームへの参加または作成が成功した時に呼ばれます。 |
OnJoinRoomFailed(short returnCode, string message) |
`JoinRoom` が失敗した時に呼ばれます。 |
OnJoinRandomFailed(short returnCode, string message) |
`JoinRandomRoom` が失敗した時に呼ばれます。(この後 `CreateRoom` を呼ぶのが一般的) |
OnCreateRoomFailed(short returnCode, string message) |
`CreateRoom` が失敗した時に呼ばれます。(指定した名前のルームが既に存在するなど) |
OnPlayerEnteredRoom(Player newPlayer) |
自分以外のプレイヤーがルームに入室した時に呼ばれます。 |
OnPlayerLeftRoom(Player otherPlayer) |
自分以外のプレイヤーがルームから退出した時に呼ばれます。 |
OnLeftRoom() |
自分がルームから退出した時に呼ばれます。`PhotonNetwork.LeaveRoom()` を呼び出した後など。 |
コード例:ランダムマッチング
以下は、マスターサーバー接続後にランダムなルームへの参加を試み、失敗した場合は新しいルームを作成する例です。
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
public class MatchmakingManager : MonoBehaviourPunCallbacks
{
void Start()
{
// アプリケーションIDなどが設定されていれば自動接続
PhotonNetwork.ConnectUsingSettings();
}
public override void OnConnectedToMaster()
{
Debug.Log("Connected to Master! Trying to join a random room...");
// ランダムなルームに参加を試みる
PhotonNetwork.JoinRandomRoom();
}
// ランダムルームへの参加が失敗した場合に呼ばれる
public override void OnJoinRandomFailed(short returnCode, string message)
{
Debug.Log("Failed to join a random room. Creating a new room...");
// ルームオプションを設定(例:最大4人まで)
RoomOptions roomOptions = new RoomOptions();
roomOptions.MaxPlayers = 4;
// 新しいルームを作成する(ルーム名はnullにするとサーバーが自動生成)
PhotonNetwork.CreateRoom(null, roomOptions);
}
// ルームへの参加が成功した場合に呼ばれる
public override void OnJoinedRoom()
{
Debug.LogFormat("Successfully joined room: {0}", PhotonNetwork.CurrentRoom.Name);
Debug.LogFormat("Current players in room: {0}", PhotonNetwork.CurrentRoom.PlayerCount);
// ここでプレイヤーキャラクターの生成など、ゲーム開始処理を行う
// 例: PhotonNetwork.Instantiate("PlayerPrefab", Vector3.zero, Quaternion.identity);
}
public override void OnCreateRoomFailed(short returnCode, string message)
{
Debug.LogErrorFormat("Failed to create room. Code: {0}, Message: {1}", returnCode, message);
}
public override void OnPlayerEnteredRoom(Player newPlayer)
{
Debug.LogFormat("Player {0} entered the room.", newPlayer.NickName);
}
public override void OnPlayerLeftRoom(Player otherPlayer)
{
Debug.LogFormat("Player {0} left the room.", otherPlayer.NickName);
}
}
これで、基本的な接続からルームへの参加までの一連の流れを実装できました。次は、ゲームオブジェクトをネットワーク越しに同期する方法を見ていきましょう!🚀
3. オブジェクトの同期:みんなの世界を同じに
マルチプレイヤーゲームでは、各プレイヤーが見ているゲームの世界(プレイヤーの位置、状態、弾の発射など)を同期させる必要があります。PUN2では、PhotonView コンポーネントと関連コンポーネントを使ってこれを実現します。
3.1. PhotonView コンポーネント
ネットワーク上で同期したいGameObjectには、必ず PhotonView コンポーネントをアタッチする必要があります。これは、ネットワーク上の各オブジェクトを一意に識別し、データの送受信を管理するための中心的な役割を担います。
PhotonViewには重要な設定項目があります。
- Observed Components: どのコンポーネントのデータを同期するかを指定します。ここにTransformやAnimator、あるいは自作のスクリプトなどを追加します。
- Synchronization: 同期の頻度や方法を設定します。
- Reliable Delta Compressed: 変更があった場合に、差分データを確実に送信します。位置など連続的なデータに適していますが、通信量は多くなる可能性があります。
- Unreliable: 定期的にデータを送信しますが、到達保証はありません。頻繁に変化し、多少欠落しても問題ないデータ(例: 頻繁な位置更新)に適しています。
- Unreliable On Change: 変更があった場合にデータを送信しますが、到達保証はありません。
- Ownership Transfer: オブジェクトの制御権(どのプレイヤーがデータを送信するか)を他のプレイヤーに譲渡できるかどうかを設定します。
同期したいオブジェクト(プレイヤーキャラクター、敵、アイテムなど)のPrefabには、必ずPhotonViewコンポーネントを追加しておきましょう。
3.2. オブジェクトの生成:PhotonNetwork.Instantiate
ネットワーク同期が必要なオブジェクトをシーンに登場させるときは、Unity標準の `Instantiate` ではなく、`PhotonNetwork.Instantiate()` を使用します。
using Photon.Pun;
using UnityEngine;
public class PlayerSpawner : MonoBehaviourPunCallbacks
{
public GameObject playerPrefab; // InspectorでプレイヤーPrefabを設定
public override void OnJoinedRoom()
{
if (playerPrefab != null)
{
// プレイヤーPrefabをネットワーク経由で生成
// Prefabは"Resources"フォルダ以下に配置されている必要がある
PhotonNetwork.Instantiate(playerPrefab.name, new Vector3(0, 1, 0), Quaternion.identity);
Debug.Log("Player instantiated!");
}
else
{
Debug.LogError("Player Prefab is not assigned!");
}
}
}
重要: `PhotonNetwork.Instantiate()` で生成するPrefabは、プロジェクトの “Resources” フォルダ(またはそのサブフォルダ)内に配置されている必要があります。Photonが実行時にPrefabを名前で検索してロードするためです。
`PhotonNetwork.Instantiate()` で生成されたオブジェクトは、生成したクライアントがオーナー(Owner)となり、デフォルトではそのオブジェクトの状態(位置など)を他のクライアントに送信する責任を持ちます。他のクライアント側では、同じPrefabが自動的に生成され、オーナーからのデータを受信して状態を同期します。
生成されたオブジェクトが自分自身(ローカルプレイヤー)のものか、他のプレイヤー(リモートプレイヤー)のものかを判別するには、PhotonViewの `IsMine` プロパティを使用します。
using Photon.Pun;
using UnityEngine;
public class PlayerController : MonoBehaviourPunCallbacks
{
void Update()
{
// このGameObjectが自分のもの(ローカルプレイヤー)である場合のみ入力を受け付ける
if (photonView.IsMine)
{
ProcessInputs();
}
else
{
// 他のプレイヤーのオブジェクトであれば、入力処理は行わない
// (位置などはPhotonViewによって同期される)
}
}
void ProcessInputs()
{
// 移動や攻撃などの入力処理をここに記述
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
transform.Translate(movement * Time.deltaTime * 5.0f); // 例: 移動処理
}
}
3.3. Transformの同期
プレイヤーキャラクターなどの位置、回転、スケールを同期するには、PhotonTransformView (または PhotonTransformViewClassic) コンポーネントを使用するのが簡単です。
- 同期したいGameObjectに `PhotonTransformView` コンポーネントを追加します。
- そのGameObjectに追加されている `PhotonView` コンポーネントの `Observed Components` リストに、追加した `PhotonTransformView` をドラッグ&ドロップで登録します。
- `PhotonTransformView` のInspectorで、同期したい項目(位置、回転、スケール)にチェックを入れます。
- 同期方法(Interpolate Option, Extrapolate Option)を設定します。Interpolate(補間)は過去のデータから現在の状態を滑らかに推測し、Extrapolate(補外)は未来の状態を予測します。一般的にはInterpolateがよく使われ、動きを滑らかに見せることができます。
これで、オーナーのクライアントでのTransformの変化が、他のクライアントにも(設定した同期方法で)反映されるようになります。カクカクした動きを減らし、滑らかな同期を実現できます。🏃♀️💨
3.4. アニメーションの同期
キャラクターアニメーションを同期するには、PhotonAnimatorView コンポーネントを使用します。
- 同期したいGameObject(Animatorコンポーネントが付いているもの)に `PhotonAnimatorView` コンポーネントを追加します。
- `PhotonView` の `Observed Components` に `PhotonAnimatorView` を登録します。
- `PhotonAnimatorView` のInspectorで、同期したいAnimatorのパラメータ(Trigger, Bool, Float, Int)を選択します。
- 同期の頻度(Synchronize Parameters)を設定します(Disabled, Discrete, Continuous)。Discreteは値が変化した時だけ送信し、Continuousは常に送信します。TriggerはDiscreteが推奨されます。
これにより、歩行アニメーションや攻撃アニメーションなどが、ネットワーク越しに他のプレイヤーにも正しく表示されるようになります。💃🕺
3.5. カスタムデータの同期(IPunObservable)
TransformやAnimator以外のカスタムデータ(HP、スコア、持っているアイテムなど)を同期したい場合は、自作のスクリプトで `IPunObservable` インターフェースを実装します。
using Photon.Pun;
using UnityEngine;
public class PlayerHealth : MonoBehaviourPun, IPunObservable
{
public float currentHealth = 100f;
private float maxHealth = 100f;
// IPunObservableインターフェースの実装
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
// データの送信 (オーナーの場合)
// HPの値をストリームに書き込む
stream.SendNext(currentHealth);
}
else
{
// データの受信 (オーナー以外の場合)
// ストリームからHPの値を受け取り、自身のHPに反映する
this.currentHealth = (float)stream.ReceiveNext();
// 必要に応じてHPバーの表示などを更新
UpdateHealthBar();
}
}
// HPが減る処理(例)
public void TakeDamage(float amount)
{
// オーナーのみがHPを直接変更できるようにする (IsMineチェック)
if (photonView.IsMine)
{
currentHealth -= amount;
if (currentHealth < 0) currentHealth = 0;
UpdateHealthBar(); // 自身のHPバー更新
// 他のクライアントにもHPの変更を通知するために、
// PhotonViewがこのスクリプトをObserveしている必要がある
}
}
void UpdateHealthBar()
{
// HPバーのUI更新処理(省略)
Debug.LogFormat("Player {0} Health: {1}", photonView.ViewID, currentHealth);
}
}
このスクリプトをGameObjectに追加し、`PhotonView` の `Observed Components` に登録すると、`OnPhotonSerializeView` メソッドが定期的に(PhotonViewのSynchronization設定に応じて)呼ばれます。
- `stream.IsWriting` が true の場合: このオブジェクトのオーナーなので、同期したいデータを `stream.SendNext()` で送信します。
- `stream.IsWriting` が false の場合: オーナー以外のクライアントなので、`stream.ReceiveNext()` でデータを受信し、オブジェクトの状態に反映します。送受信の順番と型を一致させる必要があります。
`IPunObservable` を使うことで、ゲームに必要な様々なカスタムデータを効率的に同期させることができます。📊
4. データの送受信:RPC、カスタムプロパティ、RaiseEvent
オブジェクトの状態同期だけでなく、特定のタイミングで他のプレイヤーにアクションを伝えたり(弾を発射した、スキルを使ったなど)、プレイヤーやルームに関する付加的な情報を共有したりする必要もあります。PUN2ではそのためのいくつかの方法を提供しています。
4.1. RPC (Remote Procedure Call)
RPC(リモートプロシージャコール)は、特定のメソッドをネットワーク越しに他のクライアントで実行させるための機能です。「このプレイヤーがこのメソッドを実行したよ!」というのを他のプレイヤーに伝えるイメージです。🔫💥
RPCを使うには、以下の手順を踏みます。
- RPCとして呼び出したいメソッドを定義し、そのメソッドに `[PunRPC]` 属性を付けます。このメソッドは `PhotonView` がアタッチされたGameObjectと同じGameObject上にあるスクリプト内に定義する必要があります。
- RPCを呼び出したい箇所で、対象の `PhotonView` コンポーネントを取得し、`photonView.RPC()` メソッドを呼び出します。
`photonView.RPC()` の主な引数は以下の通りです。
- `methodName` (string): 呼び出したい `[PunRPC]` 属性付きのメソッド名を文字列で指定します。
- `target` (RpcTarget): RPCを誰に送るかを指定します。
RpcTarget.All
: ルーム内の全員(自分を含む)RpcTarget.Others
: 自分以外の全員RpcTarget.MasterClient
: 現在のマスタークライアントのみRpcTarget.AllBuffered
: 全員に送り、後から入室したプレイヤーにも送る(サーバーにバッファリングされる)RpcTarget.OthersBuffered
: 自分以外に送り、後から入室したプレイヤーにも送るRpcTarget.AllViaServer
: 全員に送るが、送信者自身もサーバー経由で受信する(実行順序が保証される)
- `parameters` (params object[]): RPCメソッドに渡したい引数を指定します。Photonがシリアライズ可能な型である必要があります(基本的な型、Vector3、Quaternion、PhotonPlayerなど)。
コード例:弾の発射通知
using Photon.Pun;
using UnityEngine;
public class WeaponController : MonoBehaviourPunCallbacks
{
public GameObject bulletPrefab;
public Transform firePoint;
void Update()
{
// 自分のキャラクターで、かつ発射ボタンが押されたら
if (photonView.IsMine && Input.GetButtonDown("Fire1"))
{
// RPCを呼び出して、他のプレイヤーにも発射を通知する
// RpcTarget.All なので自分も含めて全員でRPCメソッドが実行される
photonView.RPC("RPC_FireBullet", RpcTarget.All);
}
}
// RPCとして呼び出されるメソッド
[PunRPC]
void RPC_FireBullet()
{
Debug.LogFormat("RPC_FireBullet called on client {0}", PhotonNetwork.LocalPlayer.ActorNumber);
// 弾を生成する(Instantiateのみ。ネットワーク同期は弾自身が行う場合)
Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
// 注意:もし弾自体もネットワークオブジェクトとして同期したい場合は、
// ここでInstantiateする代わりに、弾用のRPCを用意するか、
// PhotonNetwork.Instantiateを使うなどの方法を検討する。
// この例では、単純に各クライアントで見た目上の弾を生成しているだけ。
}
// もし弾道計算などをサーバーで行いたい、あるいは発射情報を正確に同期したい場合
[PunRPC]
void RPC_FireBulletWithInfo(Vector3 position, Quaternion rotation, float initialSpeed, PhotonMessageInfo info)
{
Debug.LogFormat("RPC_FireBulletWithInfo called by {0}", info.Sender.NickName);
// 受け取った情報に基づいて弾を生成・発射
GameObject bullet = Instantiate(bulletPrefab, position, rotation);
// bullet.GetComponent<Rigidbody>().velocity = bullet.transform.forward * initialSpeed; // 例
}
void FireBulletReliably()
{
// 上記のRPC_FireBulletWithInfoを呼び出す例
float speed = 20f;
photonView.RPC("RPC_FireBulletWithInfo", RpcTarget.All, firePoint.position, firePoint.rotation, speed);
}
}
ヒント: RPCメソッドには `PhotonMessageInfo info` という引数を追加できます。これにより、RPCの送信者 (`info.Sender`) や送信時刻 (`info.SentServerTimestamp`) などの情報を取得できます。
注意: RPCは特定のイベントを伝えるのには便利ですが、頻繁に呼び出しすぎるとネットワーク負荷が高くなる可能性があります。位置同期のような継続的なデータの同期には `PhotonTransformView` や `IPunObservable` を使うのが適しています。
4.2. カスタムプロパティ (Custom Properties)
カスタムプロパティは、ルームまたはプレイヤーに関連付けられるキーと値のペア(Hashtable)です。これらは自動的に同期され、ルーム内の全プレイヤーが参照できます。GameObjectに直接関連しない情報(例:現在のマップ名、プレイヤーのスコア、選択キャラクター、準備完了状態など)を共有するのに便利です。🏷️
- ルームプロパティ: `PhotonNetwork.CurrentRoom.SetCustomProperties()` で設定します。ルーム全体に関わる情報(例: ゲームモード、マップ、現在のラウンド)に使います。
- プレイヤープロパティ: `PhotonNetwork.LocalPlayer.SetCustomProperties()` で自分のプロパティを設定します。各プレイヤー固有の情報(例: スコア、キル/デス数、選択したチーム、準備状態)に使います。
プロパティが変更されると、`MonoBehaviourPunCallbacks` を継承したスクリプトで以下のコールバックが呼ばれます。
- `OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged)`
- `OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps)`
コード例:プレイヤーの準備完了状態
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using ExitGames.Client.Photon; // Hashtableを使うために必要
public class PlayerReadyManager : MonoBehaviourPunCallbacks
{
private const string READY_PROPERTY_KEY = "IsReady"; // プロパティのキーを定数で定義
// プレイヤーリストのUIなどを更新するメソッド(例)
void UpdatePlayerList()
{
Debug.Log("--- Player List Update ---");
foreach (Player player in PhotonNetwork.PlayerList)
{
object isReadyValue;
// プレイヤーのカスタムプロパティから準備状態を取得
if (player.CustomProperties.TryGetValue(READY_PROPERTY_KEY, out isReadyValue))
{
Debug.LogFormat("Player {0} ({1}): Ready = {2}", player.ActorNumber, player.NickName, (bool)isReadyValue);
}
else
{
Debug.LogFormat("Player {0} ({1}): Ready = false (Not Set)", player.ActorNumber, player.NickName);
}
}
Debug.Log("------------------------");
}
// 準備完了/解除ボタンが押された時の処理(UIなどから呼び出す想定)
public void ToggleReadyState()
{
bool currentReadyState = false;
object isReadyValue;
if (PhotonNetwork.LocalPlayer.CustomProperties.TryGetValue(READY_PROPERTY_KEY, out isReadyValue))
{
currentReadyState = (bool)isReadyValue;
}
// 新しい準備状態を設定
bool newReadyState = !currentReadyState;
// カスタムプロパティを設定するためのHashtableを作成
Hashtable propertiesToSet = new Hashtable();
propertiesToSet.Add(READY_PROPERTY_KEY, newReadyState);
// 自分のプレイヤーカスタムプロパティを設定
PhotonNetwork.LocalPlayer.SetCustomProperties(propertiesToSet);
Debug.LogFormat("Set my ready state to: {0}", newReadyState);
}
// プレイヤープロパティが更新された時に呼ばれるコールバック
public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
Debug.LogFormat("Player {0} properties updated.", targetPlayer.NickName);
// 変更されたプロパティに準備状態が含まれているか確認
if (changedProps.ContainsKey(READY_PROPERTY_KEY))
{
Debug.Log("Ready state changed! Updating player list.");
UpdatePlayerList(); // プロパティが変更されたらプレイヤーリストを更新
}
}
// ルームに参加した時に初期状態を設定し、リストを更新
public override void OnJoinedRoom()
{
// 初期状態として未準備を設定
Hashtable initialProps = new Hashtable();
initialProps.Add(READY_PROPERTY_KEY, false);
PhotonNetwork.LocalPlayer.SetCustomProperties(initialProps);
UpdatePlayerList();
}
// 他のプレイヤーが入室した時もリストを更新
public override void OnPlayerEnteredRoom(Player newPlayer)
{
UpdatePlayerList();
}
// 他のプレイヤーが退出した時もリストを更新
public override void OnPlayerLeftRoom(Player otherPlayer)
{
UpdatePlayerList();
}
}
ヒント: カスタムプロパティは、ルーム作成時 (`CreateRoom`) やルーム参加時 (`JoinRoom`, `JoinOrCreateRoom`, `JoinRandomRoom`) にも初期値を設定できます。また、`RoomOptions` で `CustomRoomPropertiesForLobby` を設定すると、そのルームプロパティをロビーでのフィルタリング(マッチメイキング)に使用できます。
注意: `SetCustomProperties` を呼び出しても、プロパティは即座には反映されません。サーバーを経由して他のプレイヤーに通知され、コールバックが呼ばれた時点で値が更新されます。これは同期の整合性を保つためです。
4.3. RaiseEvent
RaiseEventは、RPCやカスタムプロパティよりも低レベルなイベント送信機能です。より柔軟なデータ構造を送信したい場合や、特定のイベントコードで処理を分岐させたい場合に使用します。📡
イベントを送信するには `PhotonNetwork.RaiseEvent()` を使用します。
- `eventCode` (byte): イベントの種類を識別するためのコード(0~199の範囲でユーザーが定義可能)。
- `eventContent` (object): 送信するデータ(Photonがシリアライズ可能な型)。
- `raiseEventOptions` (RaiseEventOptions): 受信者(Receivers)、キャッシュするかどうか(Caching)などを指定します。
- `sendOptions` (SendOptions): 送信の信頼性(Reliability)を指定します。
イベントを受信するには、`MonoBehaviourPunCallbacks` を継承したスクリプトで `IOnEventCallback` インターフェースの `OnEvent()` メソッドを実装(オーバーライド)します。
コード例:タイマー同期イベント
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using ExitGames.Client.Photon;
public class GameTimer : MonoBehaviourPunCallbacks, IOnEventCallback // IOnEventCallbackを追加
{
private const byte TIMER_SYNC_EVENT_CODE = 1; // イベントコードを定義
public float gameTimeRemaining = 60.0f;
private bool isTimerRunning = false;
void Start()
{
// マスタークライアントのみがタイマーを開始・同期する
if (PhotonNetwork.IsMasterClient)
{
StartTimer();
}
}
void Update()
{
if (isTimerRunning && PhotonNetwork.IsMasterClient) // マスタークライアントのみが時間経過を処理
{
gameTimeRemaining -= Time.deltaTime;
if (gameTimeRemaining <= 0)
{
gameTimeRemaining = 0;
isTimerRunning = false;
// ゲーム終了処理など
Debug.Log("Time's up!");
// 終了イベントを送信しても良い
}
// 定期的に現在の残り時間を他のクライアントに送信 (例: 1秒ごと)
// RaiseEventは負荷がかかる可能性があるので頻度に注意
if (Time.frameCount % 60 == 0) // 簡易的な1秒ごとの送信
{
SendTimerSyncEvent(gameTimeRemaining);
}
}
// UI表示更新など
UpdateTimerDisplay(gameTimeRemaining);
}
void StartTimer()
{
isTimerRunning = true;
gameTimeRemaining = 60.0f; // 初期時間を設定
SendTimerSyncEvent(gameTimeRemaining); // 開始時の時間を通知
Debug.Log("Timer started by Master Client.");
}
void SendTimerSyncEvent(float time)
{
object[] content = new object[] { time }; // 送信するデータ(残り時間)
RaiseEventOptions raiseEventOptions = new RaiseEventOptions { Receivers = ReceiverGroup.Others }; // 自分以外の全員に送信
SendOptions sendOptions = new SendOptions { Reliability = true }; // 確実に送信
PhotonNetwork.RaiseEvent(TIMER_SYNC_EVENT_CODE, content, raiseEventOptions, sendOptions);
Debug.LogFormat("Sent Timer Sync Event: {0}s", time);
}
// イベント受信時の処理
public void OnEvent(EventData photonEvent)
{
if (photonEvent.Code == TIMER_SYNC_EVENT_CODE)
{
object[] data = (object[])photonEvent.CustomData;
float receivedTime = (float)data[0];
this.gameTimeRemaining = receivedTime; // 受信した時間でタイマーを更新
isTimerRunning = receivedTime > 0; // 時間が0より大きければタイマー動作中とみなす
Debug.LogFormat("Received Timer Sync Event: {0}s", receivedTime);
UpdateTimerDisplay(gameTimeRemaining);
}
}
void UpdateTimerDisplay(float time)
{
// タイマーUIの表示更新(省略)
// Debug.LogFormat("Timer Display: {0:00.0}", time);
}
// MonoBehaviourPunCallbacksにはOnEventがないため、明示的に登録/解除が必要
// ただし、MonoBehaviourPunCallbacks は IOnEventCallback を内部的に処理してくれるので
// 以下の登録・解除は不要な場合が多い(自動で処理される)
/*
public override void OnEnable()
{
base.OnEnable(); // MonoBehaviourPunCallbacksのOnEnableを呼ぶ
PhotonNetwork.AddCallbackTarget(this); // イベントコールバックのターゲットとして登録
}
public override void OnDisable()
{
base.OnDisable(); // MonoBehaviourPunCallbacksのOnDisableを呼ぶ
PhotonNetwork.RemoveCallbackTarget(this); // 登録解除
}
*/
}
RaiseEventは最も自由度が高い方法ですが、RPCやカスタムプロパティで実現できる場合は、そちらの方がコードがシンプルになることが多いです。用途に応じて適切な方法を選びましょう。✅
5. コールバックとインターフェース:Photonからの通知を受け取る
これまでにもいくつか登場しましたが、Photon PUN2は様々なネットワークイベント(接続、切断、ルームへの参加/退出、プロパティの変更など)が発生した際に、特定のメソッドを呼び出すコールバックの仕組みを提供しています。これにより、イベントに応じた処理を簡単に実装できます。🔔
PUN2のコールバックは、いくつかのインターフェースによってグループ化されています。主なものを以下に示します。
インターフェース | 主なコールバック内容 | 代表的なメソッド例 |
---|---|---|
IConnectionCallbacks |
サーバーへの接続、切断、リージョンに関するイベント | OnConnectedToMaster , OnDisconnected , OnRegionListReceived |
ILobbyCallbacks |
ロビーへの参加/退出、ルームリストの更新、ロビー統計情報の更新 | OnJoinedLobby , OnLeftLobby , OnRoomListUpdate , OnLobbyStatisticsUpdate |
IMatchmakingCallbacks |
ルームの作成/参加/ランダム参加の成功/失敗、フレンドリストの更新 | OnCreatedRoom , OnCreateRoomFailed , OnJoinedRoom , OnJoinRoomFailed , OnJoinRandomFailed , OnFriendListUpdate |
IInRoomCallbacks |
ルーム内でのプレイヤーの入退室、マスタークライアントの変更 | OnPlayerEnteredRoom , OnPlayerLeftRoom , OnMasterClientSwitched |
IRoomCallbacks |
(IInRoomCallbacksを含む) ルームプロパティの更新 | OnRoomPropertiesUpdate |
IPlayerCallbacks |
(IInRoomCallbacksを含む) プレイヤープロパティの更新 | OnPlayerPropertiesUpdate |
IPunOwnershipCallbacks |
PhotonViewのオーナーシップ(制御権)の要求と移譲に関するイベント | OnOwnershipRequest , OnOwnershipTransfered , OnOwnershipTransferFailed |
IOnEventCallback |
`PhotonNetwork.RaiseEvent` で送信されたカスタムイベントの受信 | OnEvent |
IPunInstantiateMagicCallback |
`PhotonNetwork.Instantiate` でオブジェクトが生成された直後のイベント | OnPhotonInstantiate |
MonoBehaviourPunCallbacks の活用
これらのインターフェースを個別に実装することも可能ですが、多くの場合、`MonoBehaviourPunCallbacks` クラスを継承するのが最も簡単です。
`MonoBehaviourPunCallbacks` は `MonoBehaviour` を継承しており、上記の主要なコールバックインターフェース(`IConnectionCallbacks`, `IMatchmakingCallbacks`, `IInRoomCallbacks`, `ILobbyCallbacks` など)を既に実装済みです。
利点は以下の通りです。
- 必要なコールバックメソッドだけを `override` キーワードを使って実装すればよいため、コードがすっきりします。
- どのインターフェースにどのメソッドがあるかを細かく覚える必要がありません。
- コールバックの登録 (`PhotonNetwork.AddCallbackTarget(this)`) と解除 (`PhotonNetwork.RemoveCallbackTarget(this)`) が `OnEnable()` と `OnDisable()` で自動的に行われるため、登録忘れや解除忘れによる問題を避けられます。
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using ExitGames.Client.Photon; // Hashtable用
// MonoBehaviourPunCallbacks を継承
public class MyNetworkCallbacks : MonoBehaviourPunCallbacks
{
// 接続成功時の処理だけを実装したい場合
public override void OnConnectedToMaster()
{
Debug.Log("マスターサーバーに接続しました!");
// ロビーに参加するなど
PhotonNetwork.JoinLobby();
}
// ルーム参加成功時の処理だけを実装したい場合
public override void OnJoinedRoom()
{
Debug.LogFormat("ルーム '{0}' に参加しました!", PhotonNetwork.CurrentRoom.Name);
// プレイヤー生成など
SpawnPlayer();
}
// 他のプレイヤーが入室した時の処理だけを実装したい場合
public override void OnPlayerEnteredRoom(Player newPlayer)
{
Debug.LogFormat("{0} さんが入室しました。", newPlayer.NickName);
// UI更新など
UpdatePlayerListUI();
}
// プレイヤープロパティ更新時の処理だけを実装したい場合
public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
{
Debug.LogFormat("{0} さんのプロパティが更新されました。", targetPlayer.NickName);
if (changedProps.ContainsKey("Score")) // 例: スコアが更新されたら
{
UpdateScoreboard();
}
}
// --- 以下は実装例 ---
void SpawnPlayer()
{
Debug.Log("プレイヤーを生成します...");
// PhotonNetwork.Instantiate(...) など
}
void UpdatePlayerListUI()
{
Debug.Log("プレイヤーリストUIを更新します...");
}
void UpdateScoreboard()
{
Debug.Log("スコアボードを更新します...");
}
// 注意: IOnEventCallback や IPunInstantiateMagicCallback など、
// MonoBehaviourPunCallbacks がデフォルトで実装していないインターフェースの
// コールバックを使いたい場合は、別途インターフェースを追加し、
// 必要に応じて AddCallbackTarget/RemoveCallbackTarget を手動で行う必要があります。
// (ただし、IOnEventCallbackは多くの場合自動で処理されます)
}
このように、`MonoBehaviourPunCallbacks` を活用することで、Photonの様々なイベントに効率的に対応するコードを書くことができます。👍
6. 応用的なトピック
基本的な使い方をマスターしたら、さらに高度な機能やテクニックを探求してみましょう。ここではいくつかの応用的なトピックを簡単に紹介します。
6.1. オフラインモード
`PhotonNetwork.OfflineMode = true;` を設定することで、Photonのネットワーク機能を無効にし、シングルプレイヤーモードとして動作させることができます。これは、マルチプレイヤー用に書いたコード(`PhotonNetwork.Instantiate` や `photonView.IsMine` など)を、シングルプレイヤーモードでも再利用したい場合に非常に便利です。デバッグやテストにも役立ちます。🎮➡️👤
using Photon.Pun;
using UnityEngine;
public class OfflineModeSwitcher : MonoBehaviour
{
public void EnableOfflineMode()
{
PhotonNetwork.OfflineMode = true; // オフラインモードを有効化
Debug.Log("Offline Mode Enabled.");
// この後、通常通り ConnectUsingSettings の代わりに CreateRoom や JoinRoom を呼ぶと、
// ローカルで仮想的なルームが作成される。
// PhotonNetwork.CreateRoom("MyOfflineRoom");
}
public void DisableOfflineMode()
{
// オフラインモードからオンラインに戻るには、一度切断する必要がある
if (PhotonNetwork.IsConnected)
{
PhotonNetwork.Disconnect();
}
PhotonNetwork.OfflineMode = false;
Debug.Log("Offline Mode Disabled. Ready for online connection.");
// 再度 ConnectUsingSettings などでオンライン接続を行う
// PhotonNetwork.ConnectUsingSettings();
}
}
6.2. ボイスチャット (Photon Voice)
Photonは、リアルタイムボイスチャット機能を提供する Photon Voice 2 という別パッケージも提供しています。PUN2と連携させることで、ゲーム内ボイスチャット機能を比較的簡単に実装できます。プレイヤー間のコミュニケーションを豊かにしたい場合に検討してみましょう。🗣️💬
Photon Voice 2を利用するには、別途アセットストアからパッケージをインポートし、`Photon Voice Network` や `Photon Voice View`, `Recorder`, `Speaker` などのコンポーネントを設定する必要があります。詳細はPhoton Voiceのドキュメントを参照してください。
6.3. サーバーリージョンの選択
Photon Cloudは世界中にサーバーリージョンを持っています。デフォルトではプレイヤーに最も近い(Pingが低い)リージョンに自動接続されますが、`PhotonNetwork.ConnectToRegion()` を使って特定のリージョンに接続することも可能です。特定の地域のプレイヤー同士でマッチングさせたい場合などに利用します。🌏
利用可能なリージョンコードはPhotonのドキュメントで確認できます(例: “jp” – 日本, “us” – アメリカ東部, “eu” – ヨーロッパなど)。
6.4. デバッグとログ
開発中は、Photonの動作を理解するためにログを確認することが重要です。`PhotonServerSettings` で `Pun Logging` を `Full` に設定すると、接続状態、送受信イベント、エラーなどの詳細な情報がUnityコンソールに出力されます。🐞
また、`PhotonNetwork` クラスにはデバッグに役立つプロパティやメソッドがあります。
- `PhotonNetwork.NetworkClientState`: 現在の接続状態(例: ConnectedToMasterServer, Joining, Joined)
- `PhotonNetwork.CurrentRoom`: 現在参加しているルームの情報(名前、プレイヤーリスト、カスタムプロパティなど)
- `PhotonNetwork.PlayerList`: 現在のルームにいるプレイヤーのリスト
- `PhotonNetwork.GetPing()`: 現在のサーバーへのPing値
- `PhotonNetwork.NetworkStatisticsToString()`: 送受信バイト数などのネットワーク統計情報
これらの情報を活用して、問題の原因を特定しましょう。
6.5. Photon Fusion や Quantum との違い
PhotonにはPUN2以外にもネットワークソリューションがあります。
- Photon Fusion: PUN2の後継と位置付けられる、より高機能なネットワークエンジンです。ティックベースのシミュレーション、クライアントサイド予測、ラグ補償などの高度な機能を備え、アクション性の高いゲームに適しています。サーバー/ホスト権限モデルを採用しており、PUN2とはアーキテクチャが異なります。2022年頃から提供が開始され、新規プロジェクトではこちらが推奨されています。
- Photon Quantum: 決定論的(Deterministic)なシミュレーションエンジンです。プレイヤーの入力のみを同期し、全クライアントが全く同じシミュレーションを実行することで同期を保ちます。チート対策に強く、eスポーツタイトルなど、非常に高い精度と公平性が求められるゲームに適しています。開発には専門的な知識が必要です。
PUN2はシンプルで学習しやすい反面、FusionやQuantumはより高度な機能を提供します。プロジェクトの要件に合わせて適切なソリューションを選択することが重要です。🔍
7. まとめ
この記事では、Photon PUN2を使ったUnityでのマルチプレイヤーゲーム開発の基本的な流れを解説しました。
- アカウント作成からApp IDの設定、Unityへのインポートといった準備。
- `ConnectUsingSettings`, `JoinOrCreateRoom` などを使ったサーバー接続とルーム管理。
- `PhotonView`, `PhotonTransformView`, `IPunObservable` を利用したオブジェクトの状態同期。
- `RPC`, `カスタムプロパティ`, `RaiseEvent` を使ったデータやイベントの送受信。
- `MonoBehaviourPunCallbacks` を活用したコールバック処理。
PUN2を使えば、複雑なネットワークプログラミングの詳細を意識することなく、マルチプレイヤーゲームのコアな機能開発に集中できます。もちろん、より高度な機能や最適化を行うには、Photonの仕組みを深く理解する必要がありますが、最初の一歩としては非常に扱いやすいフレームワークです。🥳
今回紹介した内容は基本的な部分ですが、これを足がかりに、ぜひPhotonの公式ドキュメントやチュートリアル、コミュニティフォーラムなどを活用して、さらに学びを深めていってください。あなたのマルチプレイヤーゲーム開発が成功することを応援しています!🚀✨
(繰り返しになりますが、Photon Fusionが新しいスタンダードとなりつつあるため、長期的なプロジェクトや新規開発ではFusionの学習も検討することをお勧めします。)
コメント