Lag Compensation
Overview
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:
A
HitboxRoot
component is needed on the topmost node of the GameObject.Dynamic objects with regular Unity
Collider
components should be kept in a separate layer from allHitbox
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 withoutHitbox
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