alap
This commit is contained in:
107
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
107
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
|
||||
// This way we can use Physics.Raycast on it.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class HistoryCollider : MonoBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
|
||||
public Collider actualCollider;
|
||||
|
||||
[Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
|
||||
public BoxCollider boundsCollider;
|
||||
|
||||
[Header("History")]
|
||||
[Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
|
||||
public int boundsLimit = 8;
|
||||
|
||||
[Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
|
||||
public int boundsPerBucket = 2;
|
||||
|
||||
[Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
double lastCaptureTime = 0;
|
||||
|
||||
[Header("Debug")]
|
||||
public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
public Color currentColor = Color.red;
|
||||
|
||||
protected HistoryBounds history = null;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
history = new HistoryBounds(boundsLimit, boundsPerBucket);
|
||||
|
||||
// ensure colliders were set.
|
||||
// bounds collider should always be a trigger.
|
||||
if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set.");
|
||||
if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set.");
|
||||
if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
|
||||
if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
|
||||
}
|
||||
|
||||
// capturing and projecting onto colliders should use physics update
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
// capture current bounds every interval
|
||||
if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
CaptureBounds();
|
||||
}
|
||||
|
||||
// project bounds onto helper collider
|
||||
ProjectBounds();
|
||||
}
|
||||
|
||||
protected virtual void CaptureBounds()
|
||||
{
|
||||
// grab current collider bounds
|
||||
// this is in world space coordinates, and axis aligned
|
||||
// TODO double check
|
||||
Bounds bounds = actualCollider.bounds;
|
||||
|
||||
// insert into history
|
||||
history.Insert(bounds);
|
||||
}
|
||||
|
||||
protected virtual void ProjectBounds()
|
||||
{
|
||||
// grab total collider encapsulating all of history
|
||||
Bounds total = history.total;
|
||||
|
||||
// don't assign empty bounds, this will throw a Unity warning
|
||||
if (history.boundsCount == 0) return;
|
||||
|
||||
// scale projection doesn't work yet.
|
||||
// for now, don't allow scale changes.
|
||||
if (transform.lossyScale != Vector3.one)
|
||||
{
|
||||
Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
|
||||
return;
|
||||
}
|
||||
|
||||
// counter rotate the child collider against the gameobject's rotation.
|
||||
// we need this to always be axis aligned.
|
||||
boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);
|
||||
|
||||
// project world space bounds to collider's local space
|
||||
boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
|
||||
boundsCollider.size = total.size; // TODO projection?
|
||||
}
|
||||
|
||||
// TODO runtime drawing for debugging?
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw total bounds
|
||||
Gizmos.color = historyColor;
|
||||
Gizmos.DrawWireCube(history.total.center, history.total.size);
|
||||
|
||||
// draw current bounds
|
||||
Gizmos.color = currentColor;
|
||||
Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5f2158d9776d4b569858f793be4da60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
196
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
196
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
@ -0,0 +1,196 @@
|
||||
// Add this component to a Player object with collider.
|
||||
// Automatically keeps a history for lag compensation.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct Capture3D : Capture
|
||||
{
|
||||
public double timestamp { get; set; }
|
||||
public Vector3 position;
|
||||
public Vector3 size;
|
||||
|
||||
public Capture3D(double timestamp, Vector3 position, Vector3 size)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void DrawGizmo()
|
||||
{
|
||||
Gizmos.DrawWireCube(position, size);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
public override string ToString() => $"(time={timestamp} pos={position} size={size})";
|
||||
}
|
||||
|
||||
[Obsolete("This is a preview version. Community feedback is welcome!")]
|
||||
public class LagCompensator : NetworkBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The collider to keep a history of.")]
|
||||
public Collider trackedCollider; // assign this in inspector
|
||||
|
||||
[Header("Settings")]
|
||||
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
|
||||
double lastCaptureTime;
|
||||
|
||||
// lag compensation history of <timestamp, capture>
|
||||
readonly Queue<KeyValuePair<double, Capture3D>> history = new Queue<KeyValuePair<double, Capture3D>>();
|
||||
|
||||
[Header("Debugging")]
|
||||
public Color historyColor = Color.white;
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
// only capture on server
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
// capture lag compensation snapshots every interval.
|
||||
// NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet.
|
||||
if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
Capture();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Capture()
|
||||
{
|
||||
// capture current state
|
||||
Capture3D capture = new Capture3D(
|
||||
NetworkTime.localTime,
|
||||
trackedCollider.bounds.center,
|
||||
trackedCollider.bounds.size
|
||||
);
|
||||
|
||||
// insert into history
|
||||
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw history
|
||||
Gizmos.color = historyColor;
|
||||
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
|
||||
// - bounds.contains
|
||||
// - increasing bounds by tolerance and checking contains
|
||||
// - threshold to bounds.closestpoint
|
||||
// let's offer a few solutions directly and see which users prefer.
|
||||
|
||||
// bounds check: checks distance to closest point on bounds in history @ -rtt.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// 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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a898831dd60c4cdfbfd9a6ea5702ed01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user