PUN2の小ネタ話

PhotonUnityNetworking2を使う機会があり、そこで実際に触って色々知ったことを書きました
この記事を見て何かの足しになれば幸いです
※普通にRPCやRaiseEventなどの用語を出すのでPhoton触ったことある人向けです

#### 使用しているPUN2とUnityバージョン
・Unity2019.3.3f1
・PUN2 v2.15

# 「RpcTarget.All」「RpcTarget.AllBufferd」について

RPCを使って同期処理を行う場合、引数に送信先を指定する部分がある
ここで「All」「AllBufferd」を指定すると自分のローカル環境で即座に実行された後、他のクライアントに同期通信を飛ばすようになっているが、実は処理速度的にはあまり良くは無い

これはPhotonNetworkPart.csにあるRPC関数の中身を見ると分かるが、通信同期用の情報をハッシュテーブル化した後に自分の環境で即時実行するため、わざわざハッシュテーブル化した情報を分解して処理を実行している
※下記コードの`rpcEvent`がハッシュテーブル、`ExecuteRpc`が実行関数

``` C#:PhotonNetworkPart.cs(RPC関数の一部抜粋)
    switch (target)
            {
                // send to a specific set of players
                case RpcTarget.All:
                    RpcOptionsToAll.InterestGroup = (byte)view.Group;   // NOTE: Test-wise, this is static and re-used to avoid memory garbage
                    PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, RpcOptionsToAll, sendOptions);

                    // Execute local
                    ExecuteRpc(rpcEvent, NetworkingClient.LocalPlayer);
                    break;

```

シンプルな対処法としては同期処理を飛ばす前に、一度その同期処理を自分の環境で実行してから「Others」「OtherBufferd」を呼び出すと、無駄がなくなってスッキリする

``` C#
TestRPC("HogeHoge");
_photonView.RPC(nameof(TestRPC), RpcTarget.Others, "HogeHoge");
        
```

但し上記のやり方だとRPC呼び出し前に自分の環境で同期処理を呼び出し忘れる危険性もあるので、何かしらラッパー関数を用意する、もしくはPhotonView.csのRPC関数を拡張する、などをした方が安全性が上がる
もしくは「AllViaServer」「AllBufferedViaServer」を使うという方法もあるが、送信者自身も通信を介して処理が実行されるので一長一短ではある

# RPCの負荷について

RPCはPUNのコードを見ていくと分かるがRaiseEventのラッパーになっており、基本的にPhotonViewがGameObjectにアタッチされていれば気軽に同期処理が行えるようになっている。そこまで同期処理でパフォーマンスを気にしない、PhotonServerを使用しないなどの条件に該当するのであれば、RPCだけ使うという方法でも十分と言える
しかしPhotonViewの数が多くなるほど、処理負荷が顕著になっていく

RPCは同期処理を実行する際にGetPhotonViewという関数を内部で実行しているが、これはシーン上にあるPhotonViewコンポーネントがアタッチされているオブジェクトを全て取得して、その中でIDが一致するものを返すという処理を行っている
これがRPCが実行されるたびに毎回呼ばれるので、PhotonViewが多いほどかなり負荷がかかる

``` C#:GetPhotonView
        public static PhotonView GetPhotonView(int viewID)
        {
            PhotonView result = null;
            photonViewList.TryGetValue(viewID, out result);

            if (result == null)
            {
                PhotonView[] views = GameObject.FindObjectsOfType(typeof(PhotonView)) as PhotonView[];

                for (int i = 0; i < views.Length; i++)
                {
                    PhotonView view = views[i];
                    if (view.ViewID == viewID)
                    {
                        if (view.didAwake)
                        {
                            Debug.LogWarning("Had to lookup view that wasn't in photonViewList: " + view);
                        }
                        return view;
                    }
                }
            }

            return result;
        }
```

上記処理の後は`MethodInfo` というReflection機能を使用して指定した関数を取得して実行処理を行っている。こちらに関しては一度呼び出されたものはキャッシュして使い回す機構にはなっているものの、ランタイム上でリフレクションはあまりよろしくない


この事からPhotonServerを使用する、パフォーマンスチューニングをがっつりやらないといけないなどの理由がある場合、手間は掛かるが拡張性やカスタマイズしやすいRaiseEventを使った方が結果的に良いと思われる
※PhotonServerとのやり取りは基本的にRaiseEventベースになっているため、RPCでPhotonServerと連携するのは難しい？

# 自作カスタムタイプはRaiseEventにも適用されるか

結論：適用されました
(RaiseEventの自作カスタムタイプ周りがどこにも載っていなかったので出来ないものだと思ってた)

CustomType.csと同じような形式でシリアライズとデシリアライズを定義して、`PhotonPeer.RegisterType`で登録処理を実行すると、RaiseEventを使用してデータを送受信した後の型変換処理も問題なく通る

# あとがき

間違っているところがあれば指摘お願いします
Photonは結構奥が深いのでまた何かあったら追記予定です