PhotonFusionセットアップ備忘録

# 概要
* 新しいPhotonSDK　PhotonFusionを試してみる

# 目的
* Photon FusionをUnityProjectに取り込んでみて動作させるまでの手順をメモとして残しておく

# 環境
* Unity 2021.3.0f1
※PhotonFusionはUnity 2020.3以降である必要があるので注意

# PhotonFusionとは

* ドキュメント
https://doc.photonengine.com/ja-jp/fusion/current/getting-started/fusion-intro

* Youtube

https://youtu.be/Leg4g6HWEcY

# UnityProjectを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/841a7eb5-b24d-8b8c-200e-1bd3f13746d7.png)

# UnityProjectを設定する
* Unity 2020.3以降;
* アセットシリアライゼーションは 必ず テキストに設定してください（プロジェクト設定（Project Settings）>エディタ（Editor）>アセットシリアライゼーション（Asset Serialization）と移動し、モードをForce textに設定）　
* Mono.Cecil (com.unity.nuget.mono-cecilパッケージがプロジェクトにない場合は、パッケージマネージャを使って手動で追加できます。)

UnityEditorのメニューから
```Window/PackageManager/左上の「＋」ボタン」/Add package add git URL/下のURLを入力```

> com.unity.nuget.mono-cecil@1.10

# PhotonFusion入手する

* 事前にアカウント登録が必要
* Donwnloadはこちらから→[リンク](https://www.photonengine.com/ja-JP/sdks#fusion)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/926fdd45-09aa-f74a-6cde-d9757bc1da1d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/3e4b864b-4706-e4ed-efa0-46c93e8c6231.png)

# PhotonFisionに使用するアプリケーションIDを発行する

* こちらから発行→[リンク](https://dashboard.photonengine.com/ja-JP/App/Create)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/fc358423-65a4-c23b-f5f0-613b17d16de7.png)

# アプリケーションIDをメモしておく

* 見つからない場合はダッシュボードに行くと見つかる→[リンク](https://dashboard.photonengine.com/ja-JP)

※FusionではなくRealtimeのアプリケーションIDだった場合は設定ミスになるので注意

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/983013e9-84fd-dfb0-c948-fe7a4cf050f6.png)

# PhotonFusionを導入

* 全てのファイルを追加でOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/2b1bfdea-a18d-517e-ab02-48832ed1abf8.png)

# アプリケーションIDを入力
* プロジェクト内の ```Assets/Photon/Fusion/Resources/PhotonAppSettings```の```AppIdFusion```の項目にIDを入力

赤色のエリアに入力
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/837bcb98-7326-2504-c024-e66d428c7136.png)

# 公式のドキュメントを元に作業を行う

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

作業フローは下
* スクリプト```BasicSpawner.cs```、```NetworkInputData.cs```、```Player.cs```を作成

動作するまでのスクリプト全容(公式ドキュメントに部分ごとの説明があるので、本ドキュメントでは割愛）

```BasicSpawner.cs
using Fusion;
using Fusion.Sockets;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
    private NetworkRunner _runner;

    [SerializeField] private NetworkPrefabRef _playerPrefab;
    private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();

    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        // Create a unique position for the player
        Vector3 spawnPosition = new Vector3((player.RawEncoded % runner.Config.Simulation.DefaultPlayers) * 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);
    }

    public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
    {
        // Find and remove the players avatar
        if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
        {
            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;

        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 OnSceneLoadDone(NetworkRunner runner) { }
    public void OnSceneLoadStart(NetworkRunner runner) { }

    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);
            }
        }
    }

    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,
            SceneObjectProvider = gameObject.AddComponent<NetworkSceneManagerBase>()
        });
    }

    public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken)
    {
        Debug.Log("On Host Migration");
    }
}
```

```NetworkInputData.cs
using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
    public Vector3 direction;
}
```

```Player.cs
using Fusion;

public class Player : NetworkBehaviour
{
    private NetworkCharacterController _cc;

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

    public override void FixedUpdateNetwork()
    {
        if (GetInput(out NetworkInputData data))
        {
            data.direction.Normalize();
            _cc.Move(5 * data.direction * Runner.DeltaTime);
        }
    }
}
```

* プレイヤー用のPrefabを作成する

Prefab概要
```
PlayerPrefab → スクリプトをアタッチ
    - Body → モデル（メッシュ）　Cubeを配置している
    - Collision → あたり判定　BoxColliderを設定

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/f3b9b18a-71fc-5d42-f61d-8056727e0848.png)

PlayerPrefab
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/97509cf4-7fee-0482-98a0-8c946b45ac32.png)

Body
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/2de94d42-7170-c7ea-5ed6-044632227dc3.png)

Collision
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/9bd43569-4e86-4e65-7f1d-b8d81f05c45e.png)

# シーンにSpawnオブジェクトを配置

* ```Spawn```オブジェクトを生成し、```BasicSpawner.cs```をアタッチする
* ```BasicSpawner```に```PlayerPrefab```をアタッチする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/52ac5e5d-5ea8-b1f0-2816-93fd27ae6a51.png)

* 地面判定用にPlaneを配置しています

# 完了

* 正しく設定できていればこれでマルチプレイが行えるはずです
1. Hostを押す
2. 別クライアントでJoinを押す
※1PCでマルチプレイを試す場合は、事前にビルドしてexe化してから「UnityEditor+ビルドしたexe」で行うとやりやすいです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119898/5b46e262-6cc8-73dd-bd01-4498bfd5db13.png)

* もし、正しく動作しない場合は下記のものを見てみると良いかもしれません（作業していてミスしそうなだなと思った工程を列挙しています）

「アプリケーションIDはFusionのものか？」
「アセットシリアライゼーションのテキストはForceTextに設定しているか？」
「パッケージマネージャーにMono.Cecilを導入しているか？」
「間違えてFusiionのSDKではなくRealtimeのSDKを導入していないか？」
　
* それでも動作しない場合は公式ドキュメントを見て設定しなおしてみる

環境構築

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

最小構成のサンプル

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

# ステップアップ（公式ドキュメントやサンプルを確認する）

公式ドキュメントをすすめる

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

公式サンプルのTankプロジェクト　ガッツリつくりこんである印象　中身を覗いてみると良いかもしれません

https://doc.photonengine.com/en-US/fusion/current/samples/fusion-tanknarok

VR版サンプル

https://doc.photonengine.com/en-us/fusion/current/hello-fusion/hello-fusion-vr