2 - シーンのセットアップ
概要
ここでは、Fusionの基本的なシーンのセットアップ方法を説明します。
この章では、以下の内容を学びます。
- セッションを開始したり、セッションに参加したりするための
NetworkRunner
- 自由に移動操作可能なキャラクター
Fusionの開始
Fusionを開始するには、NetworkRunner
でStartGame
メソッドを呼び出す必要があります。そのためには、あらかじめシーン上にNetworkRunner
コンポーネントを置いておくか、コードから追加するかのどちらかが必須です。いずれにせよ、ネットワーク周りのロジックの大部分はある程度のコードを記述することを要求されるため、このチュートリアルではコード中心で進めていきます。
Unityでデフォルトのシーンを開き、以下の手順を行ってください。
- 新規で空のゲームオブジェクトを作成する
- 上記に新規でスクリプトを追加する
- 上記のスクリプトの名前を
BasicSpawner
にする - スクリプトを開き、
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
メソッド内でNetworkRunner
をBasicSpawner
と同一のゲームオブジェクトに追加することで、NetworkRunner
はINetworkRunnerCallbacks
を実装した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で、以下の手順を行ってください。
- 新規で空のゲームオブジェクトを作成し、名前を
PlayerPrefab
にする - 上記に
NetworkObject
コンポーネントを追加する
NetworkObject
によって、ゲームオブジェクトにはネットワークIDが付与され、全てのピアがそのオブジェクトを参照できるようになります。ユーザーはこのオブジェクトをアバターとして操作することになるため、NetworkCharacterController
も必要です。これは必須ではありませんが、プレイヤーが操作するオブジェクトのプロトタイプ作成に便利なので、ここではこれを追加します。NetworkCharacterController
が依存しているUnityのCharacterController
コンポーネントは自動的に追加されます。
一般的なアドバイスとして、ネットワークオブジェクトとビジュアル表現は別に分けておくのが良いでしょう。これを実現するには、以下の手順を行ってください。
PlayerPrefab
の子オブジェクトに、Unity標準のCube
を追加する- 上記の名前の
Body
にする - 上記の
Collider
を削除する
アバターの構成は、以下のようになります。
プロジェクトを保存すると、Fusionは新しいネットワークオブジェクトをベイクします。その後、オブジェクトをプロジェクトのフォルダへドラッグしてアバターのプレハブを作成し、シーンからオブジェクトを削除します。
アバターのスポーン
ホストモードで実行するゲームでは、ホストのみが新しいオブジェクトをスポーンする権限を持っています。つまり、全てのプレイヤーアバターは、プレイヤーがセッションへ参加した時に、ホストがスポーンする必要があるということです。ちょうどINetworkRunnerCallbacks
インターフェースのOnPlayerJoined
メソッドが、そのタイミングに最適です。
ちなみに、プレイヤーがセッションから切断した時には、OnPlayerLeft
が呼び出されます。
OnPlayerJoined
とOnPlayerLeft
のスタブを、以下のコードで置き換えてください。
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エディターに戻って、作成したアバターのプレハブをBasicSpawner
のPlayer Prefab
フィールドにアタッチすることを忘れないようにしてください。
入力の収集
入力権限を持っていても、クライアントはネットワーク上の状態を直接更新することは許可されません。かわりに、ホストがネットワーク上の状態を更新するための入力構造体を提供することができます。
クライアントは入力をローカル上で適用して、ユーザーに即時のフィードバックを提供することができますが、それはあくまでローカル上の予測であって、ホストによって上書きされる可能性があります。
入力をユーザーから収集する前に、入力情報を格納するデータ構造を定義する必要があります。NetworkInputData
と名付けた新規スクリプトを作成し、以下のような構造体を作成してください。
C#
using Fusion;
using UnityEngine;
public struct NetworkInputData : INetworkInput
{
public Vector3 direction;
}
簡単のため、この例では移動方向を指すベクトルを使用しています。より帯域幅の消費量が少ない方法を取りたいなら、例えば、各方向1ビットのビットフィールドを使用すると良いでしょう。ただしFusionは入力を圧縮して、実際に変更があったデータのみを送信するため、時期尚早な最適化に躍起にならないようにしてください。
Fusionが入力をポーリングする時にOnInput
コールバックが呼び出されるので、そこでクライアントは入力を収集する必要があります。BasicSpawner
のOnInput()
スタブを、以下のように置き換えてください。
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側に渡していて、これでホストやクライアントが入力権限を持つオブジェクトの操作が可能になります。
入力の適用
最終ステップとして、収集した入力データをプレイヤーアバターに適用します。
PlayerPrefab
を選択する- 上記に新規スクリプトを追加し、名前は
Player
にする - 上記スクリプトを開き、
MonoBehaviour
をNetworkBehaviour
にする - 上記スクリプトに
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)
の位置に設置し、X
とZ
のスケールを100
に調整します。新規マテリアルを割り当てて、オブジェクトと床の色を変えておきましょう。
アプリケーションをビルドして、複数のインスタンスを起動(または、Unityエディターから直接インスタンスを実行)します。Host
ボタンを押すクライアントは一人で、それ以外のクライアントはJoin
を押してください。
Unityエディターでゲームを実行する場合、ビルドされたアプリケーションに比べてフレームレートが非常に高くなる可能性があるため、実際よりも予測が正確になりジッターが抑えられる結果になることがあります。正確を期すなら、スタンドアロンビルドを実行してテストをお試しください。
Back to top