入力
はじめに
入力は、Quantumコアアーキテクチャの重要なコンポーネントになります。決定論的ネットワーキングライブラリでは、システムの出力は与えられた入力によって完全に決定されます。つまり、ネットワーク内のすべてのクライアントの入力が同じであれば、出力も同じになります。
DSLでの定義
入力は、任意のDSLファイルで定義できます。例えば、移動方向とジャンプボタンを持つ入力構造体は、次のようになります。
Qtn
input
{
button Jump;
FPVector3 Direction;
}
サーバーは、ティックにおける完全なセット(すべてのプレイヤー入力)を確定し、それらをまとめて送信します。そのため、構造体はできるだけ最小サイズに抑えてください。
コマンド
決定論的コマンドは、Quantumの別の入力方法です。任意のデータとサイズを持つことができるため、「アイテムを購入する」「どこかにテレポートする」などの特殊な入力に最適です。
Unityでのポーリング
入力をQuantumシミュレーションへ送信するために、Unity内で入力をポーリングしてください。これを行うには、ゲームプレイシーンのMonoBehaviour
でPollInput
コールバックを購読してください。
C#
private void OnEnable()
{
QuantumCallback.Subscribe(this, (CallbackPollInput callback) => PollInput(callback));
}
そして、コールバック内で入力ソースを読み込んで、入力構造体へデータを渡します。
C#
public void PollInput(CallbackPollInput callback)
{
Quantum.Input i = new Quantum.Input();
var direction = new Vector3();
direction.x = UnityEngine.Input.GetAxisRaw("Horizontal");
direction.y = UnityEngine.Input.GetAxisRaw("Vertical");
i.Jump = UnityEngine.Input.GetKey(KeyCode.Space);
// 固定小数点数に変換する
i.Direction = direction.ToFPVector3();
callback.SetInput(i, DeterministicInputFlags.Repeatable);
}
備考:ここでは、浮動小数点数から固定小数点数への変換が、シミュレーションへ共有される前に行われているため、決定論的です。
最適化
Quantum 3は入力をデルタ圧縮するものの、帯域幅の最適化のためにInput
の生データを出来るだけコンパクトにするのが、一般的なグッドプラクティスです。以下にいくつかの最適化方法を示します。
ボタン
キー入力の表現には、ブール型や類似のデータ型ではなく、DSLの入力定義でButton
型を使用します。これはインスタンスごとに1ビットしか使用しないため、可能な限り使用することが望ましいです。ネットワーク上では1ビットしか使用しませんが、ローカルではもう少し多くのゲームステートを保持します。1ビットは現在のフレームでボタンが押されたかどうかのみを表現し、残りの情報はローカルで計算されます。
ボタンは以下のように定義されます。
Qtn
input
{
button Jump;
}
Unityスクリプトからボタンの値をポーリングする際の重要な点は、「現在のボタンの状態(現在のフレームで押されているかどうか)」をポーリングすることです。これによって、Quantumは内部プロパティを自動的に設定して、ユーザーはシミュレーションコード上でWasPressed
・IsDown
・WasReleased
などの特定の状態をポーリングできます。
つまり、UnityではGetKeyUp()
/GetKeyDown()
などの特定の状態を設定する必要はありません。これらを使用すると、UnityとQuantumとの実行レートの違いから、いくつかの状態が失われ、入力の反応性が悪くなる問題が発生する可能性があります。
したがって、入力構造体のbutton
値を設定する際は、以下のように常に現在のボタンの状態をポーリングするようにしてください。
C#
// Unity内でプレイヤー入力をポーリングする
input.Jump = UnityEngine.Input.GetKey(KeyCode.Space);
ボタンの状態は、Quantumシミュレーションコードからも更新できます。ユーザーが入力構造体とbutton
型を使用してボットエンティティも更新する場合など、非プレイヤーエンティティのボタン押下をシミュレートするのに非常に便利です。これを実現するには、以下のようにシミュレーションコードでボタンの状態を毎フレーム設定する必要があります。
C#
// Quantumコード内
input.button.Update(frame, value);
そうすると、特定の状態(Pressed
・Down
・Released
)も内部に生成されます。ボタンの状態を毎フレーム更新しないと、これら状態が誤って設定されることになります。
方向のエンコード
典型的には、移動は方向ベクトルで表されることが多く、DSL
ファイルで以下のように定義されます。
Qtn
input
{
FPVector2 Direction;
}
しかし、FPVector2
は2つのFP
から構成されていて、16バイトのデータになります。同じルームに多数のクライアントがいる場合、送信データ量は大きくなる可能性があります。
これを最適化する方法の1つは、Input
構造体を拡張して、方向ベクトルをベクトルではなくByte
にエンコードして送信することです。実装の一例は次の通りです。
まず、通常通りに入力を定義し、方向のFPVector2
をByte
に置き換えて、エンコードされた値を保存するようにします。
Qtn
input
{
Byte EncodedDirection;
}
次に、コンポーネントの拡張と同じ方法で入力構造体を拡張します。(機能の追加を参照)
C#
namespace Quantum
{
partial struct Input
{
public FPVector2 Direction
{
get
{
if (EncodedDirection == default)
return default;
Int32 angle = ((Int32)EncodedDirection - 1) * 2;
return FPVector2.Rotate(FPVector2.Up, angle * FP.Deg2Rad);
}
set
{
if (value == default)
{
EncodedDirection = default;
return;
}
var angle = FPVector2.RadiansSigned(FPVector2.Up, value) * FP.Rad2Deg;
angle = (((angle + 360) % 360) / 2) + 1;
EncodedDirection = (Byte) (angle.AsInt);
}
}
}
}
この実装によって、使用感は以前と同じままで、16バイトが1バイトになります。これは、Direction
プロパティを利用して、EncodedDirection
の値を自動的にエンコード/デコードしています。