Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A LagCompensator Revamp #3770

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
169 changes: 61 additions & 108 deletions Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,59 @@
// Add this component to a Player object with collider.
// Add this component to every Player
// Automatically keeps a history for lag compensation.
// Then create a dummy player with everything removed exept colliders.
// put in normal player position and rotation for tracked,
// then the dummy position and orientation for compensated.
// all you have to do after is call UpdateColliders() whenever a player fires.
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Mirror
{
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public struct Capture3D : Capture
{
public double timestamp { get; set; }
public Vector3 position;
public Vector3 size;
public Quaternion rotation;

public Capture3D(double timestamp, Vector3 position, Vector3 size)
public Capture3D(double timestamp, Vector3 position, Quaternion rotation)
{
this.timestamp = timestamp;
this.position = position;
this.size = size;
this.rotation = rotation;
}

public void DrawGizmo()
{
Gizmos.DrawWireCube(position, size);
}
public void DrawGizmo() { }

public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) =>
new Capture3D(
0, // interpolated snapshot is applied directly. don't need timestamps.
Vector3.LerpUnclamped(from.position, to.position, (float)t),
Vector3.LerpUnclamped(from.size, to.size, (float)t)
Quaternion.LerpUnclamped(from.rotation, to.rotation, (float)t),
);

public override string ToString() => $"(time={timestamp} pos={position} size={size})";
public override string ToString() => $"(time={timestamp} pos={position} size={rotation})";
Amph1dian marked this conversation as resolved.
Show resolved Hide resolved
}


public class LagCompensator : NetworkBehaviour
{
[Header("Components")]
[Tooltip("The collider to keep a history of.")]
public Collider trackedCollider; // assign this in inspector
[Tooltip("The GameObject to enable / disable. NOTE: compensatedPosition & compensatedOrientation can both be compensatedGameobject. those are simply for more control.")]
public Transform compensatedGameObject;
[Tooltip("The Transform to Apply the tracked position")]
public Transform compensatedPosition;
[Tooltip("The Transform to Apply the tracked rotation.")]
public Transform compensatedOrientation;
[Tooltip("The position to keep track of.")]
public Transform trackedPosition;
[Tooltip("The rotation to keep track of.")]
public Transform trackedOrientation;

[Header("Settings")]
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
Expand All @@ -64,14 +79,10 @@ protected virtual void Update()
}
}

protected virtual void Capture()
void Capture()
{
// capture current state
Capture3D capture = new Capture3D(
NetworkTime.localTime,
trackedCollider.bounds.center,
trackedCollider.bounds.size
);
Capture3D capture = new Capture3D(NetworkTime.localTime, trackedPosition.position, trackedOrientation.rotation);

// insert into history
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
Expand All @@ -84,32 +95,6 @@ protected virtual void OnDrawGizmos()
LagCompensation.DrawGizmos(history);
}

// sampling ////////////////////////////////////////////////////////////
// sample the sub-tick (=interpolated) history of this object for a hit test.
// 'viewer' needs to be the player who fired!
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
[Server]
public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample)
{
// never trust the client: estimate client time instead.
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
// the estimation is very good. the error is as low as ~6ms for the demo.
// note that passing 'rtt' is fine: EstimateTime halves it to latency.
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime);

// sample the history to get the nearest snapshots around 'timestamp'
if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t))
{
// interpolate to get a decent estimation at exactly 'timestamp'
sample = Capture3D.Interpolate(resultBefore, resultAfter, t);
return true;
}
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");

sample = default;
return false;
}

// convenience tests ///////////////////////////////////////////////////
// there are multiple different ways to check a hit against the sample:
// - raycasting
Expand All @@ -123,73 +108,41 @@ public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sampl
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
// this is super simple and fast, but not 100% physically accurate since we don't raycast.
[Server]
public virtual bool BoundsCheck(
NetworkConnectionToClient viewer,
Vector3 hitPoint,
float toleranceDistance,
out float distance,
out Vector3 nearest)
public async void UpdateColliders(NetworkConnectionToClient localCon)
{
// first, sample the history at -rtt of the viewer.
if (Sample(viewer, out Capture3D capture))
{
// now that we know where the other player was at that time,
// we can see if the hit point was within tolerance of it.
// TODO consider rotations???
// TODO consider original collider shape??
Bounds bounds = new Bounds(capture.position, capture.size);
nearest = bounds.ClosestPoint(hitPoint);
distance = Vector3.Distance(nearest, hitPoint);
return distance <= toleranceDistance;
}
nearest = hitPoint;
distance = 0;
return false;
}
// never trust the client: estimate client time instead.
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
// the estimation is very good. the error is as low as ~6ms for the demo.
double buffertime = (NetworkManager.singleton.sendRate / 100) * 9;
double rtt = localCon.rtt; // the function needs rtt, which is latency * 2
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, rtt, buffertime);
Dictionary<LagCompensator, Capture3D> resultInterp = new Dictionary<LagCompensator, Capture3D>();

// raycast check: creates a collider the sampled position and raycasts to hitPoint.
// 'viewer' needs to be the player who fired!
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
// this is physically accurate (checks against walls etc.), with the cost
// of a runtime instantiation.
//
// originPoint: where the player fired the weapon.
// hitPoint: where the player's local raycast hit.
// tolerance: scale up the sampled collider by % in order to have a bit of a tolerance.
// 0 means no extra tolerance, 0.05 means 5% extra tolerance.
// layerMask: the layer mask to use for the raycast.
[Server]
public virtual bool RaycastCheck(
NetworkConnectionToClient viewer,
Vector3 originPoint,
Vector3 hitPoint,
float tolerancePercent,
int layerMask,
out RaycastHit hit)
{
// first, sample the history at -rtt of the viewer.
if (Sample(viewer, out Capture3D capture))
var tasks = NetworkServer.connections.Values.Select(async netcon =>
{
// instantiate a real physics collider on demand.
// TODO rotation??
// TODO different collier types??
GameObject temp = new GameObject("LagCompensatorTest");
temp.transform.position = capture.position;
BoxCollider tempCollider = temp.AddComponent<BoxCollider>();
tempCollider.size = capture.size * (1 + tolerancePercent);

// raycast
Vector3 direction = hitPoint - originPoint;
float maxDistance = direction.magnitude * 2;
bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask);

// cleanup
Destroy(temp);
return result;
}

hit = default;
return false;
LagCompensator conPlayer = netcon.identity.GetComponent<LagCompensator>();

// sample the history to get the nearest snapshots around 'timestamp'
if (LagCompensation.Sample(conPlayer.history, estimatedTime, lagCompensationSettings.captureInterval, out resultBefore, out resultAfter, out double t))
{
// interpolate to get a decent estimation at exactly 'timestamp'
resultInterp.Add(conPlayer, Capture3D.Interpolate(resultBefore, resultAfter, t));
resultTime = NetworkTime.localTime;
}
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}, netcon: {netcon}, history: {conPlayer.history.Count}");

if (netcon == localCon)
{
conPlayer.trackedGameObject.SetActive(false);
return;
}
if(!conPlayer.trackedGameObject.activeInHierarchy)
conPlayer.trackedGameObject.SetActive(true);

conPlayer.compensatedPosition.position = resultInterp[conPlayer].position;
conPlayer.compensatedOrientation.rotation = resultInterp[conPlayer].rotation;
});
Task.WhenAll(tasks);
}
}
}