Lag Compensation
概要
Server Mode
および Host Mode
遅延補正は、ペースの速いマルチプレイヤーゲームにおける根本的な問題を解決します。クライアントを信頼することなく、クライアントにWYSIWYG (ウィジウィグ:見たものが、手に入るもの) エクスペリエンスを与えます。
問題は、ネットワーク上のマシンのどれもが、ゲーム内でまったく同じティックにないことです。あるクライアントに見えていて、アクションのベースにしているものは、そのクライアントにとって100%正しいものでしかありません。典型的な例としては、遠くのオブジェクトに対する正確なショット検出です。クライアントはターゲットに直接照準を合わせていますが、実際にはターゲットはすでに移動しています。
- もしオーソリティブなサーバーが、自分自身の世界認識のみに基づいて命中検出を行った場合、誰も意図的に何かに命中させることはできません。
- もしクライアントに権限が与えられ、何をヒットしたかをサーバーに伝えることができれば、システムは単純なゲーム破壊に対して非常に脆弱性の高いものになってしまうでしょう。
遅延補正により、サーバーは各クライアントの視点から一瞬だけ世界を見ることができ、実際に不可能なショットを打てる位置にいたかどうかを判断することができます。残念なことに、これは、ターゲットが壁の向こうに安全に引き離されたと思っていたにもかかわらず、ヒットしてしまう可能性があることを意味します。
Fusionは、ヒットボックスが以前どこにあったかの履歴を保持し、各クライアントのビューが現在のサーバーの状態からどのくらい遅れているかを把握しています。この履歴を使って、Fusionは過去のレイキャストを行うことで、ラグを補正したレイキャストを行うことができます。
超高精度を実現するために、Fusionはさらに一歩進んだ遅延補正を行います。AAAゲームのフレームレートは通常、ネットワークのティックレートよりも高いです。プレイヤーが画面上で実際に見ているのは、通常、個別のティックではなく、2つのティックの間の内挿です。Fusionは、遅延補正されたレイキャストが2ティック間のどの程度で行われたかを正確に把握しており、これを利用してサブティック精度のレイキャストを行うことができます。
遅延補正機能
Fusionを使用するゲーム開発者にとって、このようなマジックはほとんど透明です。必要なのは、ビルド済みの HitboxRoot
と Hitbox
コンポーネントだけです。
ヒットボックスルート
ネットワークオブジェクトに遅延補正されたヒットボックスを設定するには、まずGameObjectの一番上のノードにHitboxRoot
コンポーネントを用意します。この HitboxRoot
は、GameObject やその子オブジェクトにあるすべての Hitbox
コンポーネントをグループ化する役割を果たします。
さらに HitboxRoot
はヒットボックスの境界球を提供し、遅延補正システムのブロードフェーズデータ構造で使用されます。この体積はルートの BroadRadius
フィールドと Offset
フィールドで設定でき、グループ化されたすべてのヒットボックスを包含する必要があります。
ヒットボックス
それぞれの Hitbox
は、ラグを補正してクエリできる単一のボリュームを表します。ネットワーク接続された GameObject に遅延補正されたヒットボックスを設定す るには、以下の手順が必要です:
1.GameObjectの一番上のノードにHitboxRoot
コンポーネントが必要です;
- 通常の Unity の
Collider
コンポーネントを持つダイナミックオブジェクトは すべてのHitbox
ノードや静的ジオメトリとは別のレイヤーにする必要があります。これは、ダイナミックコライダーへのヒットを回避しつつ、ラグを補正したレイキャストをHitbox
コンポーネントのないすべての静的ジオメトリにヒットさせるための最速かつ唯一の確実な方法です。ダイナミックコライダーは物理的な相互作用に必要なので、完全に削除することはできませんが、ダイナミックオブジェクトの遅延補正ヒット検出はヒットボックスのみに依存するべきです。これを解決するには、ダイナミックコライダーを遅延補正レイキャストで無視できるレイヤーに保つことです。
注意: ヒットボックスはそれらが定義されるGameObjectのレイヤーを使用します。同じHitboxRoot
の下にグループ化されていても、異なるヒットボックスは同一のレイヤーを共有する必要はありません。
注意: 1つの HitboxRoot
につき31個の Hitbox
ノードという制限があります。1つのオブジェクトやプレハブにそれ以上の子ノードが必要な場合は、階層を分解して複数のルートに分散させる必要があります。
ヒットボックス階層の具体的な構造は、完全にそれぞれゲームのニーズ次第となります。
クエリ
FusionのLag-compensated APIでは、物理クエリとしてレイキャスト、スフィアオーバーラップ、ボックスオーバーラップをサポートしており、PhysXの対応するクエリと非常によく似た構文になっています。また、特定のHitbox
の正確な位置と回転を問い合わせることも可能です。
C#
using System.Collections.Generic;
using Fusion;
using UnityEngine;
public class LagCompensationExampleBehaviour : NetworkBehaviour {
public float rayLength = 10.0f;
public float cooldownSeconds = 1.0f;
public LayerMask layerMask = -1;
[Networked]
public TickTimer ActiveCooldown { get; set; }
private readonly List<LagCompensatedHit> _hits = new List<LagCompensatedHit>();
public override void FixedUpdateNetwork() {
if (GetInput<NetworkInputPrototype>(out var input)) {
if (ActiveCooldown.ExpiredOrNotRunning(Runner) && input.Buttons.IsSet(NetworkInputPrototype.BUTTON_FIRE)) {
// reset cooldown
ActiveCooldown = TickTimer.CreateFromSeconds(Runner, cooldownSeconds);
// perform lag-compensated query
Runner.LagCompensation.RaycastAll(transform.position, transform.forward, rayLength, player: Object.InputAuthority, _hits, layerMask, clearHits: true);
for (var i = 0; i < _hits.Count; i++) {
// proceed with gameplay logic
}
}
}
}
}
player
パラメータは、どの視点からクエリを解決するかを指定し、通常はこのヒットスキャン/プロジェクタイルをコントロールしている Object.InputAuthority
に帰属します。言い換えると、レイキャストは、それを制御していた特定のプレーヤークライアントが見ていたタイムフレームに一致するデータに対して行われます。正確な目盛りや内挿パラメータを指定することもできますが、複雑な計算を追加する必要はありません。
Hitboxes へのクエリ以外に、hit options で IncludePhysX
フラグが指定されている場合、遅延補正されたレイキャストも通常のPhysXコライダーをクエリできます。この場合、通常のUnityのコライダーは遅延補正されず、クエリが解決されるときに現在の状態で見られることに注意してください。
重要: 同じ Unity インスタンスで複数のピアを実行している場合、Runner.GetPhysicsScene
または GetPhysicsScene2D
を使用して、ピアのそれぞれの物理シーンを取得することができます。これは、通常のUnityコライダーを含む遅延補正クエリによって自動的に行われますが、マルチピアのコンテキストで通常のPhysicsクエリを実行する場合は注意が必要です。
サブティック精度
デフォルトでは、遅延補正されたクエリは、正規化された内挿係数(アルファ)とビジュアライゼーションがどちらの状態に近いかに応じて、クライアントのView内挿に使用されている "From "または "To "状態のいずれかに対して解決されます。
多くのゲームではこのレベルの精度で十分ですが、hit options にサブティック精度フラグを含めることで、クエリの精度をさらに向上させることができます。
C#
Runner.LagCompensation.OverlapSphere(position, radius, player: Object.InputAuthority, hits, options: HitOptions.SubtickAccuracy);
ヒットのフィルタリング
レイヤーマスクとフラグ
すべての遅延補正クエリでは、レイヤーマスク を使用してどのレイヤーを考慮すべきかを定義し、他のレイヤーのヒットボックスをフィルタリングすることができます。それに加えて、hit options に IgnoreInputAuthority
フラグを含めると、クエリを実行するプレイヤー(入力権限)がコントロールするオブジェクトに属するすべてのヒットボックスが自動的に無視されます。
C#
// this can be cached or defined on the Inspector with a LayerMask field
var layerMask = LayerMask.GetMask("Player", "Destructible");
var options = HitOptions.IgnoreInputAuthority;
Runner.LagCompensation.Raycast(transform.position, transform.forward, rayLength, player: Object.InputAuthority, out var hit, layerMask, options);
フィルタリングコールバック
クエリの大まかな解決によって見つかったすべての HitboxRoot
エントリを前処理するコールバックを提供することも可能です。
C#
using System.Collections.Generic;
using Fusion;
using Fusion.LagCompensation;
public class MyBehaviourFoo : NetworkBehaviour {
private readonly List<LagCompensatedHit> _hits = new List<LagCompensatedHit>();
private PreProcessingDelegate _preProcessingCachedDelegate;
private void Awake() {
// Caching a delegate avoids recurrent delegate allocation from method.
// Using a lambda expression (not a closure) will also prevent that (delegate cached by compiler).
_preProcessingCachedDelegate = PreProcessHitboxRoots;
}
public override void FixedUpdateNetwork() {
if (GetInput<NetworkInputPrototype>(out var input) && input.Buttons.IsSet(NetworkInputPrototype.BUTTON_FIRE)) {
var hitsCount = Runner.LagCompensation.RaycastAll(transform.position, transform.forward, 10, Object.InputAuthority, _hits, preProcessRoots: _preProcessingCachedDelegate);
if (hitsCount > 0) {
// proceed with gameplay logic
}
}
}
private static void PreProcessHitboxRoots(ref Query query, List<HitboxRoot> candidates, HashSet<int> preProcessedColliders) {
// HB root candidates can be iterated over (in reverse order, in order to remove entries while iterating)
for (var i = candidates.Count - 1; i >= 0; i--) {
var root = candidates[i];
if (root.name == "Please Ignore Me") {
// removed roots will not be processed any further
candidates.RemoveAt(i);
continue;
}
// it is possible to iterate over the hitboxes of each root
for (var j = 0; j < root.Hitboxes.Length; j++) {
var hb = root.Hitboxes[j];
// e.g. bypass the layer mask and Hitbox Active-state checks
if (hb.name == "Always Narrow-Check Me") {
preProcessedColliders.Add(hb.ColliderIndex);
}
}
}
}
}
前処理メソッドやラムダを自己完結させ、クロージャを作成しないようにするために、カスタム引数として遅延補正クエリにオブジェクトを渡すこともできます。
C#
var query = SphereOverlapQuery.CreateQuery(transform.position, 10.0f, player: Object.InputAuthority,
preProcessRoots: (ref Query query, List<HitboxRoot> candidates, HashSet<int> preProcessedColliders) => {
if (query.UserArgs is MyCustomUserArgs myUserArgs) {
Log.Info($"User Arg: {myUserArgs.Arg}");
}
});
query.UserArgs = new MyCustomUserArgs { Arg = 42 };
var hitCount = Runner.LagCompensation.ResolveQuery(ref query, _hits);
Lag Compensation in 2D Games
現時点では、Hitbox
は球かボックスの形状としてしか記述できない。しかし、これらの形状はロックされた平面(例えば、XY平面)に配置されクエリされた場合には、それぞれ2Dの円やボックスのエミュレートに使用できます。
C#
if (Runner.LagCompensation.Raycast(transform.position, Vector2.right, length: 10, Object.InputAuthority, out var hit)) {
if (hit.Hitbox.TryGetComponent(out NetworkRigidbody2D nrb)) {
nrb.Rigidbody.AddForceAtPosition(Vector2.up, hit.Point);
}
}
注意: クエリオプションで IncludePhysX
フラグを使用すると、常にUnityの3D物理エンジン(PhysX)をクエリします。通常の2Dコライダをクエリするには、ピアのそれぞれのPhysics2Dシーンで別のクエリを実行することが可能です。
C#
var hitboxHitsCount = Runner.LagCompensation.RaycastAll(transform.position, Vector2.right, 10, Object.InputAuthority, hitboxHits);
var colliderHitsCount = Runner.GetPhysicsScene2D().Raycast(transform.position, Vector2.right, 10, results: colliderHits);
Back to top