Photonでオブジェクトをスポーンする方法の調査およびIPunPrefabPoolとUniRx.Toolkit.ObjectPoolを使ったオブジェクトプールのサンプル

# はじめに
Photonでネットワーク上にオブジェクトを生成・同期（以後「スポーン」と呼ぶ）する方法とそのメリット・デメリットをいろいろ調べた。ついでにUniRxのオブジェクトプールの機構を使う方法も調べた。

制限やデメリットと感じる特徴はわかりやすいように赤字にしてます。

記事の最後に実用的なサンプルコードもあります。

## `PhotonNetwork.Instantiate()`を普通に使う

* <font color="red">スポーンするプレハブは`string`で指定かつResouces直下に入れておく必要あり</font>
* `Awake`が呼ばれた時点で`PhotonView`は初期済み（`ownerId`などがセットされている）
* スポーンの命令は自動でキャッシュされ、後から入ってきたプレイヤー上にもスポーンする
* スポーンしたオブジェクトの削除（デスポーン）は`PhotonNetwork.Destroy()`
  * オブジェクトがデスポーン済みなら後から入ってきたプレイヤーにスポーンの命令が飛ばない（`Instantiate()`命令のキャッシュは`Destroy()`命令で消える）
* <font color="red">呼び出したオーナー以外はスポーン・デスポーンの時にフックできない</font>

最後の特徴は例えば「プレイヤーがスポーンしたら`PlayerManager`に登録したい」といったときに不便。スポーンするプレハブの`OnPhotonInstantiate`内で`PlayerManager.Add(this)`みたいに登録することもできるけど、それだと密結合になるのでできれば避けたい。

あとResourcesは💩だから使いたくない。


## `PhotonNetwork.Instantiate()`と`IPunPrefabPool`を使う

`IPunPrefabPool`というのはPhoton組み込みのオブジェクトプール機構を使うときに実装が必要になるインターフェース。

```csharp
public interface IPunPrefabPool
{
    // prefabIdを受け取って新しいorプールされたインスタンスを返す必要がある
    GameObject Instantiate(string prefabId, Vector3 position, Quaternion rotation);

    // スポーンしたオブジェクトの破棄処理を書く
    void Destroy(GameObject gameObject);
}
```

`PhotonNetwork.PrefabPool`に`IPunPrefabPool`を実装したインスタンスを入れておけば`PhotonNetwork.Instantiate()`が呼ばれたときにそのインスタンスが使われる。

見るからに好き放題フック入れられそうだし、`Instantiate()`でインスタンス返せばいいならResources使わなくて良さそう！←必須だった💩

### 使用例

https://github.com/jeanfabre/Exitgames--PUN--Pooling_u4/blob/master/Assets/PUN%20Custom%20Samples/SmartPool/Scripts/PunSmartPoolBridge.cs から引用

```csharp
using System.Collections.Generic;
using UnityEngine;
using System.Collections;

/// <summary>
/// Pun smart pool bridge.
/// Common Pitfalls:
/// -- even when using a pool manager, you need to store your prefab inside a Resources Folder, 
///    it's because PUN needs to load the prefab and assign viewIDs to all PhotonViews. Note: It's not instantiated by pun at all, just loaded for analyzing.
/// -- on the instanciated prefab, use OnPhotonInstantiate() to catch info on initializating if you must, OnEnable and Start aren't suitable due to the network initializaion processes.
/// </summary>
public class PunSmartPoolBridge : MonoBehaviour, IPunPrefabPool
{
	public void Start ()
	{
	    PhotonNetwork.PrefabPool = this;
	}

    public GameObject Instantiate(string prefabId, Vector3 position, Quaternion rotation)
    {
		Debug.LogWarning("Instantiate Prefab: " + prefabId);
		
		GameObject go = SmartPool.Spawn(prefabId);
        go.transform.position = position;
        go.transform.rotation = rotation;

        return go;
    }

    public void Destroy(GameObject gameObject)
    {
		SmartPool.Despawn(gameObject);

    }
}
```

[SmartPool](https://www.assetstore.unity3d.com/en/#!/content/5911)っていうアセットを使ってる。

----

* <font color="red">スポーンするプレハブは`string`で指定かつResouces直下に入れておく必要あり</font>
* <font color="red">`Awake`が呼ばれた時点で`PhotonView`は初期化されていない
* スポーンの命令は自動でキャッシュされ、後から入ってきたプレイヤー上にもスポーンする
* スポーンしたオブジェクトの削除は`PhotonNetwork.Destroy()`
  * オブジェクトがデスポーン済みなら後から入ってきたプレイヤーにスポーンの命令が飛ばない（`Instantiate()`命令のキャッシュは`Destroy()`命令で消える）
* スポーン・デスポーン時のフックは自分で追加可能


## `PhotonView#RPC()`を使う

[Photon公式の説明](https://doc.photonengine.com/ja-jp/pun/current/manuals-and-demos/instantiation#__________)からサンプルコードを引用

```csharp
void SpawnPlayerEverywhere()
{
    // You must be in a Room already
    // Marco Polo tutorial shows how to connect and join room
    // See: http://doc.photonengine.com/en/pun/current/tutorials/tutorial-marco-polo
 
    // Manually allocate PhotonViewID
    int id1 = PhotonNetwork.AllocateViewID();
 
    PhotonView photonView = this.GetComponent<PhotonView>();
    photonView.RPC("SpawnOnNetwork", PhotonTargets.AllBuffered, transform.position, transform.rotation, id1, PhotonNetwork.player);
}
 
public Transform playerPrefab; //set this in the inspector
 
[RPC]
void SpawnOnNetwork(Vector3 pos, Quaternion rot, int id1, PhotonPlayer np)
{
    Transform newPlayer = Instantiate(playerPrefab, pos, rot) as Transform;
 
    // Set player's PhotonView
    PhotonView[] nViews = newPlayer.GetComponentsInChildren<PhotonView>();
    nViews[0].viewID = id1;
}
```

0. `PhotonNetwork.AllocateViewID()`で割り当てに使用する`viewID`を得る
0. `PhotonView#RPC()`でほかのクライアントにスポーン命令を送出する（命令は`PhotonTargets.AllBuffered`などを指定して明示的にキャッシュする）
0. RPCを受け取ったクライアント上でプレハブを`Object.Instantiate()`し、送られてきた`viewID`を`PhotonView`に割り当てる

----

* スポーンするプレハブはResourcesに入れておく必要はない
* <font color="red">`Awake`が呼ばれた時点で`PhotonView`は初期化されていない</font>（プレハブをゴニョゴニョすると解決可能）
* <font color="red">`OnPhotonInstantiate()`が呼ばれない</font>（自分で`SendMessage()`すれば呼べる）
* スポーンの命令をキャッシュするように指定すると後から入ってきたプレイヤー上にもスポーンする
* スポーンしたオブジェクトの削除は同様にRPCを使う（<font color="red">`PhotonNetwork.Destroy()`は使えない</font>）
* オブジェクトがデスポーン済みでも後から入ってきたプレイヤーにスポーンの命令が飛ぶ（`Instantiate()`命令のキャッシュは`Destroy()`命令で消えない）
  * <font color="red">スポーンと破棄が溜まると後から入ってきたプレイヤー上で無駄な処理が走る</font>
* スポーン・デスポーン時のフックは自分で追加可能

ちなみに`OnPhotonInstantiate()`とサーバにキャッシュされたRPCは

　　`Awake()` → `OnPhotonInstantiate()` → サーバにキャッシュされたRPC() → `Start()`

の順で呼ばれる。

また、Photonの`PrefabPool`を使う場合はプールから取り出したインスタンスの`Awake()`と`Start()`は呼ばれないので`OnPhotonInstantiate()`で初期化すべき。こちらはプールから取り出したインスタンスに対しても呼ばれる。


## `PhotonNetwork.RaiseEvent()`を使う

* スポーンするプレハブはResourcesに入れておく必要はない
* `Awake`が呼ばれた時点で`PhotonView`は初期化されていない（プレハブをゴニョゴニョすると解決可能）
* <font color="red">`OnPhotonInstantiate()`が呼ばれない</font>（自分で`SendMessage()`すれば呼べる）
* スポーンの命令をキャッシュするように指定すると後から入ってきたプレイヤー上にもスポーンする
* スポーンしたオブジェクトの削除は同様に`RaiseEvent()`を使う（<font color="red">`PhotonNetwork.Destroy()`は使えない</font>）
* スポーンの命令はキャッシュから削除可能
* スポーンしたときのフックは自分で追加可能
* <font color="red">0~199の200個しかないイベントコードを生成・破棄で2つ消費する</font>


# やり方ごとのメリット・デメリットまとめ

◯ … 良い
△ … 工夫でどうにかなる
× … どうにもならない

|    | PhotonNetwork.Instantiate() | RPC() | RaiseEvent() |
|:---|:---------------------------:|:-----:|:------------:|
| Resources |             ×（必須） | ◯（不要） | ◯（不要） |
| Awake時点でPhotonViewの初期化 | ◯（済） or ×（PrefabPool使用時） | △（未） | △（未） |
| スポーン命令のキャッシュ | ◯（賢い） | ×（消せない） | △（消せる） |
| OnPhotonInstantiate | ◯（呼ばれる） | △（呼ばれない） | △（呼ばれない） |
| PhotonNetwork.Destroy() | ◯（使える） | ×（使えない） | ×（使えない） |
| イベントコード | ◯（消費しない） | ◯（消費しない） | ×（消費する） |
| スポーン・削除時フック | △（PrefabPool使用時のみ実装可） | △（実装可） | △（実装可） |

サーバ上のスポーン命令のキャッシュが消せないからRPCは論外な気がする。

# `IPunPrefabPool`と`UniRx.Toolkit.ObjectPool`を使った`NetworkSpawner`のサンプル

スポーンしたオブジェクトを監視するためのクラスを書いてみた。

* UniRx
* PhotonRx

を使用、`SingletonMonoBehaviour`は、まぁ自分のシングルトンクラスの実装で置き換えてください。

```NetworkSpawner.cs
using Photon;
using PhotonRx;
using System.Linq;
using UniRx;
using UniRx.Toolkit;
using UniRx.Triggers;
using UnityEngine;

namespace Jagapippi.Network
{
    public abstract class NetworkSpawner<T> : SingletonMonoBehaviour<NetworkSpawner<T>> where T : PunBehaviour
    {
        protected ObjectPoolAdapter _objectPoolAdapter;

        protected virtual ObjectPoolAdapter objectPoolAdapter
        {
            get
            {
                if (_objectPoolAdapter == null)
                {
                    _objectPoolAdapter = CreateObjectPoolAdapter();

                    _objectPoolAdapter.OnPhotonInstantiateAsObservable()
                        .Subscribe(instance => _ComponentCollection.Add(instance))
                        .AddTo(this);

                    _objectPoolAdapter.OnReturnAsObservable()
                        .Subscribe(instance => _ComponentCollection.Remove(instance))
                        .AddTo(this);
                }
                return _objectPoolAdapter;
            }
        }

        protected abstract ObjectPoolAdapter CreateObjectPoolAdapter();

        #region properties

        [SerializeField] protected GameObject _prefab;
        [SerializeField] protected bool _usePool = true;

        protected readonly ReactiveCollection<T> _ComponentCollection = new ReactiveCollection<T>();

        public IReadOnlyReactiveCollection<T> ComponentCollection
        {
            get { return _ComponentCollection; }
        }

        #endregion

        protected virtual void Awake()
        {
            if (PhotonNetwork.PrefabPool == null)
            {
                PhotonNetwork.PrefabPool = new ObjectPoolManager();
            }

            var objectPoolManager = (ObjectPoolManager) PhotonNetwork.PrefabPool;

            objectPoolManager.AddObjectPool(this.objectPoolAdapter);

            this.OnDestroyAsObservable()
                .Subscribe(_ =>
                {
                    objectPoolManager.RemoveObjectPool(this.objectPoolAdapter);
                    this.objectPoolAdapter.Clear();
                });
        }

        #region Spawn methods

        public static void Spawn()
        {
            instance.Spawn(Vector3.zero, Quaternion.identity);
        }

        public static void Spawn(Transform pos)
        {
            instance.Spawn(pos.position, pos.rotation);
        }

        public static void Spawn(Vector3 position)
        {
            instance.Spawn(position, Quaternion.identity);
        }

        public static void Spawn(Quaternion rotation)
        {
            instance.Spawn(Vector3.zero, rotation);
        }

        public virtual void Spawn(Vector3 position, Quaternion rotation, params object[] args)
        {
            PhotonNetwork.Instantiate(_prefab.name, position, rotation, 0, args);
        }

        public static void SpawnSceneObject()
        {
            instance.SpawnSceneObject(Vector3.zero, Quaternion.identity);
        }

        public static void SpawnSceneObject(Transform pos)
        {
            instance.SpawnSceneObject(pos.position, pos.rotation);
        }

        public static void SpawnSceneObject(Vector3 position)
        {
            instance.SpawnSceneObject(position, Quaternion.identity);
        }

        public static void SpawnSceneObject(Quaternion rotation)
        {
            instance.SpawnSceneObject(Vector3.zero, rotation);
        }

        public virtual void SpawnSceneObject(Vector3 position, Quaternion rotation, params object[] args)
        {
            PhotonNetwork.InstantiateSceneObject(_prefab.name, position, rotation, 0, args);
        }

        #endregion

        public static void Destroy(PhotonView photonView)
        {
            if (photonView == null
                || photonView.isMine == false
                || instance.ComponentCollection.Contains(photonView.gameObject.GetComponent<T>()) == false
            )
            {
                Debug.LogWarning("tried to destroy invalid PhotonView!");
                return;
            }

            PhotonNetwork.Destroy(photonView);
        }

        protected abstract class ObjectPool : ObjectPool<T>
        {
            protected ObjectPool(GameObject prefab, bool usePool)
            {
                _prefab = prefab;
                _usePool = usePool;
            }

            private readonly GameObject _prefab;
            private readonly bool _usePool = true;

            protected GameObject prefab
            {
                get { return _prefab; }
            }

            protected bool usePool
            {
                get { return _usePool; }
            }

            public virtual bool HasPrefab(string prefabId)
            {
                // TODO: 名前以外の衝突しないIDで照合する
                return _prefab.name == prefabId;
            }

            protected override T CreateInstance()
            {
                // TODO: Awake時点でのPhotonViewの初期化を保証する
                return Instantiate(this.prefab).GetComponent<T>();
            }

            public new virtual void Return(T instance)
            {
                base.Return(instance);
                if (this.usePool == false)
                {
                    this.Clear();
                }
            }
        }

        protected class ObjectPoolAdapter : ObjectPoolManager.ObjectPool
        {
            private readonly ObjectPool _objectPool;

            public ObjectPoolAdapter(ObjectPool objectPool)
            {
                _objectPool = objectPool;
            }

            private readonly Subject<T> _onPhotonInstantiate = new Subject<T>();

            public IObservable<T> OnPhotonInstantiateAsObservable()
            {
                return _onPhotonInstantiate;
            }

            private readonly Subject<T> _onReturn = new Subject<T>();

            public IObservable<T> OnReturnAsObservable()
            {
                return _onReturn;
            }

            public override Component Rent()
            {
                var instance = _objectPool.Rent();

                // TODO: すべてのOnPhotonInstantiateが呼ばれてからOnNext()する
                instance.OnPhotonInstantiateAsObservable()
                    .Take(1)
                    .TakeUntil(instance.OnDisableAsObservable())
                    .Subscribe(_ => _onPhotonInstantiate.OnNext(instance))
                    .AddTo(instance);

                return instance;
            }

            public override bool Return(GameObject gameObject)
            {
                var instance = gameObject.GetComponent<T>();
                if (instance && instance.GetType().IsSubclassOf(typeof(T)) == false)
                {
                    _onReturn.OnNext(instance);
                    _objectPool.Return(instance);
                    return true;
                }
                return false;
            }

            public override bool HasPrefab(string prefabId)
            {
                return _objectPool.HasPrefab(prefabId);
            }

            public virtual void Clear()
            {
                _objectPool.Clear();
            }
        }
    }
}
```

```ObjectPoolManager.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Jagapippi.Network
{
    public class ObjectPoolManager : IPunPrefabPool
    {
        private readonly List<ObjectPool> _objectPoolList = new List<ObjectPool>();

        public void AddObjectPool(ObjectPool _objectPool)
        {
            if (_objectPoolList.Contains(_objectPool) == false)
            {
                _objectPoolList.Add(_objectPool);
            }
        }

        public void RemoveObjectPool(ObjectPool _objectPool)
        {
            _objectPoolList.Remove(_objectPool);
        }

        GameObject IPunPrefabPool.Instantiate(string prefabId, Vector3 position, Quaternion rotation)
        {
            var objectPool = _objectPoolList.First(pool => pool.HasPrefab(prefabId));
            var go = objectPool.Rent().gameObject;
            go.transform.SetPositionAndRotation(position, rotation);
            return go;
        }

        void IPunPrefabPool.Destroy(GameObject gameObject)
        {
            foreach (var objectPool in _objectPoolList)
            {
                if (objectPool.Return(gameObject))
                {
                    return;
                }
            }
        }

        public abstract class ObjectPool
        {
            public abstract bool HasPrefab(string prefabId);
            public abstract Component Rent();
            public abstract bool Return(GameObject gameObject);
        }
    }
}
```

## 使い方

`NetworkSpawner`クラスを継承して使う。

* Tにはキャッシュしたいコンポーネントを指定
* キャッシュしたいコンポーネントのオブジェクトプールクラスを定義する
* <b>スポーン・デスポーンは`_ComponentCollection`への追加・削除を`Subscribe()`で監視する</b>
* インスペクタで
  * スポーンさせたいプレハブをセットする（Resources以下にも入れる必要あり）
  * 「Use Pool」フラグをセットする（オブジェクトプールは不要でフックだけほしいときは`false`にする）
* オブジェクトのスポーンは各種`Spawn()`を呼ぶ
* シーンオブジェクトのスポーンは各種`SpawnSceneObject()`を呼ぶ
* デスポーンはクラスのstaticメソッドの`Destroy()`を呼ぶか`PhotonNetwork.Destroy()`を呼ぶ（たぶん前者のほうが安全）


### 使用例

```PlayerSpawner.cs
using UniRx;
using UnityEngine;

namespace Jagapippi.Network.Game
{
    public class PlayerSpawner : NetworkSpawner<Player>
    {
        private readonly ReactiveProperty<Player> _LocalPlayer = new ReactiveProperty<Player>();

        public ReadOnlyReactiveProperty<Player> LocalPlayer
        {
            get { return _LocalPlayer.ToReadOnlyReactiveProperty(); }
        }

        public IReadOnlyReactiveCollection<Player> PlayerList
        {
            get { return _ComponentCollection; }
        }

        protected override void Awake()
        {
            base.Awake();

            this.PlayerList.ObserveAdd()
                .Select(e => e.Value)
                .Subscribe(player => PhotonPlayer.Find(player.photonView.ownerId).TagObject = player)
                .AddTo(this);

            this.PlayerList.ObserveAdd()
                .Select(e => e.Value)
                .Where(player => player.photonView.isMine)
                .Subscribe(player => _LocalPlayer.Value = player)
                .AddTo(this);

            this.PlayerList.ObserveRemove()
                .Select(e => e.Value)
                .Where(player => player.photonView.isMine)
                .Subscribe(player => _LocalPlayer.Value = null)
                .AddTo(this);
        }

        protected override ObjectPoolAdapter CreateObjectPoolAdapter()
        {
            return new ObjectPoolAdapter(new PlayerObjectPool(_prefab, _usePool));
        }

        protected class PlayerObjectPool : ObjectPool
        {
            public PlayerObjectPool(GameObject prefab, bool usePrefab) : base(prefab, usePrefab)
            {
            }
        }
    }
}
```

# 感想とかTODOとか

スポーンで生まれる側がどういう手段でスポーンしたかを気にせずに実装できるようにしたかったが、Awake時点でPhotonViewの初期化が完了しているようにするのはできなかった。あと結局Resourcesが必須💩

まとめてて思ったけど`RaiseEvent()`使えばイベントコード消費する以外全部解決できそうな気がする。というか`PhotonNetwork.Instantiate()`も内部的に`RaiseEvent()`使ってるし。

`Awake`呼び出し時点でうんぬんはプレハブを`SetActive(false)`しておけば`Awake()`とそれ以降の呼び出しタイミングを制御できるのでそういうコードを挟む。

# 参考資料

* [僕もPhotonを使いたい](http://www.urablog.xyz/archive/category/PUN)
* [IPunPrefabPool Working Sample with SmartPool(free)](http://forum.photonengine.com/discussion/8214/ipunprefabpool-working-sample-with-smartpool-free)
* [UniRxのObjectPoolを利用する](http://qiita.com/toRisouP/items/2a5fb86654525a4a8453)
* PhotonNetwork.cs（PhotonのSDK内のソース）
* NetworkingPeer.cs（PhotonのSDK内のソース）
* PhotonClasses.cs（PhotonのSDK内のソース）

ソース読まないとわからなかったことがたくさんあったのでソース読むのおすすめ。