Photon Fusion v1.1.1 Stable を Unity v2021.3.41f ～に導入する方法

# はじめに
Photon PUN２の後継と言われているPhoton Fuisionの導入方法についてまとめました。
Fuisonの機能については公式で詳細に記されていますのでこちらをご確認ください。

https://www.photonengine.com/ja-JP/Fusion

まず、Fusionは公式に導入から簡単な実装までのチュートリアルがあります。
**しかし、現時点（2022/6/22)でのチュートリアルはUnityやFusion,その他関連パッケージのバージョンアップが原因で下記URLのサイトの説明の通りに進めてもうまくいきません。**

photon fusion 101 - Getting Started (チュートリアル)

https://doc.photonengine.com/ja-jp/fusion/current/fusion-100/fusion-101

そのため、今回は現時点のそれぞれの最新バージョンを用いて導入する方法についてまとめました。

# 実装したもの
先に実装するもののゴールについて説明します。

行うこととしては、公式チュートリアルの
・photon fusion 101 - Getting Started (チュートリアル)
・photon fusion 102 - シーンをセットアップする
を現バージョン用に修正します。

**GameMode: Host/Cliant Mode**(プレイヤーの一人１をHostにして残りのプレイヤーがクライアントになるモードです。オブジェクトの生成や管理は、ホストプレイヤーのPCが処理します。ゲームモードの詳細は公式サイトをご確認ください。）

**実装環境**
Windows10
Unity 2021.3.41f **(ver. 2021.3.5f1でも動作確認済み)**
Photon Fusion 1.1.1(Stable)
Mono Cecil 1.11.4
![unity3.41f_FusionStable.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/4f1a743f-1e09-6a31-1616-a9d835509eef.gif)

# 実装方法
まず、この投稿のステップ0～5については、公式の「photon fusion 101 - Getting Started」（上にURLあり）と同じです。そのため、実行環境と同じバージョンであるか、だけ注意して公式サイト通りに進めたので問題はありません。

おそらくバージョンが合っていれば、**チュートリアル通りに進めると、公式のステップ7完了時にエラーが出ています**。
それらの修正は、本投稿のステップ7で行いますので、**公式サイトを見て進めた方は本投稿のステップ7から読み始めてください。**

## ステップ0 - アカウントとFusion用Photonサーバの作成
まず、Photon のアカウントを作成して、PhotonサーバのAppID(アプリケーションID）を取得するところまで行います。
前提として、1つのアプリケーションにつき1つのAppIDがあれば良いので、
**すでにFusionのAppIDがある場合は行う必要はありません。**
また、こちらのステップについては公式サイトに簡単で丁寧な説明がありますので、割愛します。
Photonのマイページにこのようなものが作成できていればOKです。
![image2022-6-22_14-10-9-.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/d3d28d16-f15e-2133-f69c-e62c06a7d818.png)
## ステップ1- SDKのダウンロード
公式サイトからFuison SDKをダウンロードします。
ダウンロードページはこちら

https://doc.photonengine.com/en-us/fusion/current/getting-started/sdk-download

![image2022-6-22_14-59-15-.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/2f72145e-3af1-190d-b287-d967cbbd8157.png)

こちらから、**Stable Builds** の **「Fusion SDK 1.1.1 F Build 512」** をダウンロード
Stable Build の最新verが更新されていれば、最新verでOK
## ステップ2 - Unityの要件を確認する
Fusionの実行に必要な最小のUnityバージョンを確認します。今回は、Unity 2021.3.41fを使用しています。
もし、Unity Hubにインストールができていなければ、インストールしておきましょう。
バージョンが変わると一部手順が変わったり、要らなくなったりするので、注意です。
## ステップ3 - 空のプロジェクトを作成する
 <img width="700" alt="step3-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/7fbe37f3-cc55-5119-2b58-8030593d283e.png">

1.　新しいプロジェクトを作成
 <img width="700" alt="step3-2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/73236147-df81-b39e-b47a-068898f7f04f.png">
2.　Unity エディターバージョン2021.3.4f1を選択
 <img width="700" alt="step3-3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/77eae006-3ad4-828a-75ac-d81ad46d01ef.png">
3.　左側の「すべてのテンプレート」を選択した状態で表示される「3D」を選択。
4.　右下のプロジェクト名を任意の名前に変更。（画像は変更前のものですが、私は「FusionSample」としています）
5.　右下の「プロジェクトを作成」を押す。

## ステップ4 - Fusion用にプロジェクトを準備する
### アセットシリアライゼーション
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/ac10b8ee-6781-5718-6aa3-7c29a7ecdbc7.png)
Fusion設定の中にはScriptableObjectアセットに保存されるものがあります。
これらの設定がいつでも読み取り可能であるようにするには、
**Edit > Project Settings > Editor > Asset Serialization > Mode**で
アセットシリアライゼーションのモードを**Force Text**にしておく必要があります。
### Mono Cecil
Photon FusionではMono Cecilパッケージを使用します。
パッケージはUnity Package Manager経由でインストールできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/cf6b6434-0032-d88b-8642-cbbf3bac775c.png)
1.　**Window > Package Manager**を開く
2.　左上の「+」のアイコンをクリックして表示される「Add package from git URL」を選択します。
3.　空欄に
**com.unity.nuget.mono-cecil**
を入力して「add」を押します。
**※スペース,改行等が入っていると正常に読み込めないので注意**


## ステップ 5 - Fusion SDKをインポートする
ステップ0から4を行うと、プロジェクトがFusion SDKをインポートできるようになります。SDKは.unitypackageファイルとして提供され、
**Assets > Import Package > Custom Packageツール**でインポートできます。
ステップ１でダウンロードしたSDKを保存場所へ移動し、インポートを開始してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/316574cd-0c15-c728-35b3-3461c397ac35.png)
1.　**Assets > Import Package > Custom Packageツール**をクリック。
2.　ステップ１でダウンロードしたSDKを保存場所へ移動しファイルを選択。
3.　表示された一覧の右下にある「import」をクリックしてインポート。
**この時点でエラーを吐いていると思いますが、後で修正するので、無視してください。**

## ステップ 6 - App IDを追加する
 <img width="600" alt="step6-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/9abc2e9e-a475-d53a-1ab4-43387c935343.png">

1.　ステップ5完了後上図のよなFusionHubが自動で開かれる。
2.　「Fusion App Id」にステップ０で取得したPhoton Fusion サーバ用のAppIDを入力。

**自動でPhoton Hub が開かない場合**
 <img width="600" alt="step6-2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/00ab32a2-f5cb-7df6-1ba0-a19c8b884831.png">
1.　Fusion > Realtime Settings を開く
 <img width="600" alt="step6-3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/b3c5efb4-c291-5beb-58af-01c52a019a80.png">
2.　App Settingsを開いてApp Id Fusionにステップ０で取得したPhoton Fusion サーバ用のAppIDを入力。

ここまでが公式チュートリアルの
**photon fusion 101 - Getting Started**
で説明されているプロジェクトにPhoton Fusionを導入するまでの説明です。

## ステップ 7 - 現時点でのエラーの修正
それではここからチュートリアルのままでは発生してしまうエラーの修正を行います。
修正するエラーはこちらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/c2d8ce61-eabe-d3fa-71c7-59f65eebb881.png)
まず、こちらのエラーについては、
Unity 2021.3.41f + Photon Fusion 1.1.1(Stable)では発生しますが、**バージョンの組み合わせ次第では発生しません。**
例として、
Fusionのバージョンを1.1.1(Stable)のまま、Unity のバージョンを2020.3.36f1まで下げる
Unity のバージョンを2021.3.41fのまま、Fusionのバージョンを1.1.2(Nightly)に上げる
のどちらかの組み合わせにすることでこのエラーを回避できます。

そのため、Fusionが今後v1.1.2以降のStable Buildを配布するようになれば、このステップの作業は不要です。
また、修正方法については、**Photon Fusion ver.1.1.2(Nightly)** を参考にしています。

それでは修正していきます。
 <img width="600" alt="step7-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/d5924a23-1e26-3721-4ebb-fbd4a60b3b4a.png">
1.　**Asset/Photon/FusionCodeGen/Fusion.CodeGen/Fusion.CodeGen.cs**を開く（ステップ7冒頭図のエラーをダブルクリックしても開けます）。
 <img width="800" alt="step7-2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/ec484069-73ae-ebe9-c4f8-6473d9f2dcaf.png">
2.　エラーがでている箇所に移動する。

<font color="Red">×</font>**修正前**

~~~Fusion.CodeGen.cs
          if (!Is(type, constraint)) {
~~~
<font color="Green">〇</font>修正後

~~~Fusion.CodeGen.cs
#if FUSION_CECIL_1_11_OR_NEWER
          if (!Is(type, constraint.ConstraintType)) {
#else
          if (!Is(type, constraint)){
#endif
~~~

3.　コードを上記のように修正します。
<img width="800" alt="step7-3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/52db3624-fb1a-2ae1-a866-c49311a977ce.png">
4.　上図のようになっていれば、OK.
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/f88f82a3-bfbb-1e7a-fb37-0ce9864fa3e7.png)
5.　先ほどのC#ファイルと同じディレクトリにある**Asset/Photon/FusionCodeGen/Fusion.CodeGen/Fusion.CodeGen.asmdef**　を開く

追加する要素

~~~Fusion.CodeGen.asmdef
,
{
    "name": "com.unity.nuget.mono-cecil",
    "expression": "1.11",
    "define": "FUSION_CECIL_1_11_OR_NEWER"
}
~~~
6.　その中の「"versionDefines"」のリストの中に上記要素を追加.
               ※追加する際、元から存在する要素と追加する要素との間に「 , 」を忘れないようにしましょう。
<img width="800" alt="step7-5" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/b3797710-7166-b42b-83dd-a7dc9ef52917.png">

7.　上図のようになっていればOK

以上で修正が完了しました。
次のステップからは、公式チュートリアルの
**photon fusion 102 - シーンをセットアップする**

https://doc.photonengine.com/ja-jp/fusion/current/fusion-100/fusion-102

を進めていきます。こちらのチュートリアルについても、説明がバージョンアップに対応していないため、一部修正する必要があります。

さらに、**ここからは修正箇所も多くあるので、作業は当ページをもとに進めて、関数など機能の説明は公式チュートリアルを参照することをおすすめします。**
## ステップ 8 - プレイヤー生成オブジェクトの作成
それでは、これから冒頭の動画のものを作っていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/d461f797-a441-2be1-b801-6f4413fed6f5.png)
まず、スクリプトファイルやPreFabファイルを格納するためのフォルダを作成します。

1.　Assetsフォルダに２つの空のフォルダ「Scripts」「Prefabs」を作成します。
2.　新しい空白のGameObjectを作成し、名前を「BasicSpawner」とする。
3.　Scriptsフォルダに移動して「C# Script」ファイルを作成して名前を「BasicSpawner」とする。
4.　ScriptファイルをGameObjectにドラッグアンドドロップしてアタッチする。

~~~BasicSpawner.cs
using System;
using System.Collections.Generic;
using Fusion;
using Fusion.Sockets;
using UnityEngine;
using UnityEngine.SceneManagement;
 
 
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
    [SerializeField] private NetworkPrefabRef _playerPrefab;
    private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
    private NetworkRunner _runner;
    private bool _mouseButton0;
    private bool _mouseButton1;
 
    //Because the game is running in hosted mode, only the host has authority to spawn new objects.
    //This means that all player avatars must be spawned by the host when they join the session.
    //Conveniently, the OnPlayerJoined method of the INetworkRunnerCallbacks interface is called
    //on this exact occasion.
    // Shared Modeの場合、誰かがJoinした際に、全クライアントで実行される。
    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        //Debug.Log("Player Joined - spawning");
        // Create a unique position for the player
        Vector3 spawnPosition = new Vector3(3, 1, 0);
        NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
        // Keep track of the player avatars so we can remove it when they disconnect
        _spawnedCharacters.Add(player, networkPlayerObject);
 
 
    }
    //Similarly, when a player disconnects, the OnPlayerLeft method is called.
    public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
    {
        //if (player != runner.LocalPlayer) { return; }
        // Find and remove the players avatar
        if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
        {
            Debug.Log("despawn");
 
            runner.Despawn(networkObject);
            _spawnedCharacters.Remove(player);
        }
    }
 
    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        var data = new NetworkInputData();
 
        if (Input.GetKey(KeyCode.W))
            data.direction += Vector3.forward;
 
        if (Input.GetKey(KeyCode.S))
            data.direction += Vector3.back;
 
        if (Input.GetKey(KeyCode.A))
            data.direction += Vector3.left;
 
        if (Input.GetKey(KeyCode.D))
            data.direction += Vector3.right;
 
        if (_mouseButton0)
            data.buttons |= NetworkInputData.MOUSEBUTTON1;
        _mouseButton0 = false;
 
        if (_mouseButton1)
            data.buttons |= NetworkInputData.MOUSEBUTTON2;
        _mouseButton1 = false;
 
        input.Set(data);
    }
    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) { }
    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 OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment<byte> data) { }
    public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
    public void OnSceneLoadDone(NetworkRunner runner) { }
    public void OnSceneLoadStart(NetworkRunner runner) { }
 
    async void StartGame(GameMode mode)
    {
        // Create the Fusion runner and let it know that we will be providing user input
        _runner = gameObject.AddComponent<NetworkRunner>();
        _runner.ProvideInput = true;
 
        // Start or join (depends on gamemode) a session with a specific name
        await _runner.StartGame(new StartGameArgs()
        {
            GameMode = mode,
            SessionName = "TestRoom",
            Scene = SceneManager.GetActiveScene().buildIndex,
            SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
        });
    }
 
    private void OnGUI()
    {
        if (_runner == null)
        {
            if (GUI.Button(new Rect(0, 0, 200, 40), "Host"))
            {
                StartGame(GameMode.Host);
            }
            if (GUI.Button(new Rect(0, 40, 200, 40), "Join"))
            {
                StartGame(GameMode.Client);
            }
        }
    }
 
    private void Update()
    {
        _mouseButton0 = _mouseButton0 | Input.GetMouseButton(0);
        _mouseButton1 = _mouseButton1 || Input.GetMouseButton(1);
    }
}
~~~
5.　上記コードをBasicSpawner.csに記入します。関数の呼び出されるタイミング等の詳細な説明は公式のチュートリアルやリファレンスをご確認ください。

## ステップ 9 - プレイヤーアバターを作成する
それでは次に各プレイヤーが操作するPrefabを作成します。今回はただのCubeで実装します。必要に応じてPrefabを変更してください。
<img width="800" alt="step9-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/619117c3-3026-dc36-62e4-3210a12a7980.png">
 1.　新しい空白のGameObjectを作成し、名前を「PlayerPrefab」にします。
 2.　作成した「PlayerPrefab」オブジェクトに以下のコンポーネントを追加します。
　　・「Network Object」
　　・「Network Character Controller Prototype」
 3.　「PlayerPrefab」オブジェクトの子オブジェクトとして標準のUnityのCubeを追加します。そして、Cubeの名前を「Body」に変更します。
 4.　「Body」ゲームオブジェクトを親の「Network Character Controller Prototype」の「Interpolation Target」プロパティにドラッグします。
<img width="800" alt="step9-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/c61e6367-066c-7c0e-b157-d73527c522d0.png">
 5.　Scriptsフォルダに移動して「C# Script」ファイルを作成して名前を「Player」とします。
 6.　Playerスクリプトに以下を記入します。
~~~Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;

public class Player : NetworkBehaviour
{
    private NetworkCharacterControllerPrototype _cc;

    private void Awake()
    {
        _cc = GetComponent<NetworkCharacterControllerPrototype>();
    }

    public override void FixedUpdateNetwork()
    {
        if (GetInput(out NetworkInputData data))
        {
            data.direction.Normalize();
            _cc.Move(5 * data.direction * Runner.DeltaTime);
        }
    }
}
~~~
 7.　作成した「Player」スクリプトを「PlayerPrefab」オブジェクトに追加します。

## ステップ 10 - インプットを実装する
~~~NetworkInputData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;
public struct NetworkInputData : INetworkInput
{
    public const byte MOUSEBUTTON1 = 0x01;
    public const byte MOUSEBUTTON2 = 0x02;
 
    public byte buttons;
    //For simplicity this examples uses a vector to indicate desired movement direction,
    //but know that there are less bandwidth expensive ways of doing this.
    //For example a bitfield with one bit per direction
    public Vector3 direction;
}
~~~
1.  Scriptsフォルダに移動して「C# Script」ファイルを作成して名前を「NetworkInputData」とします。
2.  上記コードを「NetworkInputData.cs」に記入します。
<img width="800" alt="step9-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/6c6e3b64-d933-0df3-15b9-0eff8eeba6ad.png">

3.　作成した「PlayerPrefab」オブジェクトを「Prefabs」フォルダにドラッグアンドドロップします。
4.　Hierarchyから「PlayerPrefab」を削除します。
5.　HierarchyにUnity標準の「EventSystem」を追加
<img width="800" alt="step9-1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/02aff9ab-2b58-cfb9-8843-10f3b873b38f.png">
6.　「Prefabs」の「PlayerPrefab」を「BasicSpawner」コンポーネントの「PlayerPrefab」にドラッグアンドドロップする。

## ステップ 11 - ステージを実装する
最後にプレイヤーを動かすためのステージを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/9e216162-9a89-665a-d8ae-0d68c1c93bbf.png)
1.　Unity標準ののCubeを追加して、名前を「Floor」にします。
2.　「Transform」コンポーネントのパラメータを上図のように修正すれば完成です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726895/d1362d6a-b7a5-667c-39d8-92b84afc987c.png)
3.　最後に冒頭の動画のアングルにしたい場合のために「MainCamera」の「Transform」コンポーネントのパラメータを記載します。お好みでご利用ください。
4.　また、「Floor」や「PlayerPrefab」の色や形などはご自由に変更してください。

## ステップ 12 - テストする
これで実装は完了です。
アプリケーションをビルドして複数のインスタンスを起動します（または、Unity内から直接1つのインスタンスを実行します）。
Hostボタンを押すクライアントは1人で、後のクライアントはJoinを押すようにしてください。
プレイヤーのスピード等は、「Network Character Controller Prototype」のパラメータで調整可能です。

**Unity内から実行する際に気を付けること**
エディタでゲームを実行する場合は、フレームレートが非常に不安定になる可能性があります。フレームレートに不安がある場合は、2つのビルドした実行ファイルからお試しください。