This document is about: FUSION 2
SWITCH TO

2 - シーンのセットアップ

概要

ここでは、Fusionの基本的なシーンのセットアップ方法を説明します。

この章では、以下の内容を学びます。

  • セッションを開始したり、セッションに参加したりするためのNetworkRunner
  • 自由に移動操作可能なキャラクター

ネットワーク入力の詳細はこちらのマニュアルをご覧ください

Fusionの開始

Fusionを開始するには、NetworkRunnerStartGameメソッドを呼び出す必要があります。そのためには、あらかじめシーン上にNetworkRunnerコンポーネントを置いておくか、コードから追加するかのどちらかが必須です。いずれにせよ、ネットワーク周りのロジックの大部分はある程度のコードを記述することを要求されるため、このチュートリアルではコード中心で進めていきます。

Unityでデフォルトのシーンを開き、以下の手順を行ってください。

  1. 新規で空のゲームオブジェクトを作成する
  2. 上記に新規でスクリプトを追加する
  3. 上記のスクリプトの名前をBasicSpawnerにする
  4. スクリプトを開き、BasicSpawnerクラスにINetworkRunnerCallbacksを実装し、各メソッドのスタブを追加する

C#

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

public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks 
{
  public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { }
  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){ }
}

INetworkRunnerCallbacksを実装することで、BasicSpawnerクラスがFusionのNetworkRunnerと連携できるようになります。NetworkRunnerはFusionの最も重要なコア要素で、実際のネットワークシミュレーションを実行します。

StartGameメソッド内でNetworkRunnerBasicSpawnerと同一のゲームオブジェクトに追加することで、NetworkRunnerINetworkRunnerCallbacksを実装したBasicSpawnerを自動的に検出し、コールバックメソッドを呼び出すようになります。

BasicSpawnerのスクリプトに以下のコードを追加してください。

C#

private 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;

    // Create the NetworkSceneInfo from the current scene
    var scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex);
    var sceneInfo = new NetworkSceneInfo();
    if (scene.IsValid) {
        sceneInfo.AddSceneRef(scene, LoadSceneMode.Additive);
    }
    
    // Start or join (depends on gamemode) a session with a specific name
    await _runner.StartGame(new StartGameArgs()
    {
        GameMode = mode,
        SessionName = "TestRoom",
        Scene = scene,
        SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
    });
}

StartGameメソッド内では、まずNetworkRunnerを作成して、このクライアントが入力を提供することを知らせます。それから、固定のセッション名・特定のゲームモード(ゲームモードについては後述)を指定した新しいセッションを開始します。ここで渡されているシーンのIndexはホストのみで使用され、クライアントはホストが指定したシーンを使用します。最後に、デフォルトのSceneMaagerを(おまけで)指定しています。(SceneManagerは、シーン上に直接置かれているネットワークオブジェクトのインスタンス化処理を行うものですが、厳密に言うと、今回の例ではそのようなオブジェクトは存在しないため必須ではありません)

Fusionは様々なネットワークトポロジーに対応していますが、ここではホストモード限定の説明を行います。ホストモードでは、一つのピアがサーバーとクライアントを兼ねてセッションを作成します。残りのピアはクライアントとして、存在するセッションに参加します。

これに対応するため、ユーザーがセッションをホストするか・既存のセッションに参加するかを選択する方法を提供する必要があります。簡単のため、以降の例ではUnityのIMGUIを使用します。以下のメソッドをBasicSpawnerクラスに追加してください。

C#

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

このアプリケーションを実行すると、ユーザーは新しいセッションをホストできるようになり、他のユーザーはそのセッションに参加できるようになります。ただし現状は、何のインタラクション要素もなく、データの通信も行われていないため、シングルプレイゲームと何も変わらないように見えるでしょう。

プレイヤーアバターの作成

これを遊べるゲームにするためには、各プレイヤーには入力を提供する方法が与えられて、シーン上にはプレイヤーアバターのような存在が必要です。

Unityで、以下の手順を行ってください。

  1. 新規で空のゲームオブジェクトを作成し、名前をPlayerPrefabにする
  2. 上記にNetworkObjectコンポーネントを追加する

NetworkObjectによって、ゲームオブジェクトにはネットワークIDが付与され、全てのピアがそのオブジェクトを参照できるようになります。ユーザーはこのオブジェクトをアバターとして操作することになるため、NetworkCharacterControllerも必要です。これは必須ではありませんが、プレイヤーが操作するオブジェクトのプロトタイプ作成に便利なので、ここではこれを追加します。NetworkCharacterControllerが依存しているUnityのCharacterControllerコンポーネントは自動的に追加されます。

一般的なアドバイスとして、ネットワークオブジェクトとビジュアル表現は別に分けておくのが良いでしょう。これを実現するには、以下の手順を行ってください。

  1. PlayerPrefabの子オブジェクトに、Unity標準のCubeを追加する
  2. 上記の名前のBodyにする
  3. 上記のColliderを削除する

アバターの構成は、以下のようになります。

プレイヤーアバターの設定
プレイヤーアバターの設定

プロジェクトを保存すると、Fusionは新しいネットワークオブジェクトをベイクします。その後、オブジェクトをプロジェクトのフォルダへドラッグしてアバターのプレハブを作成し、シーンからオブジェクトを削除します。

アバターのスポーン

ホストモードで実行するゲームでは、ホストのみが新しいオブジェクトをスポーンする権限を持っています。つまり、全てのプレイヤーアバターは、プレイヤーがセッションへ参加した時に、ホストがスポーンする必要があるということです。ちょうどINetworkRunnerCallbacksインターフェースのOnPlayerJoinedメソッドが、そのタイミングに最適です。

ちなみに、プレイヤーがセッションから切断した時には、OnPlayerLeftが呼び出されます。

OnPlayerJoinedOnPlayerLeftのスタブを、以下のコードで置き換えてください。

C#

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

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    if (runner.IsServer)
    {
        // Create a unique position for the player
        Vector3 spawnPosition = new Vector3((player.RawEncoded % runner.Config.Simulation.PlayerCount) * 3, 1, 0);
        NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
        // Keep track of the player avatars for easy access
        _spawnedCharacters.Add(player, networkPlayerObject);
    }
}

public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
    if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
    {
        runner.Despawn(networkObject);
        _spawnedCharacters.Remove(player);
    }
}

基本的にはUnityのInstantiate()メソッドをrunner.Spawn()に置き換えているだけで、メソッドのパラメーターもほぼ同じなので、見覚えがあるコードになっていると思います。最後のパラメーターは、アバターの入力権限を持たせるプレイヤーの参照です。入力権限はオブジェクトの「所有権」とは異なることに注意してください。詳細は後述します。

Unityエディターに戻って、作成したアバターのプレハブをBasicSpawnerPlayer Prefabフィールドにアタッチすることを忘れないようにしてください。

入力の収集

入力権限を持っていても、クライアントはネットワーク上の状態を直接更新することは許可されません。かわりに、ホストがネットワーク上の状態を更新するための入力構造体を提供することができます。

クライアントは入力をローカル上で適用して、ユーザーに即時のフィードバックを提供することができますが、それはあくまでローカル上の予測であって、ホストによって上書きされる可能性があります。

入力をユーザーから収集する前に、入力情報を格納するデータ構造を定義する必要があります。NetworkInputDataと名付けた新規スクリプトを作成し、以下のような構造体を作成してください。

C#

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
  public Vector3 direction;
}

簡単のため、この例では移動方向を指すベクトルを使用しています。より帯域幅の消費量が少ない方法を取りたいなら、例えば、各方向1ビットのビットフィールドを使用すると良いでしょう。ただしFusionは入力を圧縮して、実際に変更があったデータのみを送信するため、時期尚早な最適化に躍起にならないようにしてください。

Fusionが入力をポーリングする時にOnInputコールバックが呼び出されるので、そこでクライアントは入力を収集する必要があります。BasicSpawnerOnInput()スタブを、以下のように置き換えてください。

C#

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

これもかなり見覚えがあるコードになっているはずです。コールバックメソッド内では、Unity標準のInputから入力を収集して、前もって定義した構造体に入力を格納しています。メソッドの最終行で入力構造体をFusion側に渡していて、これでホストやクライアントが入力権限を持つオブジェクトの操作が可能になります。

入力の適用

最終ステップとして、収集した入力データをプレイヤーアバターに適用します。

  1. PlayerPrefabを選択する
  2. 上記に新規スクリプトを追加し、名前はPlayerにする
  3. 上記スクリプトを開き、MonoBehaviourNetworkBehaviourにする
  4. 上記スクリプトにFixedUpdateNetwork()を実装する(Fusionのシミュレーションループと連動させる)

C#

using Fusion;

public class Player : NetworkBehaviour
{
  public override void FixedUpdateNetwork(){}
}

FixedUpdateNetworkはティックごとに呼び出されるメソッドです。過去の正しいネットワーク上の状態を適用する「ロールバック」から、現在の(それまで予測していた)ローカル上のティックまでの「再シミュレーション」のため、このメソッドは1フレームの間に複数回実行されることがあります。

ティックごとの正しい入力を確実に適用するためは、入力をFixedUpdateNetworkで適用する必要があります。ティックに対応した入力はGetInput()というシンプルなメソッドから取得できます。入力を取得したら、NetworkCharacterControllerから実際の動作をアバターに反映させましょう。

Playerクラスの完全な実装は、以下のようになります。

C#

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

チートを防ぐため、取得した入力を正規化している点に着目してください。

テスト

ここで残りは、本当にゲーム内でアバターを移動操作できるのかを検証するのみになりました。ただその前に、スポーンしたオブジェクトが画面外に落ちてしまわないように、シーンには床を用意する必要があります。

Cubeオブジェクトを作成して、(0,-1,0)の位置に設置し、XZのスケールを100に調整します。新規マテリアルを割り当てて、オブジェクトと床の色を変えておきましょう。

アプリケーションをビルドして、複数のインスタンスを起動(または、Unityエディターから直接インスタンスを実行)します。Hostボタンを押すクライアントは一人で、それ以外のクライアントはJoinを押してください。

Unityエディターでゲームを実行する場合、ビルドされたアプリケーションに比べてフレームレートが非常に高くなる可能性があるため、実際よりも予測が正確になりジッターが抑えられる結果になることがあります。正確を期すなら、スタンドアロンビルドを実行してテストをお試しください。

次は ホストモード入門 3 - 予測

Back to top