This page is a work in progress and could be pending updates.

Lag Compensation

Overview

Lag Compensation is only available in Server Mode and Host Mode

Lag compensation solves a fundamental problem in fast-paced network games: giving a client a WYSIWYG (What You See Is What You Get) experience without being able to trust the client.

The problem is none of the machines on the network are at exactly the same tick in the game. What one client sees and base their actions on is only 100% correct for them. The classic example is detecting a precise shot at a distant object; although the client has the target directly in their crosshairs, in reality it has already moved.

  • If the authoritative server base the hit detection solely on its own perception of the world, no one will ever intentionally hit anything.
  • If the client is allowed authority and can tell the server what they hit, the system will be wide open to simple game-destroying exploits.

Lag compensation allows the server to, momentarily, see the world from each clients perspective and decide if they were in fact in a position to take that impossible shot. On the flip side, this means the target may be hit although they themselves felt they were safely tugged away behind a wall; however, this is much less likely to be noticeable.

Fusion keeps a history of where hitboxes were located in the past and knows how far behind each clients view is compared to the current server state. Using this history Fusion is able to perform lag compensation raycasts by raycasting in the past.

For ultra high precision, Fusion takes lag compensation one step further. The framerate of AAA games is usually higher than the tick rate of the network. What the player actually see on screen is usually not at a discrete tick, but an interpolation between two ticks. Fusion knows exactly how far between two ticks the lag compensated raycast was made and can use this for sub-tick accurate raycast.

Back To Top

Lag Compensated Features

For game developers using Fusion all this magic is near transparent. All that is needed are the prebuilt HitboxRoot and Hitbox components.

Hitbox Root

In order to set up a lag compensated hitbox on a networked object, the first step required is to have a HitboxRoot component on the topmost node of the GameObject. The HitboxRoot serves as a way to group all Hitbox components found on the GameObject and/or its children.

Besides that, a HitboxRoot also provides a bounding sphere for the hitboxes, to be used in the broad-phase data structure of the lag-compensation system. This volume can be configured through the root's BroadRadius and Offset fields and must encompass all grouped hitboxes, including volumes they might occupy during the lifetime of the object due to animations, for instance.

Back To Top

Hitbox

Each Hitbox represents a single volume that can be queried with lag-compensation. To set up a lag compensated hitbox on a networked GameObject, the following steps are required:

  1. A HitboxRoot component is needed on the topmost node of the GameObject;

  2. Hitbox nodes should be kept in a separate layer from regular Unity Collider components for all dynamic objects; this is the fastest and only reliable way to separate dynamic Colliders from Hitboxes when raycasting. This separation allows lag compensated raycasts to hit all static geometry while avoiding the need to add Hitbox components which would be prohibitively expensive. At the same time all dynamic hits should rely exclusively on Hitboxes, but removing dynamic Colliders entirely does not work because they are needed to allow objects to interact nicely with PhysX. The middle ground is to keep the dynamic colliders on a layer that can be ignored by lag compensated ray-casts.

N.B.: Hitboxes use the layer of the GameObject in which they are defined. Different hitboxes do NOT need to share the same layer, even if they are grouped under the same HitboxRoot.

N.B.: There is a limit of 31 Hitbox nodes per HitboxRoot. If there is a need for more child nodes in a single object or prefab, the hierarchy must be broken down and distributed across several roots.

The specific structure of the hit-box hierarchy is entirely up to the needs of the specific game.

Back To Top

Queries

Fusion's Lag-compensated API includes support for raycasts, sphere overlaps and box overlaps as physics queries, the syntax to perform them being very similar to their PhysX counterparts. It is also possible to query which exact position and rotation a specific Hitbox had.

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

        }
      }
    }
  }
}

The player parameter specifies from which perspective the query should be resolved and is usually attributed to the Object.InputAuthority that is in control of this hitscan/Projectile. In other words, the raycast will be done against the data matching the timeframe viewed by the specific player client who was in control of it. All of this happens automatically, no need to add any complex math, although providing the exact ticks and interpolation parameters is also an option.

Besides querying Hitboxes, a lag-compensated query can also be resolved regular colliders kept within PhysX if the IncludePhysX flag is specified on the hit options. Note that, in this case, regular Unity colliders are not lag-compensated, but seen in their current state when the query is resolved.

N.B.: When running multiple peers on the same Unity instance, it is possible to retrieve the peer's respective physics scene through Runner.GetPhysicsScene or GetPhysicsScene2D. This is automatically done by a lag-compensated query that includes regular Unity colliders, but requires attention when performing regular Physics queries in a multi-peer context.

Back To Top

Sub-tick Accuracy

By default, lag-compensated queries are resolved against either the "From" or "To" states being used for View interpolation on the client, depending on the normalized interpolation factor (alpha) and to which state the visualization is closer to.

Although this level of accuracy can be enough for many games, it is possible to improve the precision of the query even further by including the sub-tick accuracy flag in its hit options and instead query an interpolated state between those two ticks using the exact interpolation factor seen by the player when the input was polled.

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

Back To Top

Filtering Hits

Layer Mask And Flags

All lag-compensated queries allow a layer mask to effectively define which layers should be considered and filter out Hitboxes in other layers. Besides that, if including the IgnoreInputAuthority flag in the hit options, all hitboxes belonging to an object controlled by the player performing the query (input authority) are automatically ignored.

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

Back To Top

Filtering Callback

In addition to those common filtering strategies, it is also possible to provide a callback to pre-process all the HitboxRoot entries found by the broad-phase resolution of the query, allowing for more game-specific filtering logic.

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

An object can also be passed in with a lag-compensated query as a custom argument, in order to allow pre-processing methods or lambdas to be self-contained and not create closures.

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

Back To Top

Lag Compensation In 2D Games

Currently, a Hitbox can only be described as a 3D shape: sphere or box. However, these shapes can still be used to emulate a 2D circle or box, respectively, if positioned and queried in a locked plane (e.g. the XY plane).

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

N.B.: Using the IncludePhysX flag in the query options will always query Unity's 3D physics engine (PhysX). In order to query regular 2D colliders, it is possible to perform a separate query in the peer's respective Physics2D scene.

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


To Document Top