Chapter 03

🔄 プレイヤーとネットワークオブジェクト

o8que
o8que
2021.04.14に更新

このチャプターでは、具体的な同期方法の解説を始める前に、同期を行うための重要な要素となるプレイヤーと、ネットワークオブジェクト(Networked Object)について詳しく説明します。

プレイヤー

ローカルプレイヤーの取得

プレイヤーの情報はPlayerクラスのオブジェクトで表現されています。自身のプレイヤーオブジェクトは、ローカルプレイヤーとしてPhotonNetworkからいつでも取得することができます。

// ローカルプレイヤーオブジェクトを取得する
var localPlayer = PhotonNetwork.LocalPlayer;

ルームに参加していない間でも、ローカルプレイヤーは取得できます。

プレイヤーのリストの取得

ルームに参加している間は、同じルームに参加しているプレイヤーオブジェクトの配列を取得できます。プレイヤーオブジェクトの配列は、ローカルプレイヤーを含むものと含まないものの2つがPhotonNetworkに用意されています。

// ルーム内のプレイヤーオブジェクトの配列(ローカルプレイヤーを含む)を取得する
var players = PhotonNetwork.PlayerList;
// ルーム内のプレイヤーオブジェクトの配列(ローカルプレイヤーを含まない)を取得する
var others = PhotonNetwork.PlayerListOthers;

ルームに参加していない間は、空の配列(new Player[0])が返されます。

よく使うフィールドとプロパティ

プレイヤーオブジェクトのフィールドとプロパティの中で、よく使うものを以下の表に示します。

名前 説明
player.ActorNumber プレイヤーのID
player.NickName プレイヤー名
player.IsLocal ローカルプレイヤーかどうか
player.IsMasterClient マスタークライアントかどうか
// ルーム内のプレイヤー全員のプレイヤー名とIDをコンソールに出力する
foreach (var player in PhotonNetwork.PlayerList) {
    Debug.Log($"{player.NickName}({player.ActorNumber})");
}

ローカルプレイヤーの一部のプロパティは、PhotonNetworkのショートカットからも利用できます。

名前 説明
PhotonNetwork.NickName ローカルプレイヤーの名前
PhotonNetwork.IsMasterClient ローカルプレイヤーがマスタークライアントかどうか
// ローカルプレイヤーの名前を設定する
PhotonNetwork.NickName = "Player";
// ローカルプレイヤーがマスタークライアントかどうかを判定する
if (PhotonNetwork.IsMasterClient) {
    Debug.Log("自身がマスタークライアントです");
}

プレイヤー参加・退出のコールバック

MonoBehaviourPunCallbacksを継承しているスクリプトは、他プレイヤーが同じルームに参加・退出した時のコールバックを受け取ることができます。他プレイヤーのオブジェクトは、コールバックの引数として渡されます。

using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class PlayerCallbacksSample : MonoBehaviourPunCallbacks
{
    // 他プレイヤーがルームへ参加した時に呼ばれるコールバック
    public override void OnPlayerEnteredRoom(Player newPlayer) {
        Debug.Log($"{newPlayer.NickName}が参加しました");
    }

    // 他プレイヤーがルームから退出した時に呼ばれるコールバック
    public override void OnPlayerLeftRoom(Player otherPlayer) {
        Debug.Log($"{otherPlayer.NickName}が退出しました");
    }
}

ネットワークオブジェクト

インスタンスの生成

ネットワーク上で同期させるゲームオブジェクトは、ネットワークオブジェクトと呼ばれます。ネットワークオブジェクトのインスタンスを生成するには、ゲームオブジェクトにPhotonViewコンポーネントを追加して、そのプレハブを「Resouces」に入れ、PhotonNetwork.Instantiate()から生成処理を行います。

  1. ゲームオブジェクトにPhotonViewコンポーネントを追加する

  2. ゲームオブジェクトのプレハブを「Resources」フォルダーの中に入れる

  3. PhotonNetwork.Instantiate()からインスタンスを生成する

// "NetworkedObject"プレパブからネットワークオブジェクトを生成する
PhotonNetwork.Instantiate("NetworkedObject", Vector3.zero, Quaternion.identity);

所有権

ネットワークオブジェクトは所有権(Ownership)によってプレイヤーと紐づきます。プレイヤーがネットワークオブジェクトを生成すると、そのプレイヤーはネットワークオブジェクトの生成者(Creator)となり、デフォルトの所有者(Owner)かつ管理者(Controller)となります。

  • 所有者 : ネットワークオブジェクトを所有しているプレイヤー
  • 生成者 : ネットワークオブジェクトを生成したプレイヤー
  • 管理者 : ネットワークオブジェクトを操作する権限を持つプレイヤー


Unityのエディター上で実行中に、インスペクターから所有権を確認できる

MonoBehaviourPunCallbacksを継承しているスクリプトは、photonViewプロパティを通して、所有権周りの様々な情報を取得できます。

名前 説明
photonView.IsMine 自身(ローカルプレイヤー)が管理者かどうか
photonView.Owner 所有者のプレイヤーオブジェクト
photonView.Controller 管理者のプレイヤーオブジェクト
photonView.OwnerActorNr 所有者のID(photonView.Owner.ActorNumberのショートカット)
photonView.CreatorActorNr 生成者のID
photonView.ControllerActorNr 管理者のID(photonView.Controller.ActorNumberのショートカット)

ネットワークオブジェクトは、通常のゲームオブジェクトと同じようにスクリプトを追加して自由に操作することができますが、photonView.IsMineで自身が管理者かどうかを判定してから処理しないと、自身のオブジェクトへの操作が他プレイヤーのオブジェクトにも影響してしまう可能性があるので注意しましょう。

using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class OwnershipSample : MonoBehaviourPunCallbacks
{
    private void Start() {
        // 自身が管理者かどうかを判定する
        if (photonView.IsMine) {
            // 所有者を取得する
            Player owner = photonView.Owner;
            // 所有者のプレイヤー名とIDをコンソールに出力する
            Debug.Log($"{owner.NickName}({photonView.OwnerActorNr})");
        }
    }
}

ルームオブジェクトの生成

ネットワークオブジェクトの生成者がルームから退出すると、基本的にそのネットワークオブジェクトは自動的に破棄されます。ネットワークオブジェクトをルームで共有して同期させたい場合には、マスタークライアントによって、ルームオブジェクトと呼ばれるルームに紐づいたネットワークオブジェクトを生成します。

// "RoomObject"プレハブからルームオブジェクトを生成する
if (PhotonNetwork.IsMasterClient) {
    PhotonNetwork.InstantiateRoomObject("RoomObject", Vector3.zero, Quaternion.identity);
}

マスタークライアント以外のプレイヤーは、PhotonNetwork.InstantiateRoomObject()を呼び出しても何も起こりません。

MonoBehaviourPunCallbacksを継承しているスクリプトは、photonViewプロパティを通して、ネットワークオブジェクトがルームオブジェクトかどうかを判定できます。

名前 説明
photonView.IsRoomView ルームオブジェクトかどうか

ルームオブジェクトは生成者を持たないので、プレイヤーがルームから退出することによって自動的に破棄されることはありません。また、ルームオブジェクトの管理者はマスタークライアントになるので、マスタークライアント側ではphotonView.IsMineがtrueになります。

(通常の)ネットワークオブジェクト ルームオブジェクト
所有者 生成者(デフォルト) null(デフォルト)
生成者 生成者 null
管理者 生成者(デフォルト) マスタークライアント(デフォルト)

🌶 「Resources」を使わずにネットワークオブジェクトを生成する

デフォルトではネットワークオブジェクトを生成するには、生成したいネットワークオブジェクトのプレハブを「Resources」に入れておく必要があります。PUN2では、このネットワークオブジェクトの生成・破棄を行う処理を自由に差し替えられるようになっていて、それを利用すると「Resources」を使わずにネットワークオブジェクトが生成できます。差し替えは簡単で、IPunPrefabPoolインターフェースを実装したクラスをPhotonNetwork.PrefabPoolに設定するだけです。

using Photon.Pun;
using UnityEngine;

// IPunPrefabPoolインターフェースを実装する
public class GamePlayerPrefabPool : MonoBehaviour, IPunPrefabPool
{
    [SerializeField]
    private GamePlayer gamePlayerPrefab = default;

    private void Start() {
        // ネットワークオブジェクトの生成・破棄を行う処理を、このクラスの処理に差し替える
        PhotonNetwork.PrefabPool = this;
    }

    GameObject IPunPrefabPool.Instantiate(string prefabId, Vector3 position, Quaternion rotation) {
        switch (prefabId) {
        case "GamePlayer":
            var player = Instantiate(gamePlayerPrefab, position, rotation);
            // 生成されたネットワークオブジェクトは非アクティブ状態で返す必要がある
            // (その後、PhotonNetworkの内部で正しく初期化されてから自動的にアクティブ状態に戻される)
            player.gameObject.SetActive(false);
            return player.gameObject;
        }
        return null;
    }

    void IPunPrefabPool.Destroy(GameObject gameObject) {
        Destroy(gameObject);
    }
}

さらに、ネットワークオブジェクトの生成・破棄を行う処理内でオブジェクトプールの仕組みを導入すると、ネットワークオブジェクトを使い回せるようになるため、比較的重いObject.Instantiate()の処理の負荷を抑えることもできます。

using System.Collections.Generic;
using Photon.Pun;
using UnityEngine;

public class GamePlayerPrefabPool : MonoBehaviour, IPunPrefabPool
{
    [SerializeField]
    private GamePlayer gamePlayerPrefab = default;

    private Stack<GamePlayer> inactiveObjectPool = new Stack<GamePlayer>();

    private void Start() {
        PhotonNetwork.PrefabPool = this;
    }

    GameObject IPunPrefabPool.Instantiate(string prefabId, Vector3 position, Quaternion rotation) {
        switch (prefabId) {
        case "GamePlayer":
            GamePlayer player;
            if (inactiveObjectPool.Count > 0) {
                player = inactiveObjectPool.Pop();
                player.transform.SetPositionAndRotation(position, rotation);
            } else {
                player = Instantiate(gamePlayerPrefab, position, rotation);
                player.gameObject.SetActive(false);
            }
            return player.gameObject;
        }
        return null;
    }

    void IPunPrefabPool.Destroy(GameObject gameObject) {
        var player = gameObject.GetComponent<GamePlayer>();
        // PhotonNetworkの内部で既に非アクティブ状態にされているので、以下の処理は不要
        // player.gameObject.SetActive(false);
        inactiveObjectPool.Push(player);
    }
}

その際、使い回されるネットワークオブジェクトのスクリプトでUnityのイベント関数を使っている場合には、少し注意が必要です。オブジェクト生成後に一度しか呼ばれないAwake()やStart()で何らかの初期化処理を行っていると、オブジェクトが使い回された時に正しく初期化処理が行われない可能性があるからです。

public class GamePlayer : MonoBehaviourPunCallbacks
{
    private void Awake() {
        // Object.Instantiateの後に一度だけ必要な初期化処理を行う
    }

    private void Start() {
        // 生成後に一度だけ(OnEnableの後に)呼ばれる、ここで初期化処理を行う場合は要注意
    }

    public override void OnEnable() {
        base.OnEnable();

        // PhotonNetwork.Instantiateの生成処理後に必要な初期化処理を行う
    }

    public override void OnDisable() {
        base.OnDisable();

        // PhotonNetwork.Destroyの破棄処理前に必要な終了処理を行う
    }
}

🌶 プレイヤーとネットワークオブジェクトの効率的な管理

PhotonNetwork.PlayersListやPhotonNetwork.PlayerListOthersは、アクセスするたびに配列のコピーを返します。取得した配列の要素数が意図せずに変わることはありませんが、頻繁にアクセスする場合には、パフォーマンス上の問題が発生する可能性があります。

また、PhotonNetwork.PhotonViewCollectionからは、ネットワークオブジェクトのイテレーターが取得できますが、複数の種類のネットワークオブジェクトを使用している場合は、ネットワークオブジェクトを種類別に分ける処理が必要になることがあるため、使い勝手はあまり良くありません。

// ルーム内のネットワークオブジェクトの名前とIDをコンソールに出力する
foreach (var photonView in PhotonNetwork.PhotonViewCollection) {
    Debug.Log($"{photonView.gameObject.name}({photonView.ViewID})");
}

上記の扱いづらさを解消するために、ネットワークオブジェクトを管理する独自クラスを作成するのがオススメです。

ここでは、チュートリアルで作成したサンプルプロジェクト(🎮)で、アバターのネットワークオブジェクトを管理するクラスを作成してみましょう。シーン上の空のゲームオブジェクトを作成して、アバターを管理するスクリプト(AvatarContainer)を追加します。


AvatarContainer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AvatarContainer : MonoBehaviour, IEnumerable<AvatarContainerChild>
{
    private List<AvatarContainerChild> avatarList = new List<AvatarContainerChild>();

    public AvatarContainerChild this[int index] => avatarList[index];
    public int Count => avatarList.Count;

    private void OnTransformChildrenChanged() {
        avatarList.Clear();
        foreach (Transform child in transform) {
            avatarList.Add(child.GetComponent<AvatarContainerChild>());
        }
    }

    public IEnumerator<AvatarContainerChild> GetEnumerator() {
        return avatarList.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }
}

アバターのネットワークオブジェクトには、アバターを管理するオブジェクトの子要素にするスクリプト(AvatarContainerChild)を追加しましょう。すると、アバターを管理するオブジェクトではOnTransformChildrenChanged()が呼ばれて、アバターのリストが更新されます。Ownerプロパティを定義しておけば、アバターのリストは、プレイヤーのリストとしても使えるようになります。

AvatarContainerChild.cs
using Photon.Pun;
using Photon.Realtime;

public class AvatarContainerChild : MonoBehaviourPunCallbacks
{
    public Player Owner => photonView.Owner;

    public override void OnEnable() {
        base.OnEnable();

        var container = FindObjectOfType<AvatarContainer>();
        if (container != null) {
            transform.SetParent(container.transform);
        }
    }
}

🌶 ネットワークオブジェクトの所有権の移譲

他プレイヤーが所有権を持つインスタンスから、所有権を取得(自分自身へ所有権を移譲)できるようにするには、まずPhotonViewの所有権オプションを変更しておく必要があります。

所有権オプション 説明
Fixed 所有権は取得できず、生成者が常に所有権を持つ(デフォルト)
Takeover 所有権を自由に取得できる
Request 所有権を取得するためには、所有者の許可が必要になる

所有権を取得したいインスタンスのPhotonViewでRequestOwnership()を呼び出すと、「Takeover」オプションを選択した時はすぐに所有権を取得し、「Request」オプションを選択した時は所有権のリクエストが行われます。

photonView.RequestOwnership();

IPunOwnershipCallbacksインターフェースを実装しているスクリプトは、所有権関連のコールバックを受け取ることができます。(MonoBehaviourPunCallbacksを継承してもコールバックは受け取れないので注意してください)
「Request」オプションを選択した時は、IPunOwnershipCallbacks.OnOwnershipRequest()に自身が所有権を持つインスタンスで所有権のリクエストが行われた際の処理(許可/拒否)を実装することで、所有権を移譲できるようになります。

// 所有権のリクエストが行われた時に呼ばれるコールバック
void IPunOwnershipCallbacks.OnOwnershipRequest(PhotonView targetView, Player requestingPlayer) {
    // 自身が所有権を持つインスタンスで所有権のリクエストが行われたら、常に許可して所有権を移譲する
    if (targetView.IsMine && targetView.ViewID == photonView.ViewID) {
        bool acceptsRequest = true;
        if (acceptsRequest) {
            targetView.TransferOwnership(requestingPlayer);
        } else {
            // リクエストを拒否する場合は、何もしない
        }
    }
}

// 所有権の移譲が行われた時に呼ばれるコールバック
void IPunOwnershipCallbacks.OnOwnershipTransfered(PhotonView targetView, Player previousOwner) {
    if (targetView.ViewID == photonView.ViewID) {
        string id = targetView.ViewID.ToString();
        string p1 = previousOwner.NickName;
        string p2 = targetView.Owner.NickName;
        Debug.Log($"ViewID {id} の所有権が {p1} から {p2} に移譲されました");
    }
}

インスタンスの所有者がルームから退出した際は、生成者に所有権が戻りますが、インスタンスの生成者がルームから退出した際は、その所有権が移譲されているかどうかにかかわらずインスタンスが自動的に削除されるため注意しましょう。

シーンオブジェクトはデフォルトで所有者を持ちませんが、通常のネットワークオブジェクトと同じように、所有権を移譲できます。所有者を持たない間は、マスタークライアントが管理者になっていて、photonView.IsMineで判別もできます。マスタークライアントがルームから退出した際は、次に割り当てられたマスタークライントが管理者になります。所有権を移譲した後に所有者がルームから退出した際は、所有者を持たない状態に戻りマスタークライアントが管理者になります。

https://doc.photonengine.com/ja-jp/pun/current/gameplay/ownershipandcontrol