2024-10-17 17:23:05 +03:00

241 lines
8.9 KiB
C#

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