ネットワーク入力
はじめに
入力の定義、ポーリング、消費は、Fusionの中核をなすものです。
Input Struct Definition
入力構造体の定義
入力構造体には、必要に応じて単純なデータや複雑なデータを格納することができます。Fusionは実際に変化するデータのみを送信します。そのため、データはコンパクトにしておく方が有利ですが(例:ボタンにフラグを使う)、あまり使われないフィールドを含めても問題ありません。例えば、ローカル プレイヤーが基本1人しかいない場合でも、複数のローカルプレイヤーから入力を収集する場合です。これらのプレイヤーが実際に入力を行っていない場合は、入力構造体の帯域幅の使用には影響しませんが、ローカルメモリには影響します。
入力構造体には、次のような制約があります。
INetworkInput
を継承していなければなりません。- プリミティブな型と構造体のみを含むことができます。
- 入力構造体とそれを保持する構造体は、トップレベルの構造体でなければならない(つまり、クラスの中にネストすることはできない)。
- C# では、プラットフォーム間で bool のサイズを統一していないため、1 ビットとして適切に直列化するために、
NetworkBool
を使用します。
Fusion は構造体の型をインテリジェントにマッピングします。これにより、プレイモードやゲームの各部分に応じて異なる構造体を使用することができます。入力をアンラップする際、Fusionは正しい型の入力のみを返します。
C#
public struct MyInput : INetworkedInput {
public Vector3 aimDirection;
}
ボタン
特別な NetworkButtons
型があります。これは、ボタンを押したことを INetworkInput
構造体に保存するための、便利なラッパーです。
入力構造体にボタンを追加するには、次のようにします。
- ボタン用の列挙型を作成します( 重要: それは明示的に定義され、0から始まる必要があります)。
INetworkedInput
にNetworkButtons
変数を追加します。
C#
enum MyButtons {
Forward = 0,
Backward = 1,
Left = 2,
Right = 3,
}
public struct MyInput : INetworkInput {
public NetworkButtons buttons;
public Vector3 aimDirection;
}
変数 NetworkButtons
に直接値を代入したり読み込んだりするために利用できる API は以下のとおりです。
void Set(int button, bool state)
: ボタンの enum 値とその状態 (pressed = true, not pressed = false) を受け取ります。bool IsSet(int button)
: ボタンの enum 値を受け取り、その状態をブール値で返します。
NetworkButtons
型はステートレスなので、ボタンの以前の状態に関するメタデータを保持しません。NetworkButtons
が提供する次のメソッド群を使用できるようにするには、ボタンの以前の状態を追跡する必要があります。これは、各プレイヤーの以前の状態の [Networked]
バージョンを作成することで簡単に行うことができます。
C#
public class PlayerInputConsumerExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
// Full snippet in the GetInput() section further down.
}
これにより、ボタンの現在の状態と以前の状態を比較し、ボタンが押されたばかりか、離されたばかりかを評価することができるようになります。
NetworkButtons GetPressed(NetworkButtons previous)
: 押されたばかりのすべてのボタンの値の集合を返します。NetworkButtons GetReleased(NetworkButtons previous)
: たった今離されたすべてのボタンの値のセットを返します。(NetworkButtons, NetworkButtons) GetPressedOrReleased(NetworkButtons previous)
: 押されただけのボタンと放されただけのボタンの値のタプルを返します。
重要: ボタンの値を割り当てるには Input.GetKey()
のみを使用します。Fusionのティックと同期していないため、見逃してしまう可能性があります。
ポーリング入力
Fusionはローカルクライアントをポーリングし、事前に定義された入力構造体に入力することで入力を収集します。Fusion Runnerは1つの入力構造体しか追跡しないので、予期せぬ動作を避けるために、入力ポーリングは一箇所で実装することを強く推奨します。
Fusion Runner は INetworkRunnerCallbacks.OnInput()
メソッドを呼び出すことで入力をポーリングします。 OnInput()
の実装は、INetworkInput
を継承した構造体に選択したデータを入力することができます。入力された構造体は、提供された NetworkInput
に対して Set()
を呼び出すことでFusionに返されます。
重要:
- 複数のポーリングサイトがあると、最後の入力構造体のバージョン以外はすべて上書きされます。
- 入力はローカルにしかポーリングされません(すべてのモード)。
SimulationBehaviour / NetworkBehaviour
SimulationBehaviour
または NetworkBehaviour
コンポーネントで OnInput()
を使用するには、INetworkRunnerCallbacks
インターフェースを実装して NetworkRunner.AddCallbacks()
を呼び出してローカルランナーにコールバック機能を登録します。
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
public void OnEnable(){
if(Runner != null){
Runner.AddCallbacks( this );
}
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
var myInput = new MyInput();
myInput.Buttons.Set(MyButtons.Forward, Input.GetKey(KeyCode.W));
myInput.Buttons.Set(MyButtons.Backward, Input.GetKey(KeyCode.S));
myInput.Buttons.Set(MyButtons.Left, Input.GetKey(KeyCode.A));
myInput.Buttons.Set(MyButtons.Right, Input.GetKey(KeyCode.D));
myInput.Buttons.Set(MyButtons.Jump, Input.GetKey(KeyCode.Space));
input.Set(myInput);
}
public void OnDisable(){
if(Runner != null){
Runner.RemoveCallbacks( this );
}
}
}
MonoBehaviourとPure CSharp
SimultionBehaviour
を継承していないスクリプトからの入力をポーリングするには、以下の手順を実行します。
InetworkRunnerCallbacks
とOnInput()
を実装する。- スクリプトの
AddCallbacks()
を呼び出して、スクリプトをNetworkRunner
に登録する。
C#
public class InputProvider : Monobehaviour, INetworkRunnerCallbacks {
public void OnEnable(){
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
myNetworkRunner.AddCallbacks( this );
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
// Same as in the snippet for SimulationBehaviour and NetworkBehaviour.
}
public void OnDisable(){
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
myNetworkRunner.RemoveCallbacks( this );
}
}
Unity新入力システム
Unity新入力システムを使用するには、プロセスは同じですが、作成された入力アクションから来る入力を収集する必要があります。
入力アクションを作成し、目的のボタンを定義したら、C#のクラスを生成し、コード内にそのインスタンスを作成します。OnInput()
で消費される入力がローカルキャッシュに保存されている限り、PlayerInput
クラスで来るイベントも使用することが可能です。
目標は、OnInput()
のボタンの状態を、古い入力システムではなく、新しい入力システムから来るものを集めることなので、システムの設定部分を除けば、他の部分は基本的に同じです。
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
// creating a instance of the Input Action created
private PlayerActionMap _playerActionMap = new PlayerActionMap();
public void OnEnable(){
if(Runner != null){
// enabling the input map
_playerActionMap.Player.Enable();
Runner.AddCallbacks(this);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var myInput = new MyInput();
var playerActions = _playerActionMap.Player;
myInput.buttons.Set(MyButtons.Jump, playerActions.Jump.IsPressed());
input.Set(myInput);
}
public void OnDisable(){
if(Runner != null){
// disabling the input map
_playerActionMap.Player.Disable();
Runner.RemoveCallbacks( this );
}
}
}
低いティックレートの入力をポーリングする
低ティックレートで入力を収集するには、Unity の Update 関数を使用して、記録された入力を構造体に蓄積し、後で消費できるようにする必要があります。
OnInput
では、この構造体が読み込まれ、input.Set()
コールによってFusionに正しく転送され、次のtickの入力のために蓄積を開始するためにリセットされます。
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
// Local variable to store the input polled.
MyInput myInput = new MyInput();
public void OnEnable() {
if(Runner != null) {
Runner.AddCallbacks( this );
}
}
public void Update()
{
if (Input.GetMouseButtonDown(0)) {
myInput.Buttons.Set(MyButtons.Attack, true);
}
if (Input.GetKeyDown(KeyCode.Space)) {
myInput.Buttons.Set(MyButtons.Jump, true);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
input.Set(myInput);
// Reset the input struct to start with a clean slate
// when polling for the next tick
myInput = default;
}
}
UIによる入力のポーリング
UI による入力のポーリングは、上記と同じロジックになります。UI経由で呼び出されるメソッドから NetworkButton
を設定し、 OnInput
で読み込みとリセットを行います。
入力の読み込み
シミュレーションで入力を読み込むと、以前にポーリングされた入力に基づいて、既存のネットワークの状態を現在の状態から新しい状態に変更することができます。Fusionは、入力構造体をネットワーク上で同期させ、入力権限を持つクライアントと状態権限を持つもの(ホスト)でシミュレーション中に利用できるようにします。
ポーリング入力とは逆に、入力の読み込みは必要な数だけ異なる場所で行うことができます。
注.: プレイヤー入力は、Input AuthoriyとState Authorityを持つクライアントでのみ利用可能です。ホストモードとサーバモードでは、プレイヤークライアントとホスト/サーバを指しますが、シェアードモードでは1つのクライアントを指します。
あるクライアントの入力を別のクライアントで読み取ることはできません。したがって、入力に依存する変更は、他のクライアントで複製するために [Networked]
状態として保存される必要があります。
GetInput()
入力構造体を取得するには、対象のオブジェクトに対して入力権限を持つ任意の NetworkBehaviour
の FixedUpdateNetwork()
内で GetInput(out T input)
を呼び出します(例えば、プレイヤーの動きを制御するコンポーネント)。GetInput()
の呼び出しは、以前に OnInput()
で入力されたものと同じ入力構造体を提供します。
以下の場合、GetInput()
の呼び出しは false を返します。
- クライアントが状態権限または入力権限を持っていない。
- 要求された入力の種類がシミュレーションに存在しない場合。
GameMode
固有の情報です。
HostMode
とServerMode
では、与えられたtickの入力はプレーヤーとホスト/サーバーのシミュレーションでのみ利用可能です。入力はプレイヤー間では__共有されません__。SharedMode
ではOnInput()
とGetInput()
のパターンを維持することは良いことですが、中央機関がないため、ローカルシミュレーションのみがローカルプレイヤーの入力にアクセスすることができます。入力はプレイヤー間で__共有されません__。
C#
using Fusion;
using UnityEngine;
public class PlayerInputConsumerExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
public override void FixedUpdateNetwork() {
if (GetInput<MyInput>(out var input) == false) return;
// compute pressed/released state
var pressed = input.Buttons.GetPressed(ButtonsPrevious);
var released = input.Buttons.GetReleased(ButtonsPrevious);
// store latest input as 'previous' state we had
ButtonsPrevious = input.Buttons;
// movement (check for down)
var vector = default(Vector3);
if (input.Buttons.IsSet(MyButtons.Forward)) { vector.z += 1; }
if (input.Buttons.IsSet(MyButtons.Backward)) { vector.z -= 1; }
if (input.Buttons.IsSet(MyButtons.Left)) { vector.x -= 1; }
if (input.Buttons.IsSet(MyButtons.Right)) { vector.x += 1; }
DoMove(vector);
// jump (check for pressed)
if (pressed.IsSet(MyButtons.Jump)) {
DoJump();
}
}
void DoMove(Vector3 vector) {
// dummy method with no logic in it
}
void DoJump() {
// dummy method with no logic in it
}
}
Runner.TryGetInputForPlayer()
NetworkRunner.TryGetInputForPlayer<T>(PlayerRef playerRef, out var input)
を呼び出すことで、NetworkBehaviour
の外側から入力を読み取ることができます。INetworkInput
型に加え、入力を取得するプレイヤーを指定する必要があります。つまり、入力権限を持つクライアントか、サーバー/ホストで、指定されたプレーヤーの入力を取得することができます。
C#
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
// Example for local player if script runs only on the client
if(myNetworkRunner.TryGetInputForPlayer<MyInput>(myNetworkRunner.LocalPlayer, out var input)){
// do logic
}
権限に関する注意
シミュレーションの完全な権限を保証するためには、入力構造体を生成する際に OnInput()
で入力値のみを収集することが重要です。入力に基づいて実行されるロジックは、完全に GetInput()
の中で行う必要があります。
例えば、弾丸を発射する場合は以下のような分岐になります。
OnInput()
: プレイヤーの発射ボタンの値を保存します。GetInput()
: 発射ボタンが押されたかどうかをチェックし、押されていれば弾丸を発射する。
1つのピアに複数のプレーヤー
「カウチ」、「画面分割」、「ローカル」マルチプレイヤーと呼ばれることがあります。複数の人が一つのピア(例えば、複数のコントローラを持つゲーム機)に入力を行い、同時にオンラインマルチプレイヤーゲームに参加することが可能です。Fusionは、一つのピアにいる全てのプレイヤーを一つのPlayerRef
として扱い(PlayerRef
は個々のプレイヤーではなく、ネットワークピアを識別します)、プレイヤー間の区別は行いません。プレイヤー とは何か、どのような入力を提供するのかを決めるのは、あなたです。
このユースケースを扱う方法の一例として、各プレイヤーに対して INetworkStruct
をネストした INetworkInput
構造体を定義する方法があります。
C#
public struct PlayerInputs : INetworkStruct
{
// All player specific inputs go here
public Vector2 dir;
}
public struct CombinedPlayerInputs : INetworkInput
{
// For this example we assume 4 players max on one peer
public PlayerInputs PlayerA;
public PlayerInputs PlayerB;
public PlayerInputs PlayerC;
public PlayerInputs PlayerD;
// Example indexer for easier access to nested player structs
public PlayerInputs this[int i]
{
get {
switch (i) {
case 0: return PlayerA;
case 1: return PlayerB;
case 2: return PlayerC;
case 3: return PlayerD;
default: return default;
}
}
set {
switch (i) {
case 0: PlayerA = value; return;
case 1: PlayerB = value; return;
case 2: PlayerC = value; return;
case 3: PlayerD = value; return;
default: return;
}
}
}
}
複数のプレーヤーの入力を収集する。
C#
public class CouchCoopInput : MonoBehaviour, INetworkRunnerCallbacks
{
public void OnInput(NetworkRunner runner, NetworkInput input)
{
// For this example each player (4 total) has one Joystick.
var myInput = new CombinedPlayerInputs();
myInput[0] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy1_X"), Input.GetAxis("Joy1_Y")) };
myInput[1] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy2_X"), Input.GetAxis("Joy2_Y")) };
myInput[2] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy3_X"), Input.GetAxis("Joy3_Y")) };
myInput[3] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy4_X"), Input.GetAxis("Joy4_Y")) };
input.Set(myInput);
}
// (removed unused INetworkRunnerCallbacks)
}
シミュレーションのための入力の取得
C#
public class CouchCoopController : NetworkBehaviour
{
// Player index 0-3, indicating which of the 4 players
// on the associated peer controls this object.
private int _playerIndex;
public override void FixedUpdateNetwork()
{
if (GetInput<CombinedPlayerInputs>(out var input))
{
var dir = input[_playerIndex].dir;
// Convert joystick direction into player heading
float heading = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, heading - 90, 0f);
}
}
}
Back to top