Photon PUN2 を使ってみよう!

セキュリティツール

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を使うための準備をしましょう。以下のステップで進めます。

  1. Photonアカウントの作成
  2. アプリケーションの登録とApp IDの取得
  3. UnityプロジェクトへのPUN2パッケージのインポート
  4. PhotonServerSettingsの設定

1.1. アカウント作成とApp ID取得

Photon Cloudを利用するには、まずPhoton Engineの公式サイトでアカウントを作成する必要があります。

  1. Photon Engineの公式サイト (https://www.photonengine.com/) へアクセスし、サインアップ(アカウント登録)を行います。
  2. サインイン後、ダッシュボードに移動し、「新しいアプリを作成する」または「Create a New App」ボタンをクリックします。
  3. Photonの種別(Photon Type)で「Photon PUN」を選択します。
  4. アプリケーション名(App Name)を任意で入力します。(例: MyFirstPhotonGame)
  5. 必要に応じて説明(Description)やURLを入力し、「作成する」または「Create」ボタンをクリックします。
  6. 作成が完了すると、ダッシュボードのアプリケーションリストに表示されます。
  7. 表示されたアプリケーションの「アプリID (App ID)」をコピーしておきます。これは後ほどUnityで設定するために必要になります。

このApp IDは、あなたのUnityアプリケーションをPhoton Cloudサーバーに接続するための鍵🔑となります。

1.2. UnityプロジェクトへのPUN2インポート

次に、作成したUnityプロジェクトにPUN2パッケージをインポートします。

  1. Unityエディタを開き、対象のプロジェクトを開きます。
  2. メニューバーから「Window」>「Asset Store」を選択し、アセットストアを開きます。
  3. 検索バーで「PUN 2 – FREE」と検索します。
  4. 「PUN 2 – FREE」アセットを見つけ、「Download」または「Import」ボタンをクリックしてプロジェクトにインポートします。
  5. インポートが完了すると、「PUN Wizard」というウィンドウが表示されることがあります。

1.3. PhotonServerSettingsの設定

PUN2をインポートすると、プロジェクトとPhoton Cloudを接続するための設定を行います。

  1. PUN Wizardウィンドウが表示された場合、「AppId or Email」の欄に、先ほどコピーしたアプリID (App ID) を貼り付け、「Setup Project」ボタンをクリックします。
  2. Wizardウィンドウが表示されない、または閉じてしまった場合は、メニューバーから「Window」>「Photon Unity Networking」>「Highlight Server Settings」を選択します。
  3. Projectウィンドウで「PhotonServerSettings」アセットが選択されるので、Inspectorウィンドウを確認します。
  4. 「Pun Logging」を「Full」に設定すると、開発中に詳細なログが表示されデバッグに役立ちます。
  5. 「App Settings」セクションの「App Id Pun」フィールドに、コピーしたアプリIDを貼り付けます。
  6. 「App Version」は、異なるバージョンのゲーム間でマッチメイキングを分離するために使用します。開発中は「1」のままで構いません。
  7. 「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) コンポーネントを使用するのが簡単です。

  1. 同期したいGameObjectに `PhotonTransformView` コンポーネントを追加します。
  2. そのGameObjectに追加されている `PhotonView` コンポーネントの `Observed Components` リストに、追加した `PhotonTransformView` をドラッグ&ドロップで登録します。
  3. `PhotonTransformView` のInspectorで、同期したい項目(位置、回転、スケール)にチェックを入れます。
  4. 同期方法(Interpolate Option, Extrapolate Option)を設定します。Interpolate(補間)は過去のデータから現在の状態を滑らかに推測し、Extrapolate(補外)は未来の状態を予測します。一般的にはInterpolateがよく使われ、動きを滑らかに見せることができます。

これで、オーナーのクライアントでのTransformの変化が、他のクライアントにも(設定した同期方法で)反映されるようになります。カクカクした動きを減らし、滑らかな同期を実現できます。🏃‍♀️💨

3.4. アニメーションの同期

キャラクターアニメーションを同期するには、PhotonAnimatorView コンポーネントを使用します。

  1. 同期したいGameObject(Animatorコンポーネントが付いているもの)に `PhotonAnimatorView` コンポーネントを追加します。
  2. `PhotonView` の `Observed Components` に `PhotonAnimatorView` を登録します。
  3. `PhotonAnimatorView` のInspectorで、同期したいAnimatorのパラメータ(Trigger, Bool, Float, Int)を選択します。
  4. 同期の頻度(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を使うには、以下の手順を踏みます。

  1. RPCとして呼び出したいメソッドを定義し、そのメソッドに `[PunRPC]` 属性を付けます。このメソッドは `PhotonView` がアタッチされたGameObjectと同じGameObject上にあるスクリプト内に定義する必要があります。
  2. 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の様々なイベントに効率的に対応するコードを書くことができます。👍

基本的な使い方をマスターしたら、さらに高度な機能やテクニックを探求してみましょう。ここではいくつかの応用的なトピックを簡単に紹介します。

`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();
    }
}

Photonは、リアルタイムボイスチャット機能を提供する Photon Voice 2 という別パッケージも提供しています。PUN2と連携させることで、ゲーム内ボイスチャット機能を比較的簡単に実装できます。プレイヤー間のコミュニケーションを豊かにしたい場合に検討してみましょう。🗣️💬

Photon Voice 2を利用するには、別途アセットストアからパッケージをインポートし、`Photon Voice Network` や `Photon Voice View`, `Recorder`, `Speaker` などのコンポーネントを設定する必要があります。詳細はPhoton Voiceのドキュメントを参照してください。

Photon Cloudは世界中にサーバーリージョンを持っています。デフォルトではプレイヤーに最も近い(Pingが低い)リージョンに自動接続されますが、`PhotonNetwork.ConnectToRegion()` を使って特定のリージョンに接続することも可能です。特定の地域のプレイヤー同士でマッチングさせたい場合などに利用します。🌏

利用可能なリージョンコードはPhotonのドキュメントで確認できます(例: “jp” – 日本, “us” – アメリカ東部, “eu” – ヨーロッパなど)。

開発中は、Photonの動作を理解するためにログを確認することが重要です。`PhotonServerSettings` で `Pun Logging` を `Full` に設定すると、接続状態、送受信イベント、エラーなどの詳細な情報がUnityコンソールに出力されます。🐞

また、`PhotonNetwork` クラスにはデバッグに役立つプロパティやメソッドがあります。

  • `PhotonNetwork.NetworkClientState`: 現在の接続状態(例: ConnectedToMasterServer, Joining, Joined)
  • `PhotonNetwork.CurrentRoom`: 現在参加しているルームの情報(名前、プレイヤーリスト、カスタムプロパティなど)
  • `PhotonNetwork.PlayerList`: 現在のルームにいるプレイヤーのリスト
  • `PhotonNetwork.GetPing()`: 現在のサーバーへのPing値
  • `PhotonNetwork.NetworkStatisticsToString()`: 送受信バイト数などのネットワーク統計情報

これらの情報を活用して、問題の原因を特定しましょう。

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の学習も検討することをお勧めします。)

コメント

タイトルとURLをコピーしました