このページは編集中です。更新が保留になっている可能性があります。

Lag Compensation

概要

遅延補正はServer Mode および Host Modeでのみ使用いただけます。

遅延補正は、テンポの速いネットワークゲームにおける基本的な問題(クライアントを信頼できないまま、クライアントにWYSIWYGの体験をさせること)を解決するものです。

問題は、ネットワーク上のどのマシンも、ゲームの中で全く同じ時間にいるわけではないということです。あるクライアントが見ているものや行動の根拠は、そのクライアントにとって100%正しいものでしかありません。典型的な例は、遠くにある物体への正確な射撃を検出する場合です。クライアントは標的を直接狙っていますが、実際には標的はすでに移動しています。

  • もし、権威あるサーバーが自分の世界認識のみに基づいて当たり判定を行うならば、誰も意図的に何かに当たることはないでしょう。
  • もし、クライアントに権限が与えられ、何に当たったかをサーバーに伝えることができると、ゲームを破壊するような悪意にさらされてしまいます。

ラグ補正により、サーバーは各クライアントの視点から世界を一時的に見て、実際に不可能なはずのショットを打てる位置にいたかどうかを判断することができます。クライアントも、壁の後ろなどの安心な場所に避難しているつもりが打たれてしまったことになりますが、これは比較的目立ちません。

Fusionは、過去にHitBox が置かれた場所の履歴を保持しており、各クライアントの視界が現在のサーバーの状態と比べてどのくらい遅れているかを把握しています。この履歴を利用して過去にレイキャストすることで、ラグ補正レイキャストを行うことができます。( 注意: クライアントは、自分が入力権限を持っているオブジェクトについてのみ、サーバーよりも先に進みます)

超高精度を実現するために、Fusionはラグ補正をさらに一歩進めました。AAAゲームのフレームレートは、通常、ネットワークのティックレートよりも高くなっています。プレイヤーが実際に画面上で見ているものは、通常、個別の刻みではなく、2つの刻みの間の補間になります。Fusionでは、遅延補正されたレイキャストが2つのティックの間のどのくらいの距離で行われたかを正確に把握しているので、これを利用してサブティック精度のレイキャストを行うことができます。

トップに戻る

ラグ補正機能

Fusionを使っているゲーム開発者にとって必要なのは、通常のUnity Collidersの代わりに、あるいはそれに加えて、あらかじめ組み込まれた HitBox コンポーネントだけです。

Hitbox Root

ネットワークに接続されたオブジェクトに遅延補償ヒットボックスを設定するには、まず最初に GameObject の最上位ノードに HitboxRoot コンポーネントを配置する必要があります。このHitboxRootはGameObjectやその子オブジェクトにあるすべてのHitboxコンポーネントをグループ化する役割を果たします。

さらに、HitboxRoot はヒットボックスの境界球を提供し、ラグ補正システムのブロードフェーズデータ構造で使用されます。このボリュームはルートの BroadRadiusOffset フィールドを通して設定することができ、例えばアニメーションによってオブジェクトのライフタイム中に占めるかもしれないボリュームを含めて、グループ化されたすべてのヒットボックスを包含しなければなりません。

トップに戻る

Hitbox

Hitboxは、遅延補正で問い合わせることができる一つのボリュームを表します。ネットワークに接続されたGameObjectに遅延補償されたヒットボックスを設定するには、以下の手順が必要です。

  1. ゲームオブジェクトの最上位ノードに、HitBoxRootコンポーネントが必要です。

  2. HitBoxノードは、すべてのダイナミックオブジェクトに対して、通常のUnityのColliderコンポーネントとは別のレイヤーに置くべきです。これは、レイキャスティングの際に、ダイナミックなColliderとHitboxを分離するための、最速かつ唯一信頼できる方法です。この分離により、ラグ補正されたレイキャストがすべての静的ジオメトリにヒットするようになりますが、その一方で、負荷のかかる HitBox コンポーネントの追加を避けることができます。同時に、すべての動的なヒットは、HitBoxにのみ依存すべきですが、動的なColliderを完全に取り除くことは、オブジェクトがPhysXとうまく相互作用するために必要であるため、うまくいきません。中間的な方法としては、遅延補正されたレイキャストによって無視できるレイヤーにダイナミックコライダを置いておくことです。

追記: ヒットボックスは、それが定義されているGameObjectのレイヤーを使用します。異なるヒットボックスが同じ HitboxRoot の下にグループ化されている場合でも、同じレイヤーを共有する必要はありません。

追記: 1つのHitBoxRootにつき、31個のHitBoxノードが制限されています。1つのオブジェクトやプレハブにそれ以上の子ノードが必要な場合は、階層を分解して複数のルートに分散させる必要があります。

Hitbox階層の具体的な構造は、特定のゲームのニーズに完全に依存します。

トップに戻る

クエリ

FusionのLag-compensated API には物理クエリーとしてレイキャスト、スフィアオーバラップ、ボックスオーバラップをサポートしており、これらを行うための構文はPhysXと非常に類似しています。また、特定の Hitbox がどの正確な位置と回転を持つかを問い合わせることも可能です

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 に起因します。言い換えれば、レイキャストは、それを制御していた特定のプレイヤークライアントが見た タイムフレーム に一致するデータに対して行われます。これはすべて自動的に行われ、複雑な計算を追加する必要はありません。ただし、正確な刻みと補間パラメータを提供することもできます。

Hitboxのクエリの他に、hit optionsIncludePhysXフラグを指定すると、PhysX内にある通常のコライダーも遅延補償クエリで解決できます。この場合、通常のUnityコライダーは遅延補償されず、クエリーが解決されたときの現在の状態で見られることに注意してください。

N.B.: 同じ Unity インスタンスで複数のピアを実行している場合、ピアのそれぞれの物理シーンを Runner.GetPhysicsScene または GetPhysicsScene2D によって取得することが可能です。これは、通常の Unity コライダーを含むラグ補正クエリによって自動的に行われますが、マルチピ アのコンテキストで通常の Physics クエリを実行する場合は注意が必要です。

トップに戻る

サブティックの精度

デフォルトでは、遅延補償クエリは、正規化補間係数(アルファ)と可視化がどちらの状態に近いかによって、クライアント上のビュー補間に使用されている "From" または "To" 状態のどちらかに対して解決されます。

多くのゲームでは、この程度の精度で十分ですが、hit options に sub-tick accuracy(サブティックの精度)フラグを入れることによりクエリーの精度をさらに向上させ、入力ポーリングの際にプレーヤーが見た正確な補間係数でその2刻みの間に補間した状態をクエリーすることが可能です。

Runner.LagCompensation.OverlapSphere(position, radius, player: Object.InputAuthority, hits, options: HitOptions.SubtickAccuracy);

トップに戻る

ヒットのフィルタリング

トップに戻る

レイヤーマスクとフラグ

すべての遅延補償クエリでは、どのレイヤーを考慮し、他のレイヤーにあるHitboxをフィルタリングするかを効果的に定義するためにレイヤーマスクを使用することができます。また、hit optionsIgnoreInputAuthority フラグを指定すると、クエリを実行しているプレイヤー(入力権限)がコントロールしているオブジェクトに属するすべてのヒットボックスは、自動的に無視されることになります。

// 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 エントリを事前に処理するコールバックを提供することも可能で、よりゲーム固有のフィルタリングロジックが可能になります。

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);
        }
      }
    }
  }
}

前処理メソッドやラムダを自己完結させ、クロージャを作成しないようにするために、遅延補正クエリでオブジェクトをカスタム引数として渡すこともできます。

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形状としてのみ記述することができます。しかし、これらの形状は、ロックされた平面(例えば、XY平面)に配置し、クエリを実行すれば、それぞれ2Dの円や箱をエミュレートするために使用することは可能です。

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シーンで別途問い合わせを行うことができます。

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);


ドキュメントのトップへ戻る