PUN2を使っていた自分から見て、Photon Fusionがどんなサービスか調べてみた - プログラム編

PUN2を使っていたのですが、Photon Fusionを使っていこうと思い、何がどう違う感じになっているのか、プログラム的な観点でまとめたものです。設計や権限などドキュメントっぽいまとめは[こちら](https://qiita.com/matchyy/items/fbe961ea9e93a84499f8)。
結果的にGameModeをSharedで利用することにしたので、そっちよりの情報になっています。

# 概略

- Photon Fusionを始めるのは簡単
- ただNetworkBehaviourのFixedUpdateNetworkはSpawnした側でしか実行されないなど、随所にPhoton Fusionの設計に準じた実装の心掛けが必要

# 主要なクラスとインターフェース

- NetworkRunner
    - 実際にマルチプレイを実施するために稼働するクラス
    - StartGameなどを実行してPhotonに接続
    - 全プレイヤーの情報（PlayerRef）なども持っています
- INetworkRunnerCallbacks
    - NetworkRunnerでマルチプレイを実装したときに発生するイベントを、callbackとして受け取れるようにするインターフェース
    - 例えば OnPlayerJoined なら、プレイヤーが参加してきたときに呼び出されます
    - この辺を利用して、後述のNetworkBehaviourでGameObjectを生成していきます
- NetworkBehaviour
    - Photon Fusion版MonoBehaviour。UnityのMonoBehaviourに合わせて動作するように設計されている模様
    - オンラインになったときにネットワークを通じて共有すべきGameObjectはこれを継承して共有します
        - NetworkRunnerでSpawnされると、全クライアントで自動的に生成されます
        → ネットワークを介し、実装した内容に応じて同期されるようになっています
    - UnityのAPIに合わせて Start → Spawned、Update → Render などが準備されています
        - FixedUpdateNetworkもあるが、これはUnityのFixedUpdateとは一味違い、Sharedだと最初理解し難いと感じました。実際のプログラムを考えてみるの生成されたGameObjectの行方のところを参照ください

## フローのイメージ

1. NetworkRunnerのStartGame()でPhotonに接続
2. ルームに入るとINetworkRunnerCallbacksのOnPlayerJoined発火
3. 2の中でNetworkRunnerのSpawn(NetworkBehaviour）を実行
4. 3をローカルでやると、ネットワーク越しに自動で他ユーザにもSpawnされる

# では実際のプログラムを考えてみます

## Photon Fusionに接続するための準備

Photon Fusionに接続すると、それぞれの状況に合わせたCallbackが最初から用意されています。まずここではINetworkRunnerCallbacksを実装して、ルームに参加したら自身のキャラクター（NetworkBehaviour/GameObject）を生成するようにして、Photon Fusionに接続した後の実装を入れておきます。
前述のフローのイメージ3に該当します。

```c#
/* using 省略 */

public class CharaManager : MonoBehaviour, INetworkRunnerCallbacks
{
    [SerializeField] private GameObject myNetworkBehaviourPrefab;

    // 参加したら自分のGameObjectを生成するイメージ
    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        Debug.Log($"{player.PlayerId} が参加しました。");
        // これだけでネットワーク越しに全てのプレイヤーにGameObjectを生成してくれる
        runner.Spawn(myNetworkBehaviourPrefab, Vector3.zero, Quaternion.identity, player);
    }

    // Callbackがたくさんあるので、それぞれの状況に応じて呼び出す
    public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { }
    public void OnInput(NetworkRunner runner, NetworkInput input) { }
    public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
    public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { }    
    public void OnConnectedToServer(NetworkRunner runner) { }
    public void OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason) { }
    public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
    public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
    public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
    public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { }
    public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
    public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
    public void OnSceneLoadDone(NetworkRunner runner) { }
    public void OnSceneLoadStart(NetworkRunner runner) { }
    public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
    public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player) { }
    public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data) { }
    public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress) { }
}
```

## Photon Fusionに接続

```csharp
/* using 省略 */

public class PhotonManager : MonoBehaviour
{
    [SerializeField] private NetworkRunner networkRunner;

    public async UniTask<StartGameResult> StartGame(INetworkRunnerCallbacks networkRunnerCallbacks)
    {
        // ここで準備しておいた処理を登録、ルームに参加すると自動で呼び出されるようになる
        networkRunner.AddCallbacks(networkRunnerCallbacks);
        var startGameArgs = new StartGameArgs
        {
            GameMode = GameMode.Shared,
        };
        return await networkRunner.StartGame(startGameArgs);
    }
}
```

## 生成されたNetworkBehaviourの行方

便宜上、以下定義でローカルとリモートという言葉を使いコメントを書いています。

- ローカル：NetworkRunnerでSpawnを呼び出した側
- リモート：NetworkRunnerのSpawnでネットワーク越しにSpawnされた側

```csharp
/* using 省略 */

public class MyNetworkBehaviour : NetworkBehaviour
{
    // プロパティにNetworkedの属性をつけるだけ、ローカルからリモートにデータ連携される。
    [Networked] public Vector3 MyPosition { get; set; }
    [Networked] public Quaternion MyRotation { get; set; }
    [Networked] public Vector3 MyScale { get; set; }

    // SharedMode限定の書き方。権限はModeで考える必要あり。
    private bool isLocal => HasStateAuthority;

    // ローカル用、リモートではnullなので注意。
    private Transform myLocal;

    // ローカル、リモートの両方で呼ばれる。
    // UnityのStartのようなイメージ。
    public override void Spawned()
    {
        if (isLocal)
        {
            myLocal = transform;
        }
    }
    
    // ローカルでのみ呼ばれる!!!
    // リモートでは呼ばれない!!!
    // ローカルの状態をリモート側に伝達するための処理を入れる。
    public override void FixedUpdateNetwork()
    {
        MyPosition = localAnimal.localPosition;
        MyRotation = localAnimal.localRotation;
        MyScale = localAnimal.localScale;
    }

    // ローカル、リモートの両方で呼ばれる。
    // UnityのUpdateのようなイメージ。
    public override void Render()
    {
        if (!isLocal)
        {
            transform.localPosition = MyPosition;
            transform.localRotation = MyRotation;
            transform.localScale = MyScale;
        }
    }
}
```

# なぜFixedUpdateNetworkが仲間外れなのか

生成されたNetworkBehaviourの行方のプログラムの中にコメントも入れてありますが、FixedUpdateNetworkがローカルでしか呼ばれないところに当初気が付かず、SpawnedとRenderはローカル・リモートで呼ばれるのに何が何だかよくわからず混乱しました。

ここはPhoton Fusionはティックベースシミレーションを前提として設計されているからだと理解しました。（ティックベースシミレーション等は[別記事](https://qiita.com/matchyy/items/fbe961ea9e93a84499f8)を参照ください）

これまでPUN2を使っていた身としてはなじみがないのでわかりませんでしたが、FixedUpdateNetworkではローカルの入力を受け付けて、それをRenderで反映させるというのがPhoton Fusionの狙いなのだと思います。

またよくメソッド名を見るとStartNetworkではなくSpawned、UpdateNetworkではなくRenderにしているのは、FixedUpdateNetworkと違いローカルだけで動作するメソッドとの差異を作るためにネーミングを変えているのだと気が付きました。

この辺がPUN2から流れてきたユーザにとっては最初の関門になりそうだなと思っています。関門と書きましたが、わかっていればそれに従って作るだけですし、枠が決まっていて変えられないということは、誰しもがPhoton Fusionの設計思想に則って作ることになるので、MVCなどのフレームワークと同じでわかりやすい作りにもできるということだと思っています。

# まだ簡単そうに見えて色々あります

- データ連携でNetworkedを使っていますが、今回は説明を省いています。また他にもNetworkTransform、RPC等あります
- isLocal => HasStateAuthority としています。この辺は選択したModeによって権限の扱いが異なるため、それぞれのModeで理解しておく必要があります。こちらは[設計編の権限（状態管理）](https://qiita.com/matchyy/items/fbe961ea9e93a84499f8#%E6%A8%A9%E9%99%90%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86)を参照ください