【Unity】Fusionを使って接続→操作まで作るメモ

Fusionを使って接続→操作までの処理を作ります。
プレイヤーが抜けたときなどの処理は記述せず、
なるべく接続→操作できるまでのデバッグに必要最低限なもののみに絞ります。


Fusionのインストール・登録

Fusion公式サイトへ行き、パッケージマネージャをインストールします。
公式のチュートリアルを踏めば問題なく進められると思います。
doc.photonengine.com

接続処理を作る

ネットワークへ接続するクラス、NetworkConnectManager .csを作ります。
デバッグ用のボタンが押された際に接続処理をさせるコンポーネントです。

スクリプトの大枠を作る

まずは以下のように処理を記述していきます。
INetworkRunnerCallbacksを継承させたクラスです。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Fusion;
using Fusion.Sockets;

public class NetworkConnectManager : MonoBehaviour, INetworkRunnerCallbacks
{

    /// <summary>
    /// 接続状態を表す列挙型
    /// </summary>
    private enum State
    {
        Offline = 0,
        Connecting,
        Online,
    };

    /// <summary>
    /// 接続状態
    /// </summary>
    private State m_state=State.Offline;

    /// <summary>
    /// デバッグ用GUI
    /// </summary>
    private void OnGUI()
    {
        GUILayout.Box($"NetworkState : {m_state}");

        if(m_state==State.Offline)
        {
            // ボタンを押したら接続
            if (GUILayout.Button("Network Connect"))
            {
                NetworkConnect();
            }
        }
    }

    /// <summary>
    /// 接続処理
    /// </summary>
    private async void  NetworkConnect()
    {
        // 接続処理を記述
    }

    #region INetworkRunnerCallbacks
    // INetworkRunnerCallbacksの関数をすべて記述(後述)
     #endregion
}

State列挙型は現在の状態を可視化するために使用します。
ここから、接続処理をさせるNetworkConnect()を書いていきます。

#region INetworkRunnerCallbacksについて

INetworkRunnerCallbacksで使用している関数は以下の通りです。
Photon Fusion: INetworkRunnerCallbacks Interface Reference

以下の処理を記述してください。
コメントは翻訳したもの使用して付けています。

        /// <summary>
        /// NetworkRunner がサーバーまたはホストに正常に接続したときのコールバック
        /// </summary>
        /// <param name="runner"></param>
        public void OnConnectedToServer(NetworkRunner runner) { }

        /// <summary>
        /// NetworkRunner がサーバーまたはホストへの接続に失敗したときのコールバック
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="remoteAddress">リモートアドレス</param>
        /// <param name="reason">理由</param>
        public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }

        /// <summary>
        /// NetworkRunner がリモート クライアントから接続要求を受信したときのコールバック
        /// </summary>
        /// <param name="runner">ローカルNetworkRunner</param>
        /// <param name="request">リクエスト</param>
        /// <param name="token">トークン</param>
        public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }

        /// <summary>
        /// 認証手順が認証サーバーから応答を返すと、コールバックが呼び出されます
        /// </summary>
        /// <param name="runner">このオブジェクトが存在するランナー</param>
        /// <param name="data">カスタム認証応答値</param>
        public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }

        /// <summary>
        /// NetworkRunner がサーバーまたはホストから切断されたときのコールバック
        /// </summary>
        /// <param name="runner"></param>
        public void OnDisconnectedFromServer(NetworkRunner runner) { }

        /// <summary>
        /// ホスト移行プロセスの開始時にコールバックが呼び出される
        /// </summary>
        /// <param name="runner">このオブジェクトが存在するランナー</param>
        /// <param name="hostMigrationToken">Fusion Runnerを再起動するために必要なすべての情報を格納する移行トークン</param>
        public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }

        /// <summary>
        /// ユーザー入力をポーリングするNetworkRunnerからのコールバック。提供される NetworkInput は次を想定しています。
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="input"></param>
        public void OnInput(NetworkRunner runner, NetworkInput input) { }

        /// <summary>
        /// ユーザー入力失敗のコールバック
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="player"></param>
        /// <param name="input"></param>
        public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }

        /// <summary>
        /// 新しいプレイヤーが参加したときのNetworkRunnerからのコールバック
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="player"></param>
        public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { }

        /// <summary>
        /// プレーヤーが切断されたときのNetworkRunnerからのコールバック
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="player"></param>
        public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="player"></param>
        /// <param name="data"></param>
        public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment<byte> data) { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="runner"></param>
        public void OnSceneLoadDone(NetworkRunner runner) { }
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="runner"></param>
        public void OnSceneLoadStart(NetworkRunner runner) { }

        /// <summary>
        /// このコールバックは、Photon Cloudからセッションの新しいリストを受信したときに呼び出されます
        /// </summary>
        /// <param name="runner"></param>
        /// <param name="sessionList"></param>
        public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { }

        /// <summary>
        /// ランナーがシャットダウンされたときに呼び出されます
        /// </summary>
        /// <param name="runner">シャットダウン中のランナー</param>
        /// <param name="shutdownReason"> Fusion がシャットダウンされた理由を説明します</param>
        public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { }


        /// <summary>
        /// このコールバックは、手動でディスパッチされたシミュレーション メッセージがリモート ピアから受信されたときに呼び出されます
        /// </summary>
        /// <param name="runner">このメッセージの対象となるランナー</param>
        /// <param name="message">メッセージポインタ</param>
        public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }

NetworkConnect()の記述

接続処理を記述します。

    /// <summary>
    /// 接続処理
    /// </summary>
    private async void  NetworkConnect()
    {
        m_state = State.Connecting;

        var runner = GetComponent<NetworkRunner>();

        // 接続時設定構造体
        var startGameArgs = new StartGameArgs()
        {
            GameMode = GameMode.AutoHostOrClient,
        };

        // 接続開始
        await runner.StartGame(startGameArgs);

        // INetworkInputを使用して
        // 入力情報を使えるようにする
        runner.ProvideInput = true;

        m_state = State.Online;
    }

StartGameArgsは接続時にRunnerに渡す構造体です。
Photon Fusion: StartGameArgs Struct Reference

今回はGameModeGameMode.AutoHostOrClientに変更しています。
Fusion側でホスト・クライアントを自動的に振り分けてくれる設定です。
Photon Fusion: Fusion Namespace Reference

接続用のGameObjectを作る

空のGameObjectを生成し、NetworkRunnerコンポーネントを付け、
その後、同じGameObjectに先ほど記述したNetworkConnectManager.csを追加してください。


INetworkRunnerCallbacksを継承したクラスは
基本的にNetworkRunnerと同じGameObject内に配置しないと
INetworkRunnerCallbacks内のコールバック関数が呼ばれません。

動作確認をして、接続できていることを確かめます。

別GameObjectでINetworkRunnerCallbacksを使用する

もしNetworkRunnerではないGameObjectで使用したい時や、
MonoBehaviourを継承させないクラスなどでコールバックをさせたい場合は、
Runner.AddCallbacks()を使用し、
引数にINetworkRunnerCallbacksを継承したクラスを入れることで使用可能です。
Photon Fusion: NetworkRunner Class Reference

public class SuperCallBacks : INetworkRunnerCallbacks
{
    // #region INetworkRunnerCallbacks...
}

public class TMPBehaviour : MonoBehaviour
{
    private SuperCallBacks m_super  = null;

    private void OnGUI()
    {
        m_super = new SuperCallBacks();

        // 追加
        var runner = FindObjectOfType<NetworkRunner>();
        runner.AddCallbacks(m_super);

        // 削除
        //runner.RemoveCallbacks(m_super);
    }
}

キャラクター生成処理を作る

キャラクター生成するクラス、CharacterSpawner.csを作ります。
接続に成功した際、キャラクターを生成する処理を持つクラスです。

生成場所のGameObjectを作る

生成場所となる空のGameObjectを1つ以上生成します。

キャラクターのPrefabを作る

空のGameObjectを生成し、その下にキャラクターのモデルを置きます。
その後、キャラクターのRootGameObjectにNetworkObjectと、
NetworkTransformコンポーネントを追加します。

NetworkTransformのInterpolation Targetに
Rootより一個下のGameObjectの参照を持たせておきます。


NetworkObjectは同期させたいGameObjectに必ず使用するコンポーネントです。

NetworkTransformは付けられているGameObjectの
Transformを同期してくれるコンポーネントです。

その後、プロジェクトフォルダに入れてPrefab化させた後に
Fusion -> RebuildPrefabTable を押します。
プレハブ | Photon Engine

生成処理を作る

以下のように処理を記述します。
NetworkConnectManager.cs側からSpawn()を呼べるようにしておきます。

using UnityEngine;
using Fusion;
public class CharacterSpawner : MonoBehaviour
{
    /// <summary>
    /// 生成プレハブ 
    /// </summary>
    [SerializeField]
    private NetworkPrefabRef m_prefab = NetworkPrefabRef.Empty;

    /// <summary>
    /// 生成位置
    /// </summary>
    [SerializeField]
    private Transform[] m_spawnPoints = null;

    /// <summary>
    /// 生成
    /// </summary>
    /// <param name="playerRef">プレイヤー情報</param>
    public void Spawn(NetworkRunner runner, PlayerRef playerRef)
    {
        var obj = runner.Spawn(m_prefab, GetSpawnPostion(playerRef), Quaternion.identity, playerRef);
        runner.SetPlayerObject(playerRef, obj);
    }

    /// <summary>
    /// スポーン位置の取得
    /// </summary>
    /// <param name="player">プレイヤー情報</param>
    /// <returns>スポーン位置</returns>
    private Vector3 GetSpawnPostion(PlayerRef player)
    {
        if (m_spawnPoints == null || m_spawnPoints.Length <= 0)
        {
            return Vector3.zero;
        }

        int index = player % m_spawnPoints.Length;
        return m_spawnPoints[index].position;
    }
}

Instantiate()ではなくRunner.Spawn()で生成します。
また、Runnerで識別しやすいようにSetPlayerObject()でセットしておきます。

NetworkConnectManagerから生成処理を呼ぶ

上記の処理をNetworkConnectManager.cs側で呼びます。

INetworkRunnerCallbacks.OnPlayerJoined()を使用し、
プレイヤーが参加した時に呼ばれるようにしておきます。

// NetworkConnectManager.cs

    /// <summary>
    /// 新しいプレイヤーが参加したときのNetworkRunnerからのコールバック
    /// </summary>
    /// <param name="runner"></param>
    /// <param name="player"></param>
    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) 
    {
        var spawner = GetComponent<CharacterSpawner>();
        spawner.Spawn(runner, player);
    }

コンポーネントを追加する

NetworkRunner&NetworkConnectManager.csが付いた
GameObjectにCharacterSpawnerコンポーネントを追加し、
作成したCharacterPrefab・スポーンポイントを設定します。


動作確認をして、出現していることを確かめます。

【GIF】

入力処理を作る

入力を取り扱うクラス、InputPoller.csを作ります。
今回はキャラクターの移動に使用します。

以下のように処理を記述します。
INetworkRunnerCallbacksを継承させたクラスです。

using System;
using System.Collections.Generic;
using UnityEngine;
using Fusion;
using Fusion.Sockets;

/// <summary>
/// 入力構造体
/// </summary>
public struct CharacterInput : INetworkInput
{

    /// <summary> 移動 </summary>
    public Vector2 Direction;
}

public class InputPoller : MonoBehaviour, INetworkRunnerCallbacks
{
    /// <summary>
    /// ユーザー入力をポーリングするNetworkRunnerからのコールバック。提供される NetworkInput は次を想定しています。
    /// </summary>
    /// <param name="runner"></param>
    /// <param name="input"></param>
    public void OnInput(NetworkRunner runner, NetworkInput input)
    {
        // 値の読み取り
        Vector2 direction = Vector2.zero;
        if (Input.GetKey(KeyCode.W)) direction.y += 1.0f;
        if (Input.GetKey(KeyCode.S)) direction.y -= 1.0f;
        if (Input.GetKey(KeyCode.D)) direction.x += 1.0f;
        if (Input.GetKey(KeyCode.A)) direction.x -= 1.0f;
        direction.Normalize();

        // 値のセット
        input.Set(new CharacterInput
        {
            Direction = direction,
        });
    }

    // #region INetworkRunnerCallbacks
    // ...
    // #endregion
}

INetworkInputを継承した構造体を作り、そこに入力値を割り当てます。
NetworkRunnerが付いたGameObjectにInputPoller.csを追加します。

移動処理を作る

キャラクターの移動処理を扱うクラス、NetworkCharacterMovement.csを作ります。

スクリプトを作る

以下のように処理を記述します。
NetworkBehaviourを継承したクラスです。

using UnityEngine;
using Fusion;

public class NetworkCharacterMovement : NetworkBehaviour
{
    [SerializeField]
    [Min(1.0f)]
    private float m_speed;

    /// <summary>
    /// 通信同期に対応した更新
    /// </summary>
    public override void FixedUpdateNetwork()
    {
        // 入力値の受け取り
        if (GetInput<CharacterInput>(out var input))
        {
            Vector3 move = new Vector3(input.Direction.x, 0.0f, input.Direction.y);
            transform.position += move * m_speed * Runner.DeltaTime;
        }
    }
}

GetInput<INetworkInput継承構造体>()で3で記述した入力の値が返されます。
ローカルで入力した情報を送信し、それを受け取って移動を行います。
Photon Fusion: NetworkBehaviour Class Reference

transformはNetworkTransformを付けているため自動的に同期されます。

Prefabに追加する

CharacterPrefabのRootObjectにNetworkCharacterMovement.csを追加します。


動作確認をして、出現していることを確かめます。


参考にしたサイト

Photon Fusion: Photon Fusion API Documentation