This document is about: FUSION 2
SWITCH TO

Lag Compensation

Overview

Lag Compensation is only available in Server Mode and Host Mode

Lag compensation solves a fundamental problem in fast-paced multiplayer games: giving clients a WYSIWYG (What You See Is What You Get) experience despite not being able to trust them.

The problem is that none of the machines on the network are at exactly the same tick in the game. What one client sees and bases their actions on is only 100% correct from their own perspective. A classic example is detecting a precise shot at a distant object—even though the client has the target directly in their crosshairs, in reality it has already moved.

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

Lag compensation allows the server to temporarily view the world from each client's perspective and decide if they were in fact in a position to take that impossible shot. Unfortunately, this means that the target may be hit even if they felt they were already safely tucked away behind a wall. However, this is much less likely to be noticeable.

Fusion keeps a history of where hitboxes were previously located and knows how far behind each client's view is compared to the current server state. Using this history, Fusion is able to compensate for lag 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, so what the player actually sees 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 to achieve sub-tick raycast accuracy.

Lag-Compensated Features

For game developers using Fusion, all this magic is nearly invisible. 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, a HitboxRoot component must first be attached to 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.

The HitboxRoot also provides a bounding sphere for the hitboxes, which is 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.

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. Dynamic objects with regular Unity Collider components should be kept in a separate layer from all Hitbox nodes and static geometry. This is the fastest and only reliable way to avoid hitting dynamic Colliders while still allowing lag-compensated raycasts to hit all static geometry without Hitbox components. Removing dynamic Colliders altogether is not an option as they are needed for physics interactions, but lag-compensated hit detection for dynamic objects should rely exclusively on Hitboxes. The solution is to keep dynamic Colliders on a layer that can be ignored by lag-compensated raycasts.

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. The layer of the Hitbox is cached when it gets registered into the lag compensation buffer, If the layer will be changed later, use Hitbox.SetLayer(int layer) method to correctly change and cache the new layer into the Hitbox.

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 each individual game.

Queries

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

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

        }
      }
    }
  }
}

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 is in control of it. All of this happens automatically—there's no need to do any complex math, although providing the exact ticks and interpolation parameters is also an option.

Besides querying Hitboxes, a lag-compensated raycast can also query regular PhysX colliders if the IncludePhysX flag is specified in 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 done automatically by lag-compensated queries that include regular Unity colliders, but requires attention when performing regular Physics queries in a multi-peer context.

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 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 SubtickAccuracy flag in its hit options, thereby querying the interpolated state between those two ticks using the exact interpolation factor seen by the player when the input was polled.

C#

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

Filtering Hits

Layer Mask and Flags

All lag-compensated queries use a layer mask to define which layers should be considered and filter out Hitboxes on other layers. Including the IgnoreInputAuthority flag in the hit options will cause all hitboxes belonging to an object controlled by the player performing the query (the input authority) to be automatically ignored.

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

Filtering Callback

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.

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

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.

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

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, by positioning and querying them in a locked plane (e.g. the XY plane).

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

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.

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