ラグ補償
概要
ラグ補償は、ハイペースなマルチプレイヤーゲームにおける根本的な問題を解決して、信頼できないクライアントにWYSIWYG(見たままが得られる)体験を与えます。
その問題とは、どのネットワーク上の端末も、全く同じティックではゲームが進行していないということです。クライアントは、自身の視点を100%正しいものとして見て、それに基づいてアクションを起こします。遠くのオブジェクトに対する正確な射撃を判定する典型的な例では、クライアントはターゲットに直接照準を合わせているにもかかわらず、実際のターゲットは既にそこから移動している可能性があります。
- もし、権限のあるサーバーが自身の世界認識のみに基づいて当たり判定を行うと、誰も射撃を意図した通りに命中させることはできないでしょう
- もし、クライアントに権限を与えて当たり判定をサーバーに伝えられるようにすると、システムは単純なチートに対して非常に脆弱になります
ラグ補償によって、サーバーは一時的に各クライアントの視点から世界を見て、実際に射撃が命中する位置にいたかどうかを判定することができます。これは残念ながら、既に壁の向こうに安全に隠れたはずのターゲットに射撃が命中する可能性が生まれることを意味しますが、大きく目立つことはほとんどありません。
Fusionは、ヒットボックスの過去の履歴を保持し、各クライアントが現在のサーバーの状態からどれほど遅れているかを知っています。そのため、履歴を使用して過去にレイキャストすることで、その遅延を補償することができます。
Fusionの先進的なラグ補償は、超高精度を実現しています。AAAタイトルのゲームのフレームレートは、ネットワークのティックレートより高いことが多いため、プレイヤーが画面上で実際に見るものは、特定のティックの状態ではなく、ティック間を補間した状態になるのが普通です。Fusionは、ティック間のどこでラグ補償レイキャストが行われたのかを厳密に把握しているため、サブティック精度でレイキャストを行うことができます。
ラグ補償機能
ラグ補償は、Fusionを使用するゲーム開発者からはほぼ透過的に処理されます。HitboxRoot
とHitbox
コンポーネントの準備のみが必要です。
Hitbox Root
ネットワークオブジェクトにラグ補償ヒットボックスを設定するには、まず最上位のゲームオブジェクトにHitboxRoot
コンポーネントを追加する必要があります。HitboxRoot
は、ゲームオブジェクトとその子孫オブジェクトから、すべてのHitbox
コンポーネントを検索し、それらをグループ化します。
また、HitboxRoot
ではヒットボックス全体の包含球が与えられ、ラグ補償システムのブロードフェーズで使用されます。球のボリュームは、BroadRadius
とOffset
フィールドから設定できます。ただしボリュームは、ヒットボックスのグループ全体を包含する必要があります。(オブジェクトがアニメーションで動く場合は、それを含む全体を覆う必要があります)
Hitbox
各Hitbox
は、単一のボリュームを表し、ラグ補償のクエリに使用されます。ネットワークオブジェクトにラグ補償ヒットボックスを設定するには、以下の手順が必要です。
- 最上位のゲームオブジェクトに
HitboxRoot
コンポーネントを追加する。 - Unity標準の
Collider
コンポーネントが付いた物理オブジェクトは、Hitbox
ノードや静的ジオメトリとレイヤーを分けてください。これはラグ補償レイキャストを、物理オブジェクトにはヒットさせずに、Hitbox
コンポーネントが付いていない静的ジオメトリにヒットさせるための、手軽で唯一確実な方法です。物理的なインタラクションが必要な物理オブジェクトからCollider
を完全に削除することはできませんが、ラグ補償の当たり判定はヒットボックスに依存することができます。これによって、レイヤー分けされた物理オブジェクトは、ラグ補償レイキャストから無視されます。
注意: ヒットボックスは、ゲームオブジェクトのレイヤーを使用します。同じHitboxRoot
でグループ化されている異なるヒットボックスを、異なるレイヤーに置いても問題ありません。Hitbox
のレイヤーは、ラグ補償のバッファに登録された時にキャッシュされます。後からレイヤーを変更する場合は、その変更を正しく反映するためにHitbox.SetLayer(int layer)
メソッドを使用して、Hitbox
を新しいレイヤーにキャッシュしてください。
注意: 1つのHitboxRoot
につき、Hitbox
ノードは最大31個に制限されています。1つのオブジェクトやプレハブにそれ以上のHitbox
ノードが必要な場合は、複数のルートオブジェクトに分解してください。
具体的なヒットボックスの階層構造は、完全に各ゲームの要求次第となります。
クエリ
Fusionのラグ補償APIは、物理クエリとして、Raycast
・OverlapSphere
・OverlapBox
に対応しています。構文はUnityのPhysics
(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
パラメーターは、どのプレイヤーの視点からクエリを解決するかを指定し、通常は、即着弾(Hitscan)/発射物(Projectile)を制御するObject.InputAuthority
になります。言い換えると、発射物などを制御する特定のプレイヤーが見ていたタイムフレームに一致するデータに対して、レイキャストが行われます。特定のティックや補間パラメーターを渡すこともできますが、処理はすべて自動的に行われるため、開発者側で複雑な計算をする必要はありません。
HitOptionsのIncludePhysX
フラグを指定すると、ラグ補償レイキャストは、ヒットボックスだけでなく、Unity標準(PhysX)のCollider
をクエリできます。しかしこの場合、Unity標準のCollider
はラグ補償されないため、クエリ解決時の状態で判定されることに注意してください。
注意: マルチピアモードを実行している場合は、ピア視点の物理シーンをRunner.GetPhysicsScene
/GetPhysicsScene2D
から取得できます。これは、ラグ補償クエリ(Unity標準のCollider
を含む)では自動的に行われますが、マルチピアのコンテキストでUnity標準の物理クエリを実行する場合には注意が必要です。
サブティック精度
デフォルトでは、ラグ補償クエリは、クライアントの補間で使用される「From」または「To」のいずれかの状態で解決されます。これは、正規化された補間係数(Alpha)と、どちらの状態のビジュアルが近いかに応じて決まります。
多くのゲームではデフォルトの精度で十分ですが、 HitOptionsのSubtickAccuracy
フラグを指定すると、プレイヤーの入力が収集された際の厳密な補間係数を使用して、ティック間を補間した状態でクエリが行われるため、クエリの精度をさらに高めることができます。
C#
Runner.LagCompensation.OverlapSphere(position, radius, player: Object.InputAuthority, hits, options: HitOptions.SubtickAccuracy);
判定のフィルタリング
レイヤーマスクとフラグ
すべてのラグ補償クエリは、レイヤーマスクで対象レイヤーを定義して、他のレイヤーのヒットボックスを除外することができます。HitOptionsの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);
2Dゲームでのラグ補償
現在、Hitbox
は3D形状(Sphere
/Box
)しかありません。しかし、固定の平面(XY平面など)に配置してクエリを実行すれば、2D形状(Circle
/Box
)としてエミュレートして使用することができます。
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)でクエリを実行します。Unity標準の2DのCollider
のクエリを実行したい場合は、ピア視点の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