mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
feature: Lag Compensation V1 (#3534)
* Lag Compensation Example based on Snapshot Interpolation example * rename scene * increase latency * update scripts * instructions * better * rename to rollback * rename scene * rebase * rename to LagComp * collider, onclick * CmdClick * flash color * cleanup * LagCompensationSettings * caputre in interval * comment * syntax * catpure, comp * ns * stuff * source * syntax * naming * history and draw history * adjust * cleanups * tests * sample and tests * simplify tests * out interpolation factor t * tostring * show result duration * sample and interpolate * fix * logs * better gizmos * store size * EstimateTime * demo: estimate time * estimation tests * perf: Queue instead of List * comment * syntax * cleaner * cleanup * syntax * comment * DrawGizmo(s) * syntax * extrapolation tests * fix insert and test * extrapolation * fix extrapolation out values * comments * TODO * TODO
This commit is contained in:
parent
43c87b4198
commit
110625b102
3
Assets/Mirror/Core/LagCompensation.meta
Normal file
3
Assets/Mirror/Core/LagCompensation.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d2656015ded44e83a24f4c4776bafd40
|
||||||
|
timeCreated: 1687920405
|
13
Assets/Mirror/Core/LagCompensation/Capture.cs
Normal file
13
Assets/Mirror/Core/LagCompensation/Capture.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
public interface Capture
|
||||||
|
{
|
||||||
|
// server timestamp at time of capture.
|
||||||
|
double timestamp { get; set; }
|
||||||
|
|
||||||
|
// optional gizmo drawing for visual debugging.
|
||||||
|
// history is only known on the server, which usually doesn't render.
|
||||||
|
// showing Gizmos in the Editor is enough.
|
||||||
|
void DrawGizmo();
|
||||||
|
}
|
||||||
|
}
|
3
Assets/Mirror/Core/LagCompensation/Capture.cs.meta
Normal file
3
Assets/Mirror/Core/LagCompensation/Capture.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 347e831952e943a49095cadd39a5aeb2
|
||||||
|
timeCreated: 1687921461
|
144
Assets/Mirror/Core/LagCompensation/LagCompensation.cs
Normal file
144
Assets/Mirror/Core/LagCompensation/LagCompensation.cs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// standalone lag compensation algorithm
|
||||||
|
// based on the Valve Networking Model:
|
||||||
|
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
public static class LagCompensation
|
||||||
|
{
|
||||||
|
// history is of <timestamp, capture>.
|
||||||
|
// Queue allows for fast 'remove first' and 'append last'.
|
||||||
|
//
|
||||||
|
// make sure to always insert in order.
|
||||||
|
// inserting out of order like [1,2,4,3] would cause issues.
|
||||||
|
// can't safeguard this because Queue doesn't have .Last access.
|
||||||
|
public static void Insert<T>(
|
||||||
|
Queue<KeyValuePair<double, T>> history,
|
||||||
|
int historyLimit,
|
||||||
|
double timestamp,
|
||||||
|
T capture)
|
||||||
|
where T : Capture
|
||||||
|
{
|
||||||
|
// make space according to history limit.
|
||||||
|
// do this before inserting, to avoid resizing past capacity.
|
||||||
|
if (history.Count >= historyLimit)
|
||||||
|
history.Dequeue();
|
||||||
|
|
||||||
|
// insert
|
||||||
|
history.Enqueue(new KeyValuePair<double, T>(timestamp, capture));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the two snapshots closest to a given timestamp.
|
||||||
|
// those can be used to interpolate the exact snapshot at that time.
|
||||||
|
// if timestamp is newer than the newest history entry, then we extrapolate.
|
||||||
|
// 't' will be between 1 and 2, before is second last, after is last.
|
||||||
|
// callers should Lerp(before, after, t=1.5) to extrapolate the hit.
|
||||||
|
// see comments below for extrapolation.
|
||||||
|
public static bool Sample<T>(
|
||||||
|
Queue<KeyValuePair<double, T>> history,
|
||||||
|
double timestamp, // current server time
|
||||||
|
double interval, // capture interval
|
||||||
|
out T before,
|
||||||
|
out T after,
|
||||||
|
out double t) // interpolation factor
|
||||||
|
where T : Capture
|
||||||
|
{
|
||||||
|
before = default;
|
||||||
|
after = default;
|
||||||
|
t = 0;
|
||||||
|
|
||||||
|
// can't sample an empty history
|
||||||
|
// interpolation needs at least one entry.
|
||||||
|
// extrapolation needs at least two entries.
|
||||||
|
// can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
|
||||||
|
if(history.Count < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// older than oldest
|
||||||
|
if (timestamp < history.Peek().Key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through the history
|
||||||
|
// TODO faster version: guess start index by how many 'intervals' we are behind.
|
||||||
|
// search around that area.
|
||||||
|
// should be O(1) most of the time, unless sampling was off.
|
||||||
|
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
|
||||||
|
KeyValuePair<double, T> prevPrev = new KeyValuePair<double, T>();
|
||||||
|
foreach(KeyValuePair<double, T> entry in history) {
|
||||||
|
// exact match?
|
||||||
|
if (timestamp == entry.Key) {
|
||||||
|
before = entry.Value;
|
||||||
|
after = entry.Value;
|
||||||
|
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// did we check beyond timestamp? then return the previous two.
|
||||||
|
if (entry.Key > timestamp) {
|
||||||
|
before = prev.Value;
|
||||||
|
after = entry.Value;
|
||||||
|
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remember the last two for extrapolation.
|
||||||
|
// Queue doesn't have access to .Last.
|
||||||
|
prevPrev = prev;
|
||||||
|
prev = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// newer than newest: extrapolate up to one interval.
|
||||||
|
// let's say we capture every 100 ms:
|
||||||
|
// 100, 200, 300, 400
|
||||||
|
// and the server is at 499
|
||||||
|
// if a client sends CmdFire at time 480, then there's no history entry.
|
||||||
|
// => adding the current entry every time would be too expensive.
|
||||||
|
// worst case we would capture at 401, 402, 403, 404, ... 100 times
|
||||||
|
// => not extrapolating isn't great. low latency clients would be
|
||||||
|
// punished by missing their targets since no entry at 'time' was found.
|
||||||
|
// => extrapolation is the best solution. make sure this works as
|
||||||
|
// expected and within limits.
|
||||||
|
if (prev.Key < timestamp && timestamp <= prev.Key + interval) {
|
||||||
|
// return the last two valid snapshots.
|
||||||
|
// can't just return (after, after) because we can't extrapolate
|
||||||
|
// if their distance is 0.
|
||||||
|
before = prevPrev.Value;
|
||||||
|
after = prev.Value;
|
||||||
|
|
||||||
|
// InverseLerp will give [after, after+interval].
|
||||||
|
// but we return [before, after, t].
|
||||||
|
// so add +1 for the distance from before->after
|
||||||
|
t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// never trust the client.
|
||||||
|
// we estimate when a message was sent.
|
||||||
|
// don't trust the client to tell us the time.
|
||||||
|
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||||
|
// Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
|
||||||
|
// => lag compensation demo estimation is off by only ~6ms
|
||||||
|
public static double EstimateTime(double serverTime, double rtt, double bufferTime)
|
||||||
|
{
|
||||||
|
// packet latency is one trip from client to server, so rtt / 2
|
||||||
|
// client view interpolation is the snapshot interpolation buffer time
|
||||||
|
double latency = rtt / 2;
|
||||||
|
return serverTime - latency - bufferTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convenience function to draw all history gizmos.
|
||||||
|
// this should be called from OnDrawGizmos.
|
||||||
|
public static void DrawGizmos<T>(Queue<KeyValuePair<double, T>> history)
|
||||||
|
where T : Capture
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<double, T> entry in history)
|
||||||
|
entry.Value.DrawGizmo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ad53cc7d12144d0ba3a8b0a4515e5d17
|
||||||
|
timeCreated: 1687921483
|
@ -0,0 +1,19 @@
|
|||||||
|
// snapshot interpolation settings struct.
|
||||||
|
// can easily be exposed in Unity inspectors.
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
// class so we can define defaults easily
|
||||||
|
[Serializable]
|
||||||
|
public class LagCompensationSettings
|
||||||
|
{
|
||||||
|
[Header("Buffering")]
|
||||||
|
[Tooltip("Keep this many past snapshots in the buffer. The larger this is, the further we can rewind into the past.\nMaximum rewind time := historyAmount * captureInterval")]
|
||||||
|
public int historyLimit = 6;
|
||||||
|
|
||||||
|
[Tooltip("Capture state every 'captureInterval' seconds. Larger values will space out the captures more, which gives a longer history but with possible gaps inbetween.\nSmaller values will have fewer gaps, with shorter history.")]
|
||||||
|
public float captureInterval = 0.100f; // 100 ms
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fa80bec245f94bf8a28ec78777992a1c
|
||||||
|
timeCreated: 1687920412
|
8
Assets/Mirror/Examples/LagCompensation.meta
Normal file
8
Assets/Mirror/Examples/LagCompensation.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b1d54ff0cbb6043d69ffefca753c48ba
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
32
Assets/Mirror/Examples/LagCompensation/Capture2D.cs
Normal file
32
Assets/Mirror/Examples/LagCompensation/Capture2D.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.LagCompensationDemo
|
||||||
|
{
|
||||||
|
public struct Capture2D : Capture
|
||||||
|
{
|
||||||
|
public double timestamp { get; set; }
|
||||||
|
public Vector2 position;
|
||||||
|
public Vector2 size;
|
||||||
|
|
||||||
|
public Capture2D(double timestamp, Vector2 position, Vector2 size)
|
||||||
|
{
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.position = position;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawGizmo()
|
||||||
|
{
|
||||||
|
Gizmos.DrawWireCube(position, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Capture2D Interpolate(Capture2D from, Capture2D to, double t) =>
|
||||||
|
new Capture2D(
|
||||||
|
0, // interpolated snapshot is applied directly. don't need timestamps.
|
||||||
|
Vector2.LerpUnclamped(from.position, to.position, (float)t),
|
||||||
|
Vector2.LerpUnclamped(from.size, to.size, (float)t)
|
||||||
|
);
|
||||||
|
|
||||||
|
public override string ToString() => $"(time={timestamp} pos={position} size={size})";
|
||||||
|
}
|
||||||
|
}
|
3
Assets/Mirror/Examples/LagCompensation/Capture2D.cs.meta
Normal file
3
Assets/Mirror/Examples/LagCompensation/Capture2D.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a2ccdcd0db384bf08f4150ffb08fd09b
|
||||||
|
timeCreated: 1687921611
|
240
Assets/Mirror/Examples/LagCompensation/ClientCube.cs
Normal file
240
Assets/Mirror/Examples/LagCompensation/ClientCube.cs
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.LagCompensationDemo
|
||||||
|
{
|
||||||
|
public class ClientCube : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Components")]
|
||||||
|
public ServerCube server;
|
||||||
|
public Renderer render;
|
||||||
|
|
||||||
|
[Header("Toggle")]
|
||||||
|
public bool interpolate = true;
|
||||||
|
|
||||||
|
// snapshot interpolation settings
|
||||||
|
[Header("Snapshot Interpolation")]
|
||||||
|
public SnapshotInterpolationSettings snapshotSettings =
|
||||||
|
new SnapshotInterpolationSettings();
|
||||||
|
|
||||||
|
// runtime settings
|
||||||
|
public double bufferTime => server.sendInterval * snapshotSettings.bufferTimeMultiplier;
|
||||||
|
|
||||||
|
// <servertime, snaps>
|
||||||
|
public SortedList<double, Snapshot3D> snapshots = new SortedList<double, Snapshot3D>();
|
||||||
|
|
||||||
|
// for smooth interpolation, we need to interpolate along server time.
|
||||||
|
// any other time (arrival on client, client local time, etc.) is not
|
||||||
|
// going to give smooth results.
|
||||||
|
internal double localTimeline;
|
||||||
|
|
||||||
|
// catchup / slowdown adjustments are applied to timescale,
|
||||||
|
// to be adjusted in every update instead of when receiving messages.
|
||||||
|
double localTimescale = 1;
|
||||||
|
|
||||||
|
// we use EMA to average the last second worth of snapshot time diffs.
|
||||||
|
// manually averaging the last second worth of values with a for loop
|
||||||
|
// would be the same, but a moving average is faster because we only
|
||||||
|
// ever add one value.
|
||||||
|
ExponentialMovingAverage driftEma;
|
||||||
|
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||||
|
|
||||||
|
// debugging ///////////////////////////////////////////////////////////
|
||||||
|
[Header("Debug")]
|
||||||
|
public Color hitColor = Color.blue;
|
||||||
|
public Color missedColor = Color.magenta;
|
||||||
|
public Color originalColor = Color.black;
|
||||||
|
|
||||||
|
[Header("Simulation")]
|
||||||
|
bool lowFpsMode;
|
||||||
|
double accumulatedDeltaTime;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
// defaultColor = render.sharedMaterial.color;
|
||||||
|
|
||||||
|
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||||
|
// 1 second holds 'sendRate' worth of values.
|
||||||
|
// multiplied by emaDuration gives n-seconds.
|
||||||
|
driftEma = new ExponentialMovingAverage(server.sendRate * snapshotSettings.driftEmaDuration);
|
||||||
|
deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * snapshotSettings.deliveryTimeEmaDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add snapshot & initialize client interpolation time if needed
|
||||||
|
public void OnMessage(Snapshot3D snap)
|
||||||
|
{
|
||||||
|
// set local timestamp (= when it was received on our end)
|
||||||
|
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||||
|
snap.localTime = NetworkTime.localTime;
|
||||||
|
|
||||||
|
// (optional) dynamic adjustment
|
||||||
|
if (snapshotSettings.dynamicAdjustment)
|
||||||
|
{
|
||||||
|
// set bufferTime on the fly.
|
||||||
|
// shows in inspector for easier debugging :)
|
||||||
|
snapshotSettings.bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||||
|
server.sendInterval,
|
||||||
|
deliveryTimeEma.StandardDeviation,
|
||||||
|
snapshotSettings.dynamicAdjustmentTolerance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert into the buffer & initialize / adjust / catchup
|
||||||
|
SnapshotInterpolation.InsertAndAdjust(
|
||||||
|
snapshots,
|
||||||
|
snapshotSettings.bufferLimit,
|
||||||
|
snap,
|
||||||
|
ref localTimeline,
|
||||||
|
ref localTimescale,
|
||||||
|
server.sendInterval,
|
||||||
|
bufferTime,
|
||||||
|
snapshotSettings.catchupSpeed,
|
||||||
|
snapshotSettings.slowdownSpeed,
|
||||||
|
ref driftEma,
|
||||||
|
snapshotSettings.catchupNegativeThreshold,
|
||||||
|
snapshotSettings.catchupPositiveThreshold,
|
||||||
|
ref deliveryTimeEma);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// accumulated delta allows us to simulate correct low fps + deltaTime
|
||||||
|
// if necessary in client low fps mode.
|
||||||
|
accumulatedDeltaTime += Time.unscaledDeltaTime;
|
||||||
|
|
||||||
|
// simulate low fps mode. only update once per second.
|
||||||
|
// to simulate webgl background tabs, etc.
|
||||||
|
// after a while, disable low fps mode and see how it behaves.
|
||||||
|
if (lowFpsMode && accumulatedDeltaTime < 1) return;
|
||||||
|
|
||||||
|
// only while we have snapshots.
|
||||||
|
// timeline starts when the first snapshot arrives.
|
||||||
|
if (snapshots.Count > 0)
|
||||||
|
{
|
||||||
|
// snapshot interpolation
|
||||||
|
if (interpolate)
|
||||||
|
{
|
||||||
|
// step
|
||||||
|
SnapshotInterpolation.Step(
|
||||||
|
snapshots,
|
||||||
|
// accumulate delta is Time.unscaledDeltaTime normally.
|
||||||
|
// and sum of past 10 delta's in low fps mode.
|
||||||
|
accumulatedDeltaTime,
|
||||||
|
ref localTimeline,
|
||||||
|
localTimescale,
|
||||||
|
out Snapshot3D fromSnapshot,
|
||||||
|
out Snapshot3D toSnapshot,
|
||||||
|
out double t);
|
||||||
|
|
||||||
|
// interpolate & apply
|
||||||
|
Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t);
|
||||||
|
transform.position = computed.position;
|
||||||
|
}
|
||||||
|
// apply raw
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snapshot3D snap = snapshots.Values[0];
|
||||||
|
transform.position = snap.position;
|
||||||
|
snapshots.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset simulation helpers
|
||||||
|
accumulatedDeltaTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnMouseDown()
|
||||||
|
{
|
||||||
|
// send the command.
|
||||||
|
// only x coordinate matters for this simple example.
|
||||||
|
if (server.CmdClicked(transform.position))
|
||||||
|
{
|
||||||
|
Debug.Log($"Click hit!");
|
||||||
|
FlashColor(hitColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log($"Click missed!");
|
||||||
|
FlashColor(missedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple visual indicator for better feedback.
|
||||||
|
// changes the cube's color for a short time.
|
||||||
|
void FlashColor(Color color) =>
|
||||||
|
StartCoroutine(TemporarilyChangeColorToGreen(color));
|
||||||
|
|
||||||
|
IEnumerator TemporarilyChangeColorToGreen(Color color)
|
||||||
|
{
|
||||||
|
Renderer r = GetComponentInChildren<Renderer>();
|
||||||
|
r.material.color = color;
|
||||||
|
yield return new WaitForSeconds(0.2f);
|
||||||
|
r.material.color = originalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
// display buffer size as number for easier debugging.
|
||||||
|
// catchup is displayed as color state in Update() already.
|
||||||
|
const int width = 30; // fit 3 digits
|
||||||
|
const int height = 20;
|
||||||
|
Vector2 screen = Camera.main.WorldToScreenPoint(transform.position);
|
||||||
|
string str = $"{snapshots.Count}";
|
||||||
|
GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str);
|
||||||
|
|
||||||
|
// client simulation buttons on the bottom of the screen
|
||||||
|
float areaHeight = 150;
|
||||||
|
GUILayout.BeginArea(new Rect(0, Screen.height - areaHeight, Screen.width, areaHeight));
|
||||||
|
GUILayout.Label("Click the black cube. Lag compensation will correct the latency.");
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
GUILayout.Label("Client Simulation:");
|
||||||
|
if (GUILayout.Button((lowFpsMode ? "Disable" : "Enable") + " 1 FPS"))
|
||||||
|
{
|
||||||
|
lowFpsMode = !lowFpsMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.Label("|");
|
||||||
|
|
||||||
|
if (GUILayout.Button("Timeline 10s behind"))
|
||||||
|
{
|
||||||
|
localTimeline -= 10.0;
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("Timeline 1s behind"))
|
||||||
|
{
|
||||||
|
localTimeline -= 1.0;
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("Timeline 0.1s behind"))
|
||||||
|
{
|
||||||
|
localTimeline -= 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.Label("|");
|
||||||
|
|
||||||
|
if (GUILayout.Button("Timeline 0.1s ahead"))
|
||||||
|
{
|
||||||
|
localTimeline += 0.1;
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("Timeline 1s ahead"))
|
||||||
|
{
|
||||||
|
localTimeline += 1.0;
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("Timeline 10s ahead"))
|
||||||
|
{
|
||||||
|
localTimeline += 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
GUILayout.EndArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnValidate()
|
||||||
|
{
|
||||||
|
// thresholds need to be <0 and >0
|
||||||
|
snapshotSettings.catchupNegativeThreshold = Math.Min(snapshotSettings.catchupNegativeThreshold, 0);
|
||||||
|
snapshotSettings.catchupPositiveThreshold = Math.Max(snapshotSettings.catchupPositiveThreshold, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8caf33fba9e694fef966e0b2f88f0afc
|
||||||
|
timeCreated: 1654065994
|
80
Assets/Mirror/Examples/LagCompensation/ClientMaterial.mat
Normal file
80
Assets/Mirror/Examples/LagCompensation/ClientMaterial.mat
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!21 &2100000
|
||||||
|
Material:
|
||||||
|
serializedVersion: 8
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: ClientMaterial
|
||||||
|
m_Shader: {fileID: 10755, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_ValidKeywords: []
|
||||||
|
m_InvalidKeywords: []
|
||||||
|
m_LightmapFlags: 4
|
||||||
|
m_EnableInstancingVariants: 0
|
||||||
|
m_DoubleSidedGI: 0
|
||||||
|
m_CustomRenderQueue: -1
|
||||||
|
stringTagMap: {}
|
||||||
|
disabledShaderPasses: []
|
||||||
|
m_SavedProperties:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TexEnvs:
|
||||||
|
- _BumpMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailAlbedoMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailMask:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailNormalMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _EmissionMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MainTex:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MetallicGlossMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _OcclusionMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _ParallaxMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
m_Ints: []
|
||||||
|
m_Floats:
|
||||||
|
- _BumpScale: 1
|
||||||
|
- _Cutoff: 0.5
|
||||||
|
- _DetailNormalMapScale: 1
|
||||||
|
- _DstBlend: 0
|
||||||
|
- _GlossMapScale: 1
|
||||||
|
- _Glossiness: 0.5
|
||||||
|
- _GlossyReflections: 1
|
||||||
|
- _Metallic: 0
|
||||||
|
- _Mode: 0
|
||||||
|
- _OcclusionStrength: 1
|
||||||
|
- _Parallax: 0.02
|
||||||
|
- _SmoothnessTextureChannel: 0
|
||||||
|
- _SpecularHighlights: 1
|
||||||
|
- _SrcBlend: 1
|
||||||
|
- _UVSec: 0
|
||||||
|
- _ZWrite: 1
|
||||||
|
m_Colors:
|
||||||
|
- _Color: {r: 0, g: 0, b: 0, a: 1}
|
||||||
|
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
|
m_BuildTextureStacks: []
|
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e402de56ca981421cbbd922919787c15
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
483
Assets/Mirror/Examples/LagCompensation/LagCompensation.unity
Normal file
483
Assets/Mirror/Examples/LagCompensation/LagCompensation.unity
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!29 &1
|
||||||
|
OcclusionCullingSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 2
|
||||||
|
m_OcclusionBakeSettings:
|
||||||
|
smallestOccluder: 5
|
||||||
|
smallestHole: 0.25
|
||||||
|
backfaceThreshold: 100
|
||||||
|
m_SceneGUID: 00000000000000000000000000000000
|
||||||
|
m_OcclusionCullingData: {fileID: 0}
|
||||||
|
--- !u!104 &2
|
||||||
|
RenderSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 9
|
||||||
|
m_Fog: 0
|
||||||
|
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||||
|
m_FogMode: 3
|
||||||
|
m_FogDensity: 0.01
|
||||||
|
m_LinearFogStart: 0
|
||||||
|
m_LinearFogEnd: 300
|
||||||
|
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||||
|
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||||
|
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||||
|
m_AmbientIntensity: 1
|
||||||
|
m_AmbientMode: 0
|
||||||
|
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||||
|
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_HaloStrength: 0.5
|
||||||
|
m_FlareStrength: 1
|
||||||
|
m_FlareFadeSpeed: 3
|
||||||
|
m_HaloTexture: {fileID: 0}
|
||||||
|
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_DefaultReflectionMode: 0
|
||||||
|
m_DefaultReflectionResolution: 128
|
||||||
|
m_ReflectionBounces: 1
|
||||||
|
m_ReflectionIntensity: 1
|
||||||
|
m_CustomReflection: {fileID: 0}
|
||||||
|
m_Sun: {fileID: 0}
|
||||||
|
m_IndirectSpecularColor: {r: 0.37311924, g: 0.38073963, b: 0.3587269, a: 1}
|
||||||
|
m_UseRadianceAmbientProbe: 0
|
||||||
|
--- !u!157 &3
|
||||||
|
LightmapSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 12
|
||||||
|
m_GIWorkflowMode: 1
|
||||||
|
m_GISettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_BounceScale: 1
|
||||||
|
m_IndirectOutputScale: 1
|
||||||
|
m_AlbedoBoost: 1
|
||||||
|
m_EnvironmentLightingMode: 0
|
||||||
|
m_EnableBakedLightmaps: 1
|
||||||
|
m_EnableRealtimeLightmaps: 0
|
||||||
|
m_LightmapEditorSettings:
|
||||||
|
serializedVersion: 12
|
||||||
|
m_Resolution: 2
|
||||||
|
m_BakeResolution: 40
|
||||||
|
m_AtlasSize: 1024
|
||||||
|
m_AO: 0
|
||||||
|
m_AOMaxDistance: 1
|
||||||
|
m_CompAOExponent: 1
|
||||||
|
m_CompAOExponentDirect: 0
|
||||||
|
m_ExtractAmbientOcclusion: 0
|
||||||
|
m_Padding: 2
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_LightmapsBakeMode: 1
|
||||||
|
m_TextureCompression: 1
|
||||||
|
m_FinalGather: 0
|
||||||
|
m_FinalGatherFiltering: 1
|
||||||
|
m_FinalGatherRayCount: 256
|
||||||
|
m_ReflectionCompression: 2
|
||||||
|
m_MixedBakeMode: 2
|
||||||
|
m_BakeBackend: 1
|
||||||
|
m_PVRSampling: 1
|
||||||
|
m_PVRDirectSampleCount: 32
|
||||||
|
m_PVRSampleCount: 512
|
||||||
|
m_PVRBounces: 2
|
||||||
|
m_PVREnvironmentSampleCount: 256
|
||||||
|
m_PVREnvironmentReferencePointCount: 2048
|
||||||
|
m_PVRFilteringMode: 1
|
||||||
|
m_PVRDenoiserTypeDirect: 1
|
||||||
|
m_PVRDenoiserTypeIndirect: 1
|
||||||
|
m_PVRDenoiserTypeAO: 1
|
||||||
|
m_PVRFilterTypeDirect: 0
|
||||||
|
m_PVRFilterTypeIndirect: 0
|
||||||
|
m_PVRFilterTypeAO: 0
|
||||||
|
m_PVREnvironmentMIS: 1
|
||||||
|
m_PVRCulling: 1
|
||||||
|
m_PVRFilteringGaussRadiusDirect: 1
|
||||||
|
m_PVRFilteringGaussRadiusIndirect: 5
|
||||||
|
m_PVRFilteringGaussRadiusAO: 2
|
||||||
|
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||||
|
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||||
|
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||||
|
m_ExportTrainingData: 0
|
||||||
|
m_TrainingDataDestination: TrainingData
|
||||||
|
m_LightProbeSampleCountMultiplier: 4
|
||||||
|
m_LightingDataAsset: {fileID: 0}
|
||||||
|
m_LightingSettings: {fileID: 0}
|
||||||
|
--- !u!196 &4
|
||||||
|
NavMeshSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_BuildSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
agentTypeID: 0
|
||||||
|
agentRadius: 0.5
|
||||||
|
agentHeight: 2
|
||||||
|
agentSlope: 45
|
||||||
|
agentClimb: 0.4
|
||||||
|
ledgeDropHeight: 0
|
||||||
|
maxJumpAcrossDistance: 0
|
||||||
|
minRegionArea: 2
|
||||||
|
manualCellSize: 0
|
||||||
|
cellSize: 0.16666667
|
||||||
|
manualTileSize: 0
|
||||||
|
tileSize: 256
|
||||||
|
accuratePlacement: 0
|
||||||
|
maxJobWorkers: 0
|
||||||
|
preserveTilesOutsideBounds: 0
|
||||||
|
debug:
|
||||||
|
m_Flags: 0
|
||||||
|
m_NavMeshData: {fileID: 0}
|
||||||
|
--- !u!1 &89338751
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 89338755}
|
||||||
|
- component: {fileID: 89338756}
|
||||||
|
- component: {fileID: 89338757}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Client Cube
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &89338755
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 89338751}
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: -5, y: 0.5, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 1292704308}
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_RootOrder: 2
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &89338756
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 89338751}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 8caf33fba9e694fef966e0b2f88f0afc, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
server: {fileID: 474480122}
|
||||||
|
render: {fileID: 1292704310}
|
||||||
|
interpolate: 1
|
||||||
|
snapshotSettings:
|
||||||
|
bufferTimeMultiplier: 8
|
||||||
|
bufferLimit: 32
|
||||||
|
catchupNegativeThreshold: -1
|
||||||
|
catchupPositiveThreshold: 1
|
||||||
|
catchupSpeed: 0.019999999552965164
|
||||||
|
slowdownSpeed: 0.03999999910593033
|
||||||
|
driftEmaDuration: 1
|
||||||
|
dynamicAdjustment: 0
|
||||||
|
dynamicAdjustmentTolerance: 1
|
||||||
|
deliveryTimeEmaDuration: 2
|
||||||
|
hitColor: {r: 0, g: 0, b: 1, a: 1}
|
||||||
|
missedColor: {r: 1, g: 0, b: 1, a: 1}
|
||||||
|
originalColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
|
--- !u!65 &89338757
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 89338751}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IsTrigger: 1
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
|
m_Center: {x: 0, y: -1, z: 0}
|
||||||
|
--- !u!1 &474480117
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 474480121}
|
||||||
|
- component: {fileID: 474480120}
|
||||||
|
- component: {fileID: 474480119}
|
||||||
|
- component: {fileID: 474480122}
|
||||||
|
- component: {fileID: 474480123}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Server Cube
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!23 &474480119
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 474480117}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_CastShadows: 1
|
||||||
|
m_ReceiveShadows: 1
|
||||||
|
m_DynamicOccludee: 1
|
||||||
|
m_StaticShadowCaster: 0
|
||||||
|
m_MotionVectors: 1
|
||||||
|
m_LightProbeUsage: 1
|
||||||
|
m_ReflectionProbeUsage: 1
|
||||||
|
m_RayTracingMode: 2
|
||||||
|
m_RayTraceProcedural: 0
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_RendererPriority: 0
|
||||||
|
m_Materials:
|
||||||
|
- {fileID: 2100000, guid: 163b909ba60cc435a95bb35396edda15, type: 2}
|
||||||
|
m_StaticBatchInfo:
|
||||||
|
firstSubMesh: 0
|
||||||
|
subMeshCount: 0
|
||||||
|
m_StaticBatchRoot: {fileID: 0}
|
||||||
|
m_ProbeAnchor: {fileID: 0}
|
||||||
|
m_LightProbeVolumeOverride: {fileID: 0}
|
||||||
|
m_ScaleInLightmap: 1
|
||||||
|
m_ReceiveGI: 1
|
||||||
|
m_PreserveUVs: 0
|
||||||
|
m_IgnoreNormalsForChartDetection: 0
|
||||||
|
m_ImportantGI: 0
|
||||||
|
m_StitchLightmapSeams: 1
|
||||||
|
m_SelectedEditorRenderState: 3
|
||||||
|
m_MinimumChartSize: 4
|
||||||
|
m_AutoUVMaxDistance: 0.5
|
||||||
|
m_AutoUVMaxAngle: 89
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_SortingLayerID: 0
|
||||||
|
m_SortingLayer: 0
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!33 &474480120
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 474480117}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &474480121
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 474480117}
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: -5, y: 0.5, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_RootOrder: 1
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &474480122
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 474480117}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: eb17f7222317f4b889bb4e80f69df3f9, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
client: {fileID: 89338756}
|
||||||
|
collider: {fileID: 474480123}
|
||||||
|
distance: 10
|
||||||
|
speed: 3
|
||||||
|
sendRate: 30
|
||||||
|
lagCompensationSettings:
|
||||||
|
historyLimit: 4
|
||||||
|
captureInterval: 0.4
|
||||||
|
historyColor: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
resultDuration: 0.5
|
||||||
|
latency: 0.05
|
||||||
|
jitter: 0.05
|
||||||
|
loss: 0.05
|
||||||
|
scramble: 0.05
|
||||||
|
--- !u!65 &474480123
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 474480117}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IsTrigger: 1
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1292704307
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1292704308}
|
||||||
|
- component: {fileID: 1292704311}
|
||||||
|
- component: {fileID: 1292704310}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Visual Offset
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1292704308
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1292704307}
|
||||||
|
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: -1, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 89338755}
|
||||||
|
m_RootOrder: 0
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!23 &1292704310
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1292704307}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_CastShadows: 1
|
||||||
|
m_ReceiveShadows: 1
|
||||||
|
m_DynamicOccludee: 1
|
||||||
|
m_StaticShadowCaster: 0
|
||||||
|
m_MotionVectors: 1
|
||||||
|
m_LightProbeUsage: 1
|
||||||
|
m_ReflectionProbeUsage: 1
|
||||||
|
m_RayTracingMode: 2
|
||||||
|
m_RayTraceProcedural: 0
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_RendererPriority: 0
|
||||||
|
m_Materials:
|
||||||
|
- {fileID: 2100000, guid: f17cbcb3229954975ab0818845a2c17f, type: 2}
|
||||||
|
m_StaticBatchInfo:
|
||||||
|
firstSubMesh: 0
|
||||||
|
subMeshCount: 0
|
||||||
|
m_StaticBatchRoot: {fileID: 0}
|
||||||
|
m_ProbeAnchor: {fileID: 0}
|
||||||
|
m_LightProbeVolumeOverride: {fileID: 0}
|
||||||
|
m_ScaleInLightmap: 1
|
||||||
|
m_ReceiveGI: 1
|
||||||
|
m_PreserveUVs: 0
|
||||||
|
m_IgnoreNormalsForChartDetection: 0
|
||||||
|
m_ImportantGI: 0
|
||||||
|
m_StitchLightmapSeams: 1
|
||||||
|
m_SelectedEditorRenderState: 3
|
||||||
|
m_MinimumChartSize: 4
|
||||||
|
m_AutoUVMaxDistance: 0.5
|
||||||
|
m_AutoUVMaxAngle: 89
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_SortingLayerID: 0
|
||||||
|
m_SortingLayer: 0
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!33 &1292704311
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1292704307}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!1 &1961486736
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1961486739}
|
||||||
|
- component: {fileID: 1961486738}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Main Camera
|
||||||
|
m_TagString: MainCamera
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!20 &1961486738
|
||||||
|
Camera:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1961486736}
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ClearFlags: 2
|
||||||
|
m_BackGroundColor: {r: 0.26415092, g: 0.26415092, b: 0.26415092, a: 0}
|
||||||
|
m_projectionMatrixMode: 1
|
||||||
|
m_GateFitMode: 2
|
||||||
|
m_FOVAxisMode: 0
|
||||||
|
m_SensorSize: {x: 36, y: 24}
|
||||||
|
m_LensShift: {x: 0, y: 0}
|
||||||
|
m_FocalLength: 50
|
||||||
|
m_NormalizedViewPortRect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 1
|
||||||
|
height: 1
|
||||||
|
near clip plane: 0.3
|
||||||
|
far clip plane: 1000
|
||||||
|
field of view: 60
|
||||||
|
orthographic: 1
|
||||||
|
orthographic size: 7
|
||||||
|
m_Depth: 0
|
||||||
|
m_CullingMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
m_RenderingPath: -1
|
||||||
|
m_TargetTexture: {fileID: 0}
|
||||||
|
m_TargetDisplay: 0
|
||||||
|
m_TargetEye: 3
|
||||||
|
m_HDR: 1
|
||||||
|
m_AllowMSAA: 1
|
||||||
|
m_AllowDynamicResolution: 0
|
||||||
|
m_ForceIntoRT: 0
|
||||||
|
m_OcclusionCulling: 1
|
||||||
|
m_StereoConvergence: 10
|
||||||
|
m_StereoSeparation: 0.022
|
||||||
|
--- !u!4 &1961486739
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1961486736}
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: -11.22}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_RootOrder: 0
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 171fa8a3c4ead4a2886e0ecd02e810c0
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
210
Assets/Mirror/Examples/LagCompensation/ServerCube.cs
Normal file
210
Assets/Mirror/Examples/LagCompensation/ServerCube.cs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.LagCompensationDemo
|
||||||
|
{
|
||||||
|
public class ServerCube : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Components")]
|
||||||
|
public ClientCube client;
|
||||||
|
public new BoxCollider collider;
|
||||||
|
|
||||||
|
[Header("Movement")]
|
||||||
|
public float distance = 10;
|
||||||
|
public float speed = 3;
|
||||||
|
Vector3 start;
|
||||||
|
|
||||||
|
[Header("Snapshot Interpolation")]
|
||||||
|
[Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")]
|
||||||
|
public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333'
|
||||||
|
public float sendInterval => 1f / sendRate;
|
||||||
|
float lastSendTime;
|
||||||
|
|
||||||
|
[Header("Lag Compensation")]
|
||||||
|
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
|
||||||
|
double lastCaptureTime;
|
||||||
|
|
||||||
|
// lag compensation history of <timestamp, capture>
|
||||||
|
Queue<KeyValuePair<double, Capture2D>> history = new Queue<KeyValuePair<double, Capture2D>>();
|
||||||
|
|
||||||
|
public Color historyColor = Color.white;
|
||||||
|
|
||||||
|
// store latest lag compensation result to show a visual indicator
|
||||||
|
[Header("Debug")]
|
||||||
|
public double resultDuration = 0.5;
|
||||||
|
double resultTime;
|
||||||
|
Capture2D resultBefore;
|
||||||
|
Capture2D resultAfter;
|
||||||
|
Capture2D resultInterpolated;
|
||||||
|
|
||||||
|
[Header("Latency Simulation")]
|
||||||
|
[Tooltip("Latency in seconds")]
|
||||||
|
public float latency = 0.05f; // 50 ms
|
||||||
|
[Tooltip("Latency jitter, randomly added to latency.")]
|
||||||
|
[Range(0, 1)] public float jitter = 0.05f;
|
||||||
|
[Tooltip("Packet loss in %")]
|
||||||
|
[Range(0, 1)] public float loss = 0.1f;
|
||||||
|
[Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")]
|
||||||
|
[Range(0, 1)] public float scramble = 0.1f;
|
||||||
|
|
||||||
|
// random
|
||||||
|
// UnityEngine.Random.value is [0, 1] with both upper and lower bounds inclusive
|
||||||
|
// but we need the upper bound to be exclusive, so using System.Random instead.
|
||||||
|
// => NextDouble() is NEVER < 0 so loss=0 never drops!
|
||||||
|
// => NextDouble() is ALWAYS < 1 so loss=1 always drops!
|
||||||
|
System.Random random = new System.Random();
|
||||||
|
|
||||||
|
// hold on to snapshots for a little while before delivering
|
||||||
|
// <deliveryTime, snapshot>
|
||||||
|
List<(double, Snapshot3D)> queue = new List<(double, Snapshot3D)>();
|
||||||
|
|
||||||
|
// latency simulation:
|
||||||
|
// always a fixed value + some jitter.
|
||||||
|
float SimulateLatency() => latency + Random.value * jitter;
|
||||||
|
|
||||||
|
// this is the average without randomness. for lag compensation math.
|
||||||
|
// in a real game, use rtt instead.
|
||||||
|
float AverageLatency() => latency + 0.5f * jitter;
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
start = transform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// move on XY plane
|
||||||
|
float x = Mathf.PingPong(Time.time * speed, distance);
|
||||||
|
transform.position = new Vector3(start.x + x, start.y, start.z);
|
||||||
|
|
||||||
|
// broadcast snapshots every interval
|
||||||
|
if (Time.time >= lastSendTime + sendInterval)
|
||||||
|
{
|
||||||
|
Send(transform.position);
|
||||||
|
lastSendTime = Time.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
Flush();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Send(Vector3 position)
|
||||||
|
{
|
||||||
|
// create snapshot
|
||||||
|
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||||
|
Snapshot3D snap = new Snapshot3D(NetworkTime.localTime, 0, position);
|
||||||
|
|
||||||
|
// simulate packet loss
|
||||||
|
bool drop = random.NextDouble() < loss;
|
||||||
|
if (!drop)
|
||||||
|
{
|
||||||
|
// simulate scramble (Random.Next is < max, so +1)
|
||||||
|
bool doScramble = random.NextDouble() < scramble;
|
||||||
|
int last = queue.Count;
|
||||||
|
int index = doScramble ? random.Next(0, last + 1) : last;
|
||||||
|
|
||||||
|
// simulate latency
|
||||||
|
float simulatedLatency = SimulateLatency();
|
||||||
|
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||||
|
double deliveryTime = NetworkTime.localTime + simulatedLatency;
|
||||||
|
queue.Insert(index, (deliveryTime, snap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Flush()
|
||||||
|
{
|
||||||
|
// flush ready snapshots to client
|
||||||
|
for (int i = 0; i < queue.Count; ++i)
|
||||||
|
{
|
||||||
|
(double deliveryTime, Snapshot3D snap) = queue[i];
|
||||||
|
|
||||||
|
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||||
|
if (NetworkTime.localTime >= deliveryTime)
|
||||||
|
{
|
||||||
|
client.OnMessage(snap);
|
||||||
|
queue.RemoveAt(i);
|
||||||
|
--i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Capture()
|
||||||
|
{
|
||||||
|
// capture current state
|
||||||
|
Capture2D capture = new Capture2D(NetworkTime.localTime, transform.position, collider.size);
|
||||||
|
|
||||||
|
// insert into history
|
||||||
|
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// client says: "I was clicked here, at this time."
|
||||||
|
// server needs to rollback to validate.
|
||||||
|
// timestamp is the client's snapshot interpolated timeline!
|
||||||
|
public bool CmdClicked(Vector2 position)
|
||||||
|
{
|
||||||
|
// 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 rtt = AverageLatency() * 2; // the function needs rtt, which is latency * 2
|
||||||
|
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, rtt, client.bufferTime);
|
||||||
|
|
||||||
|
// compare estimated time with actual client time for debugging
|
||||||
|
double error = Math.Abs(estimatedTime - client.localTimeline);
|
||||||
|
Debug.Log($"CmdClicked: serverTime={NetworkTime.localTime:F3} clientTime={client.localTimeline:F3} estimatedTime={estimatedTime:F3} estimationError={error:F3} position={position}");
|
||||||
|
|
||||||
|
// sample the history to get the nearest snapshots around 'timestamp'
|
||||||
|
if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out resultBefore, out resultAfter, out double t))
|
||||||
|
{
|
||||||
|
// interpolate to get a decent estimation at exactly 'timestamp'
|
||||||
|
resultInterpolated = Capture2D.Interpolate(resultBefore, resultAfter, t);
|
||||||
|
resultTime = NetworkTime.localTime;
|
||||||
|
|
||||||
|
// check if there really was a cube at that time and position
|
||||||
|
Bounds bounds = new Bounds(resultInterpolated.position, resultInterpolated.size);
|
||||||
|
if (bounds.Contains(position))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.Log($"CmdClicked: interpolated={resultInterpolated} doesn't contain {position}");
|
||||||
|
}
|
||||||
|
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDrawGizmos()
|
||||||
|
{
|
||||||
|
// should we apply special colors to an active result?
|
||||||
|
bool showResult = NetworkTime.localTime <= resultTime + resultDuration;
|
||||||
|
|
||||||
|
// draw interpoalted result first.
|
||||||
|
// history meshcubes should write over it for better visibility.
|
||||||
|
if (showResult)
|
||||||
|
{
|
||||||
|
Gizmos.color = Color.black;
|
||||||
|
Gizmos.DrawCube(resultInterpolated.position, resultInterpolated.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw history
|
||||||
|
Gizmos.color = historyColor;
|
||||||
|
LagCompensation.DrawGizmos(history);
|
||||||
|
|
||||||
|
// draw result samples after. useful to see the selection process.
|
||||||
|
if (showResult)
|
||||||
|
{
|
||||||
|
Gizmos.color = Color.cyan;
|
||||||
|
Gizmos.DrawWireCube(resultBefore.position, resultBefore.size);
|
||||||
|
Gizmos.DrawWireCube(resultAfter.position, resultAfter.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Examples/LagCompensation/ServerCube.cs.meta
Normal file
11
Assets/Mirror/Examples/LagCompensation/ServerCube.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eb17f7222317f4b889bb4e80f69df3f9
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
80
Assets/Mirror/Examples/LagCompensation/ServerMaterial.mat
Normal file
80
Assets/Mirror/Examples/LagCompensation/ServerMaterial.mat
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!21 &2100000
|
||||||
|
Material:
|
||||||
|
serializedVersion: 8
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: ServerMaterial
|
||||||
|
m_Shader: {fileID: 10755, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_ValidKeywords: []
|
||||||
|
m_InvalidKeywords: []
|
||||||
|
m_LightmapFlags: 4
|
||||||
|
m_EnableInstancingVariants: 0
|
||||||
|
m_DoubleSidedGI: 0
|
||||||
|
m_CustomRenderQueue: -1
|
||||||
|
stringTagMap: {}
|
||||||
|
disabledShaderPasses: []
|
||||||
|
m_SavedProperties:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TexEnvs:
|
||||||
|
- _BumpMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailAlbedoMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailMask:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _DetailNormalMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _EmissionMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MainTex:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MetallicGlossMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _OcclusionMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _ParallaxMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
m_Ints: []
|
||||||
|
m_Floats:
|
||||||
|
- _BumpScale: 1
|
||||||
|
- _Cutoff: 0.5
|
||||||
|
- _DetailNormalMapScale: 1
|
||||||
|
- _DstBlend: 0
|
||||||
|
- _GlossMapScale: 1
|
||||||
|
- _Glossiness: 0.5
|
||||||
|
- _GlossyReflections: 1
|
||||||
|
- _Metallic: 0
|
||||||
|
- _Mode: 0
|
||||||
|
- _OcclusionStrength: 1
|
||||||
|
- _Parallax: 0.02
|
||||||
|
- _SmoothnessTextureChannel: 0
|
||||||
|
- _SpecularHighlights: 1
|
||||||
|
- _SrcBlend: 1
|
||||||
|
- _UVSec: 0
|
||||||
|
- _ZWrite: 1
|
||||||
|
m_Colors:
|
||||||
|
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
|
m_BuildTextureStacks: []
|
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f83d36483f2c647d9a8385d3fbb9135b
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
26
Assets/Mirror/Examples/LagCompensation/Snapshot3D.cs
Normal file
26
Assets/Mirror/Examples/LagCompensation/Snapshot3D.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// a simple snapshot with timestamp & interpolation
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.LagCompensationDemo
|
||||||
|
{
|
||||||
|
public struct Snapshot3D : Snapshot
|
||||||
|
{
|
||||||
|
public double remoteTime { get; set; }
|
||||||
|
public double localTime { get; set; }
|
||||||
|
public Vector3 position;
|
||||||
|
|
||||||
|
public Snapshot3D(double remoteTime, double localTime, Vector3 position)
|
||||||
|
{
|
||||||
|
this.remoteTime = remoteTime;
|
||||||
|
this.localTime = localTime;
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snapshot3D Interpolate(Snapshot3D from, Snapshot3D to, double t) =>
|
||||||
|
new Snapshot3D(
|
||||||
|
// interpolated snapshot is applied directly. don't need timestamps.
|
||||||
|
0, 0,
|
||||||
|
// lerp unclamped in case we ever need to extrapolate.
|
||||||
|
Vector3.LerpUnclamped(from.position, to.position, (float)t));
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Examples/LagCompensation/Snapshot3D.cs.meta
Normal file
11
Assets/Mirror/Examples/LagCompensation/Snapshot3D.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 78563da1b871d45b3a9f763a2a469b76
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
2
Assets/Mirror/Examples/LagCompensation/_DISABLE VSYNC_
Normal file
2
Assets/Mirror/Examples/LagCompensation/_DISABLE VSYNC_
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
otherwise it may not look entirely smooth.
|
||||||
|
even on 120 hz.
|
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8a02664e59ea04082ba401785fdf2a3f
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
6
Assets/Mirror/Examples/LagCompensation/_README.txt
Normal file
6
Assets/Mirror/Examples/LagCompensation/_README.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Rollback / Lag Compensation is a standalone, Unity / netcode independent algorithm.
|
||||||
|
This is a simple demo to test it, without Mirror.
|
||||||
|
We want this to be usable in all game engines.
|
||||||
|
|
||||||
|
The demo intentionally introduces latency so that server / client cubes are
|
||||||
|
at different positions when clicking.
|
7
Assets/Mirror/Examples/LagCompensation/_README.txt.meta
Normal file
7
Assets/Mirror/Examples/LagCompensation/_README.txt.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b607c436b68734cb99de2663169c5b44
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
3
Assets/Mirror/Tests/Editor/LagCompensation.meta
Normal file
3
Assets/Mirror/Tests/Editor/LagCompensation.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: db5b356723f24f938279b24215e26ec8
|
||||||
|
timeCreated: 1687927277
|
@ -0,0 +1,223 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirror.Tests
|
||||||
|
{
|
||||||
|
// a simple snapshot with timestamp & interpolation
|
||||||
|
struct SimpleCapture : Capture
|
||||||
|
{
|
||||||
|
public double timestamp { get; set; }
|
||||||
|
public int value;
|
||||||
|
|
||||||
|
public SimpleCapture(double timestamp, int value)
|
||||||
|
{
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawGizmo() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LagCompensationTests
|
||||||
|
{
|
||||||
|
// buffer for convenience so we don't have to create it manually each time
|
||||||
|
Queue<KeyValuePair<double, SimpleCapture>> history;
|
||||||
|
|
||||||
|
// some defaults
|
||||||
|
const int HistoryLimit = 4;
|
||||||
|
const double Interval = 1;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
history = new Queue<KeyValuePair<double, SimpleCapture>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Insert()
|
||||||
|
{
|
||||||
|
// insert a few
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 2, new SimpleCapture(2, 20));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 3, new SimpleCapture(3, 30));
|
||||||
|
|
||||||
|
Assert.That(history.Count, Is.EqualTo(3));
|
||||||
|
Assert.That(history.ElementAt(0).Key, Is.EqualTo(1));
|
||||||
|
Assert.That(history.ElementAt(1).Key, Is.EqualTo(2));
|
||||||
|
Assert.That(history.ElementAt(2).Key, Is.EqualTo(3));
|
||||||
|
Assert.That(history.ElementAt(0).Value.value, Is.EqualTo(10));
|
||||||
|
Assert.That(history.ElementAt(1).Value.value, Is.EqualTo(20));
|
||||||
|
Assert.That(history.ElementAt(2).Value.value, Is.EqualTo(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InsertAboveLimit()
|
||||||
|
{
|
||||||
|
// inserting more than limit, should evict the oldest one
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 2, new SimpleCapture(2, 20));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 3, new SimpleCapture(3, 30));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 4, new SimpleCapture(4, 40));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 5, new SimpleCapture(5, 50));
|
||||||
|
|
||||||
|
Assert.That(history.Count, Is.EqualTo(4));
|
||||||
|
Assert.That(history.ElementAt(0).Key, Is.EqualTo(2));
|
||||||
|
Assert.That(history.ElementAt(1).Key, Is.EqualTo(3));
|
||||||
|
Assert.That(history.ElementAt(2).Key, Is.EqualTo(4));
|
||||||
|
Assert.That(history.ElementAt(3).Key, Is.EqualTo(5));
|
||||||
|
Assert.That(history.ElementAt(0).Value.value, Is.EqualTo(20));
|
||||||
|
Assert.That(history.ElementAt(1).Value.value, Is.EqualTo(30));
|
||||||
|
Assert.That(history.ElementAt(2).Value.value, Is.EqualTo(40));
|
||||||
|
Assert.That(history.ElementAt(3).Value.value, Is.EqualTo(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Empty()
|
||||||
|
{
|
||||||
|
Assert.That(LagCompensation.Sample(history, 0, Interval, out _, out _, out _), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Single_Interpolate()
|
||||||
|
{
|
||||||
|
// always need at least two values
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
Assert.That(LagCompensation.Sample(history, 0.5, Interval, out _, out _, out _), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Single_Extrapolate()
|
||||||
|
{
|
||||||
|
// insert a few
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
Assert.That(LagCompensation.Sample(history, 1.5, Interval, out _, out _, out _), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Double_Interpolate()
|
||||||
|
{
|
||||||
|
// always need at least two values
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 2, new SimpleCapture(2, 20));
|
||||||
|
|
||||||
|
// sample older than first
|
||||||
|
Assert.That(LagCompensation.Sample(history, 0, Interval, out SimpleCapture before, out SimpleCapture after, out double t), Is.False);
|
||||||
|
|
||||||
|
// sample exactly first
|
||||||
|
Assert.That(LagCompensation.Sample(history, 1, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(10));
|
||||||
|
Assert.That(after.value, Is.EqualTo(10));
|
||||||
|
Assert.That(t, Is.EqualTo(0.0));
|
||||||
|
|
||||||
|
// sample between first and second
|
||||||
|
Assert.That(LagCompensation.Sample(history, 1.5, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(10));
|
||||||
|
Assert.That(after.value, Is.EqualTo(20));
|
||||||
|
Assert.That(t, Is.EqualTo(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Double_Extrapolate()
|
||||||
|
{
|
||||||
|
// insert a few
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 2, new SimpleCapture(2, 20));
|
||||||
|
|
||||||
|
// sample newer than newest: should extrapolate even if we only have one entry
|
||||||
|
Assert.That(LagCompensation.Sample(history, 2.5, Interval, out SimpleCapture before, out SimpleCapture after, out double t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(10));
|
||||||
|
Assert.That(after.value, Is.EqualTo(20));
|
||||||
|
Assert.That(t, Is.EqualTo(1.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Triple_Interpolate()
|
||||||
|
{
|
||||||
|
// insert a few
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 2, new SimpleCapture(2, 20));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 3, new SimpleCapture(3, 30));
|
||||||
|
|
||||||
|
// sample older than first
|
||||||
|
Assert.That(LagCompensation.Sample(history, 0, Interval, out SimpleCapture before, out SimpleCapture after, out double t), Is.False);
|
||||||
|
|
||||||
|
// sample exactly first
|
||||||
|
Assert.That(LagCompensation.Sample(history, 1, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(10));
|
||||||
|
Assert.That(after.value, Is.EqualTo(10));
|
||||||
|
Assert.That(t, Is.EqualTo(0.0));
|
||||||
|
|
||||||
|
// sample between first and second
|
||||||
|
Assert.That(LagCompensation.Sample(history, 1.5, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(10));
|
||||||
|
Assert.That(after.value, Is.EqualTo(20));
|
||||||
|
Assert.That(t, Is.EqualTo(0.5));
|
||||||
|
|
||||||
|
// sample exactly second
|
||||||
|
Assert.That(LagCompensation.Sample(history, 2, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(20));
|
||||||
|
Assert.That(after.value, Is.EqualTo(20));
|
||||||
|
Assert.That(t, Is.EqualTo(0.0));
|
||||||
|
|
||||||
|
// sample between second and third
|
||||||
|
Assert.That(LagCompensation.Sample(history, 2.5, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(20));
|
||||||
|
Assert.That(after.value, Is.EqualTo(30));
|
||||||
|
Assert.That(t, Is.EqualTo(0.5));
|
||||||
|
|
||||||
|
// sample exactly third
|
||||||
|
Assert.That(LagCompensation.Sample(history, 3, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(30));
|
||||||
|
Assert.That(after.value, Is.EqualTo(30));
|
||||||
|
Assert.That(t, Is.EqualTo(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Sample_Triple_Extrapolate()
|
||||||
|
{
|
||||||
|
// let's say we capture every 100 ms:
|
||||||
|
// 100, 200, 300, 400
|
||||||
|
// and the server is at 499
|
||||||
|
// if a client sends CmdFire at time 480, then there's no history entry.
|
||||||
|
// => adding the current entry every time would be too expensive.
|
||||||
|
// worst case we would capture at 401, 402, 403, 404, ... 100 times
|
||||||
|
// => not extrapolating isn't great. low latency clients would be
|
||||||
|
// punished by missing their targets since no entry at 'time' was found.
|
||||||
|
// => extrapolation is the best solution. make sure this works as
|
||||||
|
// expected and within limits.
|
||||||
|
|
||||||
|
// insert a few
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 1, new SimpleCapture(1, 10));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 2, new SimpleCapture(2, 20));
|
||||||
|
LagCompensation.Insert(history, HistoryLimit, 3, new SimpleCapture(3, 30));
|
||||||
|
|
||||||
|
// sample at 3.9, just before we capture the next one.
|
||||||
|
// this should return before=after=3, with t=1.9.
|
||||||
|
// the user can then extrapolate manually.
|
||||||
|
Assert.That(LagCompensation.Sample(history, 3.9, Interval, out SimpleCapture before, out SimpleCapture after, out double t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(20));
|
||||||
|
Assert.That(after.value, Is.EqualTo(30));
|
||||||
|
Assert.That(t, Is.EqualTo(1.9));
|
||||||
|
|
||||||
|
// exactly interval is still fine
|
||||||
|
Assert.That(LagCompensation.Sample(history, 4, Interval, out before, out after, out t), Is.True);
|
||||||
|
Assert.That(before.value, Is.EqualTo(20));
|
||||||
|
Assert.That(after.value, Is.EqualTo(30));
|
||||||
|
Assert.That(t, Is.EqualTo(2.0));
|
||||||
|
|
||||||
|
// it should never extrapolate further than one interval.
|
||||||
|
Assert.That(LagCompensation.Sample(history, 4.01, Interval, out before, out after, out t), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EstimateTime()
|
||||||
|
{
|
||||||
|
// server is at t=100
|
||||||
|
// client has an rtt of 80ms
|
||||||
|
// snapshot interpolation buffer time is 30ms
|
||||||
|
// 100 - 0.080/2 - 0.030 = 99.93
|
||||||
|
Assert.That(LagCompensation.EstimateTime(100, 0.080, 0.030), Is.EqualTo(99.93).Within(0.001));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 094f4925fa98415da2f06b9daa0fd3e5
|
||||||
|
timeCreated: 1687927277
|
Loading…
Reference in New Issue
Block a user