Formatting

This commit is contained in:
MrGadget1024 2023-02-24 03:38:18 -05:00
parent a424ee87d1
commit d92cc65315
7 changed files with 3965 additions and 3965 deletions

View File

@ -3,180 +3,180 @@
namespace Mirror namespace Mirror
{ {
public static partial class NetworkClient public static partial class NetworkClient
{ {
// TODO expose the settings to the user later. // TODO expose the settings to the user later.
// via NetMan or NetworkClientConfig or NetworkClient as component etc. // via NetMan or NetworkClientConfig or NetworkClient as component etc.
// decrease bufferTime at runtime to see the catchup effect. // decrease bufferTime at runtime to see the catchup effect.
// increase to see slowdown. // increase to see slowdown.
// 'double' so we can have very precise dynamic adjustment without rounding // 'double' so we can have very precise dynamic adjustment without rounding
[Header("Snapshot Interpolation: Buffering")] [Header("Snapshot Interpolation: Buffering")]
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")] [Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
public static double bufferTimeMultiplier = 2; public static double bufferTimeMultiplier = 2;
public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
// <servertime, snaps> // <servertime, snaps>
public static SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>(); public static SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
// for smooth interpolation, we need to interpolate along server time. // for smooth interpolation, we need to interpolate along server time.
// any other time (arrival on client, client local time, etc.) is not // any other time (arrival on client, client local time, etc.) is not
// going to give smooth results. // going to give smooth results.
// in other words, this is the remote server's time, but adjusted. // in other words, this is the remote server's time, but adjusted.
// //
// internal for use from NetworkTime. // internal for use from NetworkTime.
// double for long running servers, see NetworkTime comments. // double for long running servers, see NetworkTime comments.
internal static double localTimeline; internal static double localTimeline;
// catchup / slowdown adjustments are applied to timescale, // catchup / slowdown adjustments are applied to timescale,
// to be adjusted in every update instead of when receiving messages. // to be adjusted in every update instead of when receiving messages.
internal static double localTimescale = 1; internal static double localTimescale = 1;
// catchup ///////////////////////////////////////////////////////////// // catchup /////////////////////////////////////////////////////////////
// catchup thresholds in 'frames'. // catchup thresholds in 'frames'.
// half a frame might be too aggressive. // half a frame might be too aggressive.
[Header("Snapshot Interpolation: Catchup / Slowdown")] [Header("Snapshot Interpolation: Catchup / Slowdown")]
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")] [Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
public static float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots public static float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")] [Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
public static float catchupPositiveThreshold = 1; public static float catchupPositiveThreshold = 1;
[Tooltip("Local timeline acceleration in % while catching up.")] [Tooltip("Local timeline acceleration in % while catching up.")]
[Range(0, 1)] [Range(0, 1)]
public static double catchupSpeed = 0.01f; // 1% public static double catchupSpeed = 0.01f; // 1%
[Tooltip("Local timeline slowdown in % while slowing down.")] [Tooltip("Local timeline slowdown in % while slowing down.")]
[Range(0, 1)] [Range(0, 1)]
public static double slowdownSpeed = 0.01f; // 1% public static double slowdownSpeed = 0.01f; // 1%
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
// we use EMA to average the last second worth of snapshot time diffs. // 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 // 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 // would be the same, but a moving average is faster because we only
// ever add one value. // ever add one value.
static ExponentialMovingAverage driftEma; static ExponentialMovingAverage driftEma;
// dynamic buffer time adjustment ////////////////////////////////////// // dynamic buffer time adjustment //////////////////////////////////////
// dynamically adjusts bufferTimeMultiplier for smooth results. // dynamically adjusts bufferTimeMultiplier for smooth results.
// to understand how this works, try this manually: // to understand how this works, try this manually:
// //
// - disable dynamic adjustment // - disable dynamic adjustment
// - set jitter = 0.2 (20% is a lot!) // - set jitter = 0.2 (20% is a lot!)
// - notice some stuttering // - notice some stuttering
// - disable interpolation to see just how much jitter this really is(!) // - disable interpolation to see just how much jitter this really is(!)
// - enable interpolation again // - enable interpolation again
// - manually increase bufferTimeMultiplier to 3-4 // - manually increase bufferTimeMultiplier to 3-4
// ... the cube slows down (blue) until it's smooth // ... the cube slows down (blue) until it's smooth
// - with dynamic adjustment enabled, it will set 4 automatically // - with dynamic adjustment enabled, it will set 4 automatically
// ... the cube slows down (blue) until it's smooth as well // ... the cube slows down (blue) until it's smooth as well
// //
// note that 20% jitter is extreme. // note that 20% jitter is extreme.
// for this to be perfectly smooth, set the safety tolerance to '2'. // for this to be perfectly smooth, set the safety tolerance to '2'.
// but realistically this is not necessary, and '1' is enough. // but realistically this is not necessary, and '1' is enough.
[Header("Snapshot Interpolation: Dynamic Adjustment")] [Header("Snapshot Interpolation: Dynamic Adjustment")]
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
public static bool dynamicAdjustment = true; public static bool dynamicAdjustment = true;
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
public static float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments) public static float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
public static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time public static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
// OnValidate: see NetworkClient.cs // OnValidate: see NetworkClient.cs
// add snapshot & initialize client interpolation time if needed // add snapshot & initialize client interpolation time if needed
// initialization called from Awake // initialization called from Awake
static void InitTimeInterpolation() static void InitTimeInterpolation()
{ {
// reset timeline, localTimescale & snapshots from last session (if any) // reset timeline, localTimescale & snapshots from last session (if any)
// Don't reset bufferTimeMultiplier here - whatever their network condition // Don't reset bufferTimeMultiplier here - whatever their network condition
// was when they disconnected, it won't have changed on immediate reconnect. // was when they disconnected, it won't have changed on immediate reconnect.
localTimeline = 0; localTimeline = 0;
localTimescale = 1; localTimescale = 1;
snapshots.Clear(); snapshots.Clear();
// initialize EMA with 'emaDuration' seconds worth of history. // initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values. // 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds. // multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * driftEmaDuration); driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * deliveryTimeEmaDuration); deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * deliveryTimeEmaDuration);
} }
// server sends TimeSnapshotMessage every sendInterval. // server sends TimeSnapshotMessage every sendInterval.
// batching already includes the remoteTimestamp. // batching already includes the remoteTimestamp.
// we simply insert it on-message here. // we simply insert it on-message here.
// => only for reliable channel. unreliable would always arrive earlier. // => only for reliable channel. unreliable would always arrive earlier.
static void OnTimeSnapshotMessage(TimeSnapshotMessage _) static void OnTimeSnapshotMessage(TimeSnapshotMessage _)
{ {
// insert another snapshot for snapshot interpolation. // insert another snapshot for snapshot interpolation.
// before calling OnDeserialize so components can use // before calling OnDeserialize so components can use
// NetworkTime.time and NetworkTime.timeStamp. // NetworkTime.time and NetworkTime.timeStamp.
#if !UNITY_2020_3_OR_NEWER #if !UNITY_2020_3_OR_NEWER
// Unity 2019 doesn't have Time.timeAsDouble yet // Unity 2019 doesn't have Time.timeAsDouble yet
OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime));
#else #else
OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble));
#endif #endif
} }
// see comments at the top of this file // see comments at the top of this file
public static void OnTimeSnapshot(TimeSnapshot snap) public static void OnTimeSnapshot(TimeSnapshot snap)
{ {
// Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}"); // Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}");
// (optional) dynamic adjustment // (optional) dynamic adjustment
if (dynamicAdjustment) if (dynamicAdjustment)
{ {
// set bufferTime on the fly. // set bufferTime on the fly.
// shows in inspector for easier debugging :) // shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
NetworkServer.sendInterval, NetworkServer.sendInterval,
deliveryTimeEma.StandardDeviation, deliveryTimeEma.StandardDeviation,
dynamicAdjustmentTolerance dynamicAdjustmentTolerance
); );
} }
// insert into the buffer & initialize / adjust / catchup // insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust( SnapshotInterpolation.InsertAndAdjust(
snapshots, snapshots,
snap, snap,
ref localTimeline, ref localTimeline,
ref localTimescale, ref localTimescale,
NetworkServer.sendInterval, NetworkServer.sendInterval,
bufferTime, bufferTime,
NetworkServer.bufferTimeMultiplierForClamping, NetworkServer.bufferTimeMultiplierForClamping,
catchupSpeed, catchupSpeed,
slowdownSpeed, slowdownSpeed,
ref driftEma, ref driftEma,
catchupNegativeThreshold, catchupNegativeThreshold,
catchupPositiveThreshold, catchupPositiveThreshold,
ref deliveryTimeEma); ref deliveryTimeEma);
// Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}"); // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}");
} }
// call this from early update, so the timeline is safe to use in update // call this from early update, so the timeline is safe to use in update
static void UpdateTimeInterpolation() static void UpdateTimeInterpolation()
{ {
// only while we have snapshots. // only while we have snapshots.
// timeline starts when the first snapshot arrives. // timeline starts when the first snapshot arrives.
if (snapshots.Count > 0) if (snapshots.Count > 0)
{ {
// progress local timeline. // progress local timeline.
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale); SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale);
// progress local interpolation. // progress local interpolation.
// TimeSnapshot doesn't interpolate anything. // TimeSnapshot doesn't interpolate anything.
// this is merely to keep removing older snapshots. // this is merely to keep removing older snapshots.
SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t); SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t);
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
} }
} }
} }
} }

View File

@ -5,253 +5,253 @@
namespace Mirror namespace Mirror
{ {
public class NetworkConnectionToClient : NetworkConnection public class NetworkConnectionToClient : NetworkConnection
{ {
// rpcs are collected in a buffer, and then flushed out together. // rpcs are collected in a buffer, and then flushed out together.
// this way we don't need one NetworkMessage per rpc. // this way we don't need one NetworkMessage per rpc.
// => prepares for LocalWorldState as well. // => prepares for LocalWorldState as well.
// ensure max size when adding! // ensure max size when adding!
readonly NetworkWriter reliableRpcs = new NetworkWriter(); readonly NetworkWriter reliableRpcs = new NetworkWriter();
readonly NetworkWriter unreliableRpcs = new NetworkWriter(); readonly NetworkWriter unreliableRpcs = new NetworkWriter();
public override string address => public override string address =>
Transport.active.ServerGetClientAddress(connectionId); Transport.active.ServerGetClientAddress(connectionId);
/// <summary>NetworkIdentities that this connection can see</summary> /// <summary>NetworkIdentities that this connection can see</summary>
// TODO move to server's NetworkConnectionToClient? // TODO move to server's NetworkConnectionToClient?
public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>(); public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();
// Deprecated 2022-10-13 // Deprecated 2022-10-13
[Obsolete(".clientOwnedObjects was renamed to .owned :)")] [Obsolete(".clientOwnedObjects was renamed to .owned :)")]
public HashSet<NetworkIdentity> clientOwnedObjects => owned; public HashSet<NetworkIdentity> clientOwnedObjects => owned;
// unbatcher // unbatcher
public Unbatcher unbatcher = new Unbatcher(); public Unbatcher unbatcher = new Unbatcher();
// server runs a time snapshot interpolation for each client's local time. // server runs a time snapshot interpolation for each client's local time.
// this is necessary for client auth movement to still be smooth on the // this is necessary for client auth movement to still be smooth on the
// server for host mode. // server for host mode.
// TODO move them along server's timeline in the future. // TODO move them along server's timeline in the future.
// perhaps with an offset. // perhaps with an offset.
// for now, keep compatibility by manually constructing a timeline. // for now, keep compatibility by manually constructing a timeline.
ExponentialMovingAverage driftEma; ExponentialMovingAverage driftEma;
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
public double remoteTimeline; public double remoteTimeline;
public double remoteTimescale; public double remoteTimescale;
double bufferTimeMultiplier = 2; double bufferTimeMultiplier = 2;
double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
// <clienttime, snaps> // <clienttime, snaps>
readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>(); readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
// Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients. // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
public int snapshotBufferSizeLimit = 64; public int snapshotBufferSizeLimit = 64;
public NetworkConnectionToClient(int networkConnectionId) public NetworkConnectionToClient(int networkConnectionId)
: base(networkConnectionId) : base(networkConnectionId)
{ {
// initialize EMA with 'emaDuration' seconds worth of history. // initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values. // 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds. // multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration); driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration); deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration);
// buffer limit should be at least multiplier to have enough in there // buffer limit should be at least multiplier to have enough in there
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit); snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit);
} }
public void OnTimeSnapshot(TimeSnapshot snapshot) public void OnTimeSnapshot(TimeSnapshot snapshot)
{ {
// protect against ever growing buffer size attacks // protect against ever growing buffer size attacks
if (snapshots.Count >= snapshotBufferSizeLimit) return; if (snapshots.Count >= snapshotBufferSizeLimit) return;
// (optional) dynamic adjustment // (optional) dynamic adjustment
if (NetworkClient.dynamicAdjustment) if (NetworkClient.dynamicAdjustment)
{ {
// set bufferTime on the fly. // set bufferTime on the fly.
// shows in inspector for easier debugging :) // shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
NetworkServer.sendInterval, NetworkServer.sendInterval,
deliveryTimeEma.StandardDeviation, deliveryTimeEma.StandardDeviation,
NetworkClient.dynamicAdjustmentTolerance NetworkClient.dynamicAdjustmentTolerance
); );
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} "); // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
} }
// insert into the server buffer & initialize / adjust / catchup // insert into the server buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust( SnapshotInterpolation.InsertAndAdjust(
snapshots, snapshots,
snapshot, snapshot,
ref remoteTimeline, ref remoteTimeline,
ref remoteTimescale, ref remoteTimescale,
NetworkServer.sendInterval, NetworkServer.sendInterval,
bufferTime, bufferTime,
NetworkServer.bufferTimeMultiplierForClamping, NetworkServer.bufferTimeMultiplierForClamping,
NetworkClient.catchupSpeed, NetworkClient.catchupSpeed,
NetworkClient.slowdownSpeed, NetworkClient.slowdownSpeed,
ref driftEma, ref driftEma,
NetworkClient.catchupNegativeThreshold, NetworkClient.catchupNegativeThreshold,
NetworkClient.catchupPositiveThreshold, NetworkClient.catchupPositiveThreshold,
ref deliveryTimeEma ref deliveryTimeEma
); );
} }
public void UpdateTimeInterpolation() public void UpdateTimeInterpolation()
{ {
// timeline starts when the first snapshot arrives. // timeline starts when the first snapshot arrives.
if (snapshots.Count > 0) if (snapshots.Count > 0)
{ {
// progress local timeline. // progress local timeline.
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale); SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale);
// progress local interpolation. // progress local interpolation.
// TimeSnapshot doesn't interpolate anything. // TimeSnapshot doesn't interpolate anything.
// this is merely to keep removing older snapshots. // this is merely to keep removing older snapshots.
SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _); SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _);
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
} }
} }
// Send stage three: hand off to transport // Send stage three: hand off to transport
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) => protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
Transport.active.ServerSend(connectionId, segment, channelId); Transport.active.ServerSend(connectionId, segment, channelId);
void FlushRpcs(NetworkWriter buffer, int channelId) void FlushRpcs(NetworkWriter buffer, int channelId)
{ {
if (buffer.Position > 0) if (buffer.Position > 0)
{ {
Send(new RpcBufferMessage{ payload = buffer }, channelId); Send(new RpcBufferMessage { payload = buffer }, channelId);
buffer.Position = 0; buffer.Position = 0;
} }
} }
// helper for both channels // helper for both channels
void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize) void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize)
{ {
// calculate buffer limit. we can only fit so much into a message. // calculate buffer limit. we can only fit so much into a message.
// max - message header - WriteArraySegment size header - batch header // max - message header - WriteArraySegment size header - batch header
int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize; int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize;
// remember previous valid position // remember previous valid position
int before = buffer.Position; int before = buffer.Position;
// serialize the message without header // serialize the message without header
buffer.Write(message); buffer.Write(message);
// before we potentially flush out old messages, // before we potentially flush out old messages,
// let's ensure this single message can even fit the limit. // let's ensure this single message can even fit the limit.
// otherwise no point in flushing. // otherwise no point in flushing.
int messageSize = buffer.Position - before; int messageSize = buffer.Position - before;
if (messageSize > bufferLimit) if (messageSize > bufferLimit)
{ {
Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}"); Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}");
return; return;
} }
// too much to fit into max message size? // too much to fit into max message size?
// then flush first, then write it again. // then flush first, then write it again.
// (message + message header + 4 bytes WriteArraySegment header) // (message + message header + 4 bytes WriteArraySegment header)
if (buffer.Position > bufferLimit) if (buffer.Position > bufferLimit)
{ {
buffer.Position = before; buffer.Position = before;
FlushRpcs(buffer, channelId); // this resets position FlushRpcs(buffer, channelId); // this resets position
buffer.Write(message); buffer.Write(message);
} }
} }
internal void BufferRpc(RpcMessage message, int channelId) internal void BufferRpc(RpcMessage message, int channelId)
{ {
int maxMessageSize = Transport.active.GetMaxPacketSize(channelId); int maxMessageSize = Transport.active.GetMaxPacketSize(channelId);
if (channelId == Channels.Reliable) if (channelId == Channels.Reliable)
{ {
BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize); BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize);
} }
else if (channelId == Channels.Unreliable) else if (channelId == Channels.Unreliable)
{ {
BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize); BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize);
} }
} }
internal override void Update() internal override void Update()
{ {
// send rpc buffers // send rpc buffers
FlushRpcs(reliableRpcs, Channels.Reliable); FlushRpcs(reliableRpcs, Channels.Reliable);
FlushRpcs(unreliableRpcs, Channels.Unreliable); FlushRpcs(unreliableRpcs, Channels.Unreliable);
// call base update to flush out batched messages // call base update to flush out batched messages
base.Update(); base.Update();
} }
/// <summary>Disconnects this connection.</summary> /// <summary>Disconnects this connection.</summary>
public override void Disconnect() public override void Disconnect()
{ {
// set not ready and handle clientscene disconnect in any case // set not ready and handle clientscene disconnect in any case
// (might be client or host mode here) // (might be client or host mode here)
isReady = false; isReady = false;
reliableRpcs.Position = 0; reliableRpcs.Position = 0;
unreliableRpcs.Position = 0; unreliableRpcs.Position = 0;
Transport.active.ServerDisconnect(connectionId); Transport.active.ServerDisconnect(connectionId);
// IMPORTANT: NetworkConnection.Disconnect() is NOT called for // IMPORTANT: NetworkConnection.Disconnect() is NOT called for
// voluntary disconnects from the other end. // voluntary disconnects from the other end.
// -> so all 'on disconnect' cleanup code needs to be in // -> so all 'on disconnect' cleanup code needs to be in
// OnTransportDisconnect, where it's called for both voluntary // OnTransportDisconnect, where it's called for both voluntary
// and involuntary disconnects! // and involuntary disconnects!
} }
internal void AddToObserving(NetworkIdentity netIdentity) internal void AddToObserving(NetworkIdentity netIdentity)
{ {
observing.Add(netIdentity); observing.Add(netIdentity);
// spawn identity for this conn // spawn identity for this conn
NetworkServer.ShowForConnection(netIdentity, this); NetworkServer.ShowForConnection(netIdentity, this);
} }
internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed)
{ {
observing.Remove(netIdentity); observing.Remove(netIdentity);
if (!isDestroyed) if (!isDestroyed)
{ {
// hide identity for this conn // hide identity for this conn
NetworkServer.HideForConnection(netIdentity, this); NetworkServer.HideForConnection(netIdentity, this);
} }
} }
internal void RemoveFromObservingsObservers() internal void RemoveFromObservingsObservers()
{ {
foreach (NetworkIdentity netIdentity in observing) foreach (NetworkIdentity netIdentity in observing)
{ {
netIdentity.RemoveObserver(this); netIdentity.RemoveObserver(this);
} }
observing.Clear(); observing.Clear();
} }
internal void AddOwnedObject(NetworkIdentity obj) internal void AddOwnedObject(NetworkIdentity obj)
{ {
owned.Add(obj); owned.Add(obj);
} }
internal void RemoveOwnedObject(NetworkIdentity obj) internal void RemoveOwnedObject(NetworkIdentity obj)
{ {
owned.Remove(obj); owned.Remove(obj);
} }
internal void DestroyOwnedObjects() internal void DestroyOwnedObjects()
{ {
// create a copy because the list might be modified when destroying // create a copy because the list might be modified when destroying
HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned); HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned);
foreach (NetworkIdentity netIdentity in tmp) foreach (NetworkIdentity netIdentity in tmp)
{ {
if (netIdentity != null) if (netIdentity != null)
{ {
NetworkServer.Destroy(netIdentity.gameObject); NetworkServer.Destroy(netIdentity.gameObject);
} }
} }
// clear the hashset because we destroyed them all // clear the hashset because we destroyed them all
owned.Clear(); owned.Clear();
} }
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -14,351 +14,351 @@
namespace Mirror namespace Mirror
{ {
public static class SortedListExtensions public static class SortedListExtensions
{ {
// removes the first 'amount' elements from the sorted list // removes the first 'amount' elements from the sorted list
public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount) public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount)
{ {
// remove the first element 'amount' times. // remove the first element 'amount' times.
// handles -1 and > count safely. // handles -1 and > count safely.
for (int i = 0; i < amount && i < list.Count; ++i) for (int i = 0; i < amount && i < list.Count; ++i)
list.RemoveAt(0); list.RemoveAt(0);
} }
} }
public static class SnapshotInterpolation public static class SnapshotInterpolation
{ {
// calculate timescale for catch-up / slow-down // calculate timescale for catch-up / slow-down
// note that negative threshold should be <0. // note that negative threshold should be <0.
// caller should verify (i.e. Unity OnValidate). // caller should verify (i.e. Unity OnValidate).
// improves branch prediction. // improves branch prediction.
public static double Timescale( public static double Timescale(
double drift, // how far we are off from bufferTime double drift, // how far we are off from bufferTime
double catchupSpeed, // in % [0,1] double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1] double slowdownSpeed, // in % [0,1]
double catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) double catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
double catchupPositiveThreshold) // in % of sendInterval) double catchupPositiveThreshold) // in % of sendInterval)
{ {
// if the drift time is too large, it means we are behind more time. // if the drift time is too large, it means we are behind more time.
// so we need to speed up the timescale. // so we need to speed up the timescale.
// note the threshold should be sendInterval * catchupThreshold. // note the threshold should be sendInterval * catchupThreshold.
if (drift > catchupPositiveThreshold) if (drift > catchupPositiveThreshold)
{ {
// localTimeline += 0.001; // too simple, this would ping pong // localTimeline += 0.001; // too simple, this would ping pong
return 1 + catchupSpeed; // n% faster return 1 + catchupSpeed; // n% faster
} }
// if the drift time is too small, it means we are ahead of time. // if the drift time is too small, it means we are ahead of time.
// so we need to slow down the timescale. // so we need to slow down the timescale.
// note the threshold should be sendInterval * catchupThreshold. // note the threshold should be sendInterval * catchupThreshold.
if (drift < catchupNegativeThreshold) if (drift < catchupNegativeThreshold)
{ {
// localTimeline -= 0.001; // too simple, this would ping pong // localTimeline -= 0.001; // too simple, this would ping pong
return 1 - slowdownSpeed; // n% slower return 1 - slowdownSpeed; // n% slower
} }
// keep constant timescale while within threshold. // keep constant timescale while within threshold.
// this way we have perfectly smooth speed most of the time. // this way we have perfectly smooth speed most of the time.
return 1; return 1;
} }
// calculate dynamic buffer time adjustment // calculate dynamic buffer time adjustment
public static double DynamicAdjustment( public static double DynamicAdjustment(
double sendInterval, double sendInterval,
double jitterStandardDeviation, double jitterStandardDeviation,
double dynamicAdjustmentTolerance) double dynamicAdjustmentTolerance)
{ {
// jitter is equal to delivery time standard variation. // jitter is equal to delivery time standard variation.
// delivery time is made up of 'sendInterval+jitter'. // delivery time is made up of 'sendInterval+jitter'.
// .Average would be dampened by the constant sendInterval // .Average would be dampened by the constant sendInterval
// .StandardDeviation is the changes in 'jitter' that we want // .StandardDeviation is the changes in 'jitter' that we want
// so add it to send interval again. // so add it to send interval again.
double intervalWithJitter = sendInterval + jitterStandardDeviation; double intervalWithJitter = sendInterval + jitterStandardDeviation;
// how many multiples of sendInterval is that? // how many multiples of sendInterval is that?
// we want to convert to bufferTimeMultiplier later. // we want to convert to bufferTimeMultiplier later.
double multiples = intervalWithJitter / sendInterval; double multiples = intervalWithJitter / sendInterval;
// add the tolerance // add the tolerance
double safezone = multiples + dynamicAdjustmentTolerance; double safezone = multiples + dynamicAdjustmentTolerance;
// Console.WriteLine($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}"); // Console.WriteLine($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
return safezone; return safezone;
} }
// helper function to insert a snapshot if it doesn't exist yet. // helper function to insert a snapshot if it doesn't exist yet.
// extra function so we can use it for both cases: // extra function so we can use it for both cases:
// NetworkClient global timeline insertions & adjustments via Insert<T>. // NetworkClient global timeline insertions & adjustments via Insert<T>.
// NetworkBehaviour local insertion without any time adjustments. // NetworkBehaviour local insertion without any time adjustments.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool InsertIfNotExists<T>( public static bool InsertIfNotExists<T>(
SortedList<double, T> buffer, // snapshot buffer SortedList<double, T> buffer, // snapshot buffer
T snapshot) // the newly received snapshot T snapshot) // the newly received snapshot
where T : Snapshot where T : Snapshot
{ {
// SortedList does not allow duplicates. // SortedList does not allow duplicates.
// we don't need to check ContainsKey (which is expensive). // we don't need to check ContainsKey (which is expensive).
// simply add and compare count before/after for the return value. // simply add and compare count before/after for the return value.
//if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive //if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive
// buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists // buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists
int before = buffer.Count; int before = buffer.Count;
buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists
return buffer.Count > before; return buffer.Count > before;
} }
// call this for every received snapshot. // call this for every received snapshot.
// adds / inserts it to the list & initializes local time if needed. // adds / inserts it to the list & initializes local time if needed.
public static void InsertAndAdjust<T>( public static void InsertAndAdjust<T>(
SortedList<double, T> buffer, // snapshot buffer SortedList<double, T> buffer, // snapshot buffer
T snapshot, // the newly received snapshot T snapshot, // the newly received snapshot
ref double localTimeline, // local interpolation time based on server time ref double localTimeline, // local interpolation time based on server time
ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
float sendInterval, // for debugging float sendInterval, // for debugging
double bufferTime, // offset for buffering double bufferTime, // offset for buffering
float clampMultiplier, // multiplier to check if time needs to be clamped float clampMultiplier, // multiplier to check if time needs to be clamped
double catchupSpeed, // in % [0,1] double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1] double slowdownSpeed, // in % [0,1]
ref ExponentialMovingAverage driftEma, // for catchup / slowdown ref ExponentialMovingAverage driftEma, // for catchup / slowdown
float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
float catchupPositiveThreshold, // in % of sendInterval float catchupPositiveThreshold, // in % of sendInterval
ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
where T : Snapshot where T : Snapshot
{ {
// first snapshot? // first snapshot?
// initialize local timeline. // initialize local timeline.
// we want it to be behind by 'offset'. // we want it to be behind by 'offset'.
// //
// note that the first snapshot may be a lagging packet. // note that the first snapshot may be a lagging packet.
// so we would always be behind by that lag. // so we would always be behind by that lag.
// this requires catchup later. // this requires catchup later.
if (buffer.Count == 0) if (buffer.Count == 0)
localTimeline = snapshot.remoteTime - bufferTime; localTimeline = snapshot.remoteTime - bufferTime;
// insert into the buffer. // insert into the buffer.
// //
// note that we might insert it between our current interpolation // note that we might insert it between our current interpolation
// which is fine, it adds another data point for accuracy. // which is fine, it adds another data point for accuracy.
// //
// note that insert may be called twice for the same key. // note that insert may be called twice for the same key.
// by default, this would throw. // by default, this would throw.
// need to handle it silently. // need to handle it silently.
if (InsertIfNotExists(buffer, snapshot)) if (InsertIfNotExists(buffer, snapshot))
{ {
// dynamic buffer adjustment needs delivery interval jitter // dynamic buffer adjustment needs delivery interval jitter
if (buffer.Count >= 2) if (buffer.Count >= 2)
{ {
// note that this is not entirely accurate for scrambled inserts. // note that this is not entirely accurate for scrambled inserts.
// //
// we always use the last two, not what we just inserted // we always use the last two, not what we just inserted
// even if we were to use the diff for what we just inserted, // even if we were to use the diff for what we just inserted,
// a scrambled insert would still not be 100% accurate: // a scrambled insert would still not be 100% accurate:
// => assume a buffer of AC, with delivery time C-A // => assume a buffer of AC, with delivery time C-A
// => we then insert B, with delivery time B-A // => we then insert B, with delivery time B-A
// => but then technically the first C-A wasn't correct, // => but then technically the first C-A wasn't correct,
// as it would have to be C-B // as it would have to be C-B
// //
// in practice, scramble is rare and won't make much difference // in practice, scramble is rare and won't make much difference
double previousLocalTime = buffer.Values[buffer.Count - 2].localTime; double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime; double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
// this is the delivery time since last snapshot // this is the delivery time since last snapshot
double localDeliveryTime = lastestLocalTime - previousLocalTime; double localDeliveryTime = lastestLocalTime - previousLocalTime;
// feed the local delivery time to the EMA. // feed the local delivery time to the EMA.
// this is what the original stream did too. // this is what the original stream did too.
// our final dynamic buffer adjustment is different though. // our final dynamic buffer adjustment is different though.
// we use standard deviation instead of average. // we use standard deviation instead of average.
deliveryTimeEma.Add(localDeliveryTime); deliveryTimeEma.Add(localDeliveryTime);
} }
// adjust timescale to catch up / slow down after each insertion // adjust timescale to catch up / slow down after each insertion
// because that is when we add new values to our EMA. // because that is when we add new values to our EMA.
// we want localTimeline to be about 'bufferTime' behind. // we want localTimeline to be about 'bufferTime' behind.
// for that, we need the delivery time EMA. // for that, we need the delivery time EMA.
// snapshots may arrive out of order, we can not use last-timeline. // snapshots may arrive out of order, we can not use last-timeline.
// we need to use the inserted snapshot's time - timeline. // we need to use the inserted snapshot's time - timeline.
double latestRemoteTime = snapshot.remoteTime; double latestRemoteTime = snapshot.remoteTime;
TimeLineOverride(latestRemoteTime, bufferTime, clampMultiplier, ref localTimeline); TimeLineOverride(latestRemoteTime, bufferTime, clampMultiplier, ref localTimeline);
double timeDiff = latestRemoteTime - localTimeline; double timeDiff = latestRemoteTime - localTimeline;
if (buffer.Count > 1) if (buffer.Count > 1)
// next, calculate average of a few seconds worth of timediffs. // next, calculate average of a few seconds worth of timediffs.
// this gives smoother results. // this gives smoother results.
// //
// to calculate the average, we could simply loop through the // to calculate the average, we could simply loop through the
// last 'n' seconds worth of timediffs, but: // last 'n' seconds worth of timediffs, but:
// - our buffer may only store a few snapshots (bufferTime) // - our buffer may only store a few snapshots (bufferTime)
// - looping through seconds worth of snapshots every time is // - looping through seconds worth of snapshots every time is
// expensive // expensive
// //
// to solve this, we use an exponential moving average. // to solve this, we use an exponential moving average.
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
// which is basically fancy math to do the same but faster. // which is basically fancy math to do the same but faster.
// additionally, it allows us to look at more timeDiff values // additionally, it allows us to look at more timeDiff values
// than we sould have access to in our buffer :) // than we sould have access to in our buffer :)
driftEma.Add(timeDiff); driftEma.Add(timeDiff);
// next up, calculate how far we are currently away from bufferTime // next up, calculate how far we are currently away from bufferTime
double drift = driftEma.Value - bufferTime; double drift = driftEma.Value - bufferTime;
// convert relative thresholds to absolute values based on sendInterval // convert relative thresholds to absolute values based on sendInterval
double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold; double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold; double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
// next, set localTimescale to catchup consistently in Update(). // next, set localTimescale to catchup consistently in Update().
// we quantize between default/catchup/slowdown, // we quantize between default/catchup/slowdown,
// this way we have 'default' speed most of the time(!). // this way we have 'default' speed most of the time(!).
// and only catch up / slow down for a little bit occasionally. // and only catch up / slow down for a little bit occasionally.
// a consistent multiplier would never be exactly 1.0. // a consistent multiplier would never be exactly 1.0.
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold); localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
// debug logging // debug logging
// Console.WriteLine($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}"); // Console.WriteLine($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");
} }
} }
// If the time difference is more than X times of buffer, we will override time to be // If the time difference is more than X times of buffer, we will override time to be
// targetTime +- x times of buffer. // targetTime +- x times of buffer.
private static void TimeLineOverride(double latestRemoteTime, double bufferTime, float clampMultiplier, ref double localTimeline) private static void TimeLineOverride(double latestRemoteTime, double bufferTime, float clampMultiplier, ref double localTimeline)
{ {
// If we want local timeline to be around bufferTime slower, // If we want local timeline to be around bufferTime slower,
// Then over her we want to clamp localTimeline to be: // Then over her we want to clamp localTimeline to be:
// target +- multiplierCheck * bufferTime. // target +- multiplierCheck * bufferTime.
double targetTime = latestRemoteTime - bufferTime; double targetTime = latestRemoteTime - bufferTime;
localTimeline = Math.Clamp(localTimeline, targetTime - clampMultiplier * bufferTime, targetTime + clampMultiplier * bufferTime); localTimeline = Math.Clamp(localTimeline, targetTime - clampMultiplier * bufferTime, targetTime + clampMultiplier * bufferTime);
} }
// sample snapshot buffer to find the pair around the given time. // sample snapshot buffer to find the pair around the given time.
// returns indices so we can use it with RemoveRange to clear old snaps. // returns indices so we can use it with RemoveRange to clear old snaps.
// make sure to use use buffer.Values[from/to], not buffer[from/to]. // make sure to use use buffer.Values[from/to], not buffer[from/to].
// make sure to only call this is we have > 0 snapshots. // make sure to only call this is we have > 0 snapshots.
public static void Sample<T>( public static void Sample<T>(
SortedList<double, T> buffer, // snapshot buffer SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time double localTimeline, // local interpolation time based on server time
out int from, // the snapshot <= time out int from, // the snapshot <= time
out int to, // the snapshot >= time out int to, // the snapshot >= time
out double t) // interpolation factor out double t) // interpolation factor
where T : Snapshot where T : Snapshot
{ {
from = -1; from = -1;
to = -1; to = -1;
t = 0; t = 0;
// sample from [0,count-1] so we always have two at 'i' and 'i+1'. // sample from [0,count-1] so we always have two at 'i' and 'i+1'.
for (int i = 0; i < buffer.Count - 1; ++i) for (int i = 0; i < buffer.Count - 1; ++i)
{ {
// is local time between these two? // is local time between these two?
T first = buffer.Values[i]; T first = buffer.Values[i];
T second = buffer.Values[i + 1]; T second = buffer.Values[i + 1];
if (localTimeline >= first.remoteTime && if (localTimeline >= first.remoteTime &&
localTimeline <= second.remoteTime) localTimeline <= second.remoteTime)
{ {
// use these two snapshots // use these two snapshots
from = i; from = i;
to = i + 1; to = i + 1;
t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline); t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
return; return;
} }
} }
// didn't find two snapshots around local time. // didn't find two snapshots around local time.
// so pick either the first or last, depending on which is closer. // so pick either the first or last, depending on which is closer.
// oldest snapshot ahead of local time? // oldest snapshot ahead of local time?
if (buffer.Values[0].remoteTime > localTimeline) if (buffer.Values[0].remoteTime > localTimeline)
{ {
from = to = 0; from = to = 0;
t = 0; t = 0;
} }
// otherwise initialize both to the last one // otherwise initialize both to the last one
else else
{ {
from = to = buffer.Count - 1; from = to = buffer.Count - 1;
t = 0; t = 0;
} }
} }
// progress local timeline every update. // progress local timeline every update.
// //
// ONLY CALL IF SNAPSHOTS.COUNT > 0! // ONLY CALL IF SNAPSHOTS.COUNT > 0!
// //
// decoupled from Step<T> for easier testing and so we can progress // decoupled from Step<T> for easier testing and so we can progress
// time only once in NetworkClient, while stepping for each component. // time only once in NetworkClient, while stepping for each component.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StepTime( public static void StepTime(
double deltaTime, // engine delta time (unscaled) double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time ref double localTimeline, // local interpolation time based on server time
double localTimescale) // catchup / slowdown is applied to time every update) double localTimescale) // catchup / slowdown is applied to time every update)
{ {
// move local forward in time, scaled with catchup / slowdown applied // move local forward in time, scaled with catchup / slowdown applied
localTimeline += deltaTime * localTimescale; localTimeline += deltaTime * localTimescale;
} }
// sample, clear old. // sample, clear old.
// call this every update. // call this every update.
// //
// ONLY CALL IF SNAPSHOTS.COUNT > 0! // ONLY CALL IF SNAPSHOTS.COUNT > 0!
// //
// returns true if there is anything to apply (requires at least 1 snap) // returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'. // from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient) // this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually. // and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway. // besides, passing "Func Interpolate" would allocate anyway.
public static void StepInterpolation<T>( public static void StepInterpolation<T>(
SortedList<double, T> buffer, // snapshot buffer SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time double localTimeline, // local interpolation time based on server time
out T fromSnapshot, // we interpolate 'from' this snapshot out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1] out double t) // at ratio 't' [0,1]
where T : Snapshot where T : Snapshot
{ {
// check this in caller: // check this in caller:
// nothing to do if there are no snapshots at all yet // nothing to do if there are no snapshots at all yet
// if (buffer.Count == 0) return false; // if (buffer.Count == 0) return false;
// sample snapshot buffer at local interpolation time // sample snapshot buffer at local interpolation time
Sample(buffer, localTimeline, out int from, out int to, out t); Sample(buffer, localTimeline, out int from, out int to, out t);
// save from/to // save from/to
fromSnapshot = buffer.Values[from]; fromSnapshot = buffer.Values[from];
toSnapshot = buffer.Values[to]; toSnapshot = buffer.Values[to];
// remove older snapshots that we definitely don't need anymore. // remove older snapshots that we definitely don't need anymore.
// after(!) using the indices. // after(!) using the indices.
// //
// if we have 3 snapshots and we are between 2nd and 3rd: // if we have 3 snapshots and we are between 2nd and 3rd:
// from = 1, to = 2 // from = 1, to = 2
// then we need to remove the first one, which is exactly 'from'. // then we need to remove the first one, which is exactly 'from'.
// because 'from-1' = 0 would remove none. // because 'from-1' = 0 would remove none.
buffer.RemoveRange(from); buffer.RemoveRange(from);
} }
// update time, sample, clear old. // update time, sample, clear old.
// call this every update. // call this every update.
// //
// ONLY CALL IF SNAPSHOTS.COUNT > 0! // ONLY CALL IF SNAPSHOTS.COUNT > 0!
// //
// returns true if there is anything to apply (requires at least 1 snap) // returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'. // from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient) // this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually. // and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway. // besides, passing "Func Interpolate" would allocate anyway.
public static void Step<T>( public static void Step<T>(
SortedList<double, T> buffer, // snapshot buffer SortedList<double, T> buffer, // snapshot buffer
double deltaTime, // engine delta time (unscaled) double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time ref double localTimeline, // local interpolation time based on server time
double localTimescale, // catchup / slowdown is applied to time every update double localTimescale, // catchup / slowdown is applied to time every update
out T fromSnapshot, // we interpolate 'from' this snapshot out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1] out double t) // at ratio 't' [0,1]
where T : Snapshot where T : Snapshot
{ {
StepTime(deltaTime, ref localTimeline, localTimescale); StepTime(deltaTime, ref localTimeline, localTimescale);
StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t); StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t);
} }
} }
} }

View File

@ -4,206 +4,206 @@
namespace Mirror.Examples.SnapshotInterpolationDemo namespace Mirror.Examples.SnapshotInterpolationDemo
{ {
public class ClientCube : MonoBehaviour public class ClientCube : MonoBehaviour
{ {
[Header("Components")] [Header("Components")]
public ServerCube server; public ServerCube server;
public Renderer render; public Renderer render;
[Header("Toggle")] [Header("Toggle")]
public bool interpolate = true; public bool interpolate = true;
// decrease bufferTime at runtime to see the catchup effect. // decrease bufferTime at runtime to see the catchup effect.
// increase to see slowdown. // increase to see slowdown.
// 'double' so we can have very precise dynamic adjustment without rounding // 'double' so we can have very precise dynamic adjustment without rounding
[Header("Buffering")] [Header("Buffering")]
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")] [Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
public double bufferTimeMultiplier = 2; public double bufferTimeMultiplier = 2;
public double bufferTime => server.sendInterval * bufferTimeMultiplier; public double bufferTime => server.sendInterval * bufferTimeMultiplier;
// <servertime, snaps> // <servertime, snaps>
public SortedList<double, Snapshot3D> snapshots = new SortedList<double, Snapshot3D>(); public SortedList<double, Snapshot3D> snapshots = new SortedList<double, Snapshot3D>();
// for smooth interpolation, we need to interpolate along server time. // for smooth interpolation, we need to interpolate along server time.
// any other time (arrival on client, client local time, etc.) is not // any other time (arrival on client, client local time, etc.) is not
// going to give smooth results. // going to give smooth results.
double localTimeline; double localTimeline;
// catchup / slowdown adjustments are applied to timescale, // catchup / slowdown adjustments are applied to timescale,
// to be adjusted in every update instead of when receiving messages. // to be adjusted in every update instead of when receiving messages.
double localTimescale = 1; double localTimescale = 1;
[Tooltip("Mirror tries to maintain 2x send interval (= 1 / Send Rate) time behind server/client. If we are way out of sync by a multiple of this buffer, we simply clamp time to within this buffer.")] [Tooltip("Mirror tries to maintain 2x send interval (= 1 / Send Rate) time behind server/client. If we are way out of sync by a multiple of this buffer, we simply clamp time to within this buffer.")]
public float bufferTimeMultiplierForClamping = 2; public float bufferTimeMultiplierForClamping = 2;
// catchup ///////////////////////////////////////////////////////////// // catchup /////////////////////////////////////////////////////////////
// catchup thresholds in 'frames'. // catchup thresholds in 'frames'.
// half a frame might be too aggressive. // half a frame might be too aggressive.
[Header("Catchup / Slowdown")] [Header("Catchup / Slowdown")]
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")] [Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")] [Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
public float catchupPositiveThreshold = 1; public float catchupPositiveThreshold = 1;
[Tooltip("Local timeline acceleration in % while catching up.")] [Tooltip("Local timeline acceleration in % while catching up.")]
[Range(0, 1)] [Range(0, 1)]
public double catchupSpeed = 0.01f; // 1% public double catchupSpeed = 0.01f; // 1%
[Tooltip("Local timeline slowdown in % while slowing down.")] [Tooltip("Local timeline slowdown in % while slowing down.")]
[Range(0, 1)] [Range(0, 1)]
public double slowdownSpeed = 0.01f; // 1% public double slowdownSpeed = 0.01f; // 1%
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
// we use EMA to average the last second worth of snapshot time diffs. // 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 // 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 // would be the same, but a moving average is faster because we only
// ever add one value. // ever add one value.
ExponentialMovingAverage driftEma; ExponentialMovingAverage driftEma;
// dynamic buffer time adjustment ////////////////////////////////////// // dynamic buffer time adjustment //////////////////////////////////////
// dynamically adjusts bufferTimeMultiplier for smooth results. // dynamically adjusts bufferTimeMultiplier for smooth results.
// to understand how this works, try this manually: // to understand how this works, try this manually:
// //
// - disable dynamic adjustment // - disable dynamic adjustment
// - set jitter = 0.2 (20% is a lot!) // - set jitter = 0.2 (20% is a lot!)
// - notice some stuttering // - notice some stuttering
// - disable interpolation to see just how much jitter this really is(!) // - disable interpolation to see just how much jitter this really is(!)
// - enable interpolation again // - enable interpolation again
// - manually increase bufferTimeMultiplier to 3-4 // - manually increase bufferTimeMultiplier to 3-4
// ... the cube slows down (blue) until it's smooth // ... the cube slows down (blue) until it's smooth
// - with dynamic adjustment enabled, it will set 4 automatically // - with dynamic adjustment enabled, it will set 4 automatically
// ... the cube slows down (blue) until it's smooth as well // ... the cube slows down (blue) until it's smooth as well
// //
// note that 20% jitter is extreme. // note that 20% jitter is extreme.
// for this to be perfectly smooth, set the safety tolerance to '2'. // for this to be perfectly smooth, set the safety tolerance to '2'.
// but realistically this is not necessary, and '1' is enough. // but realistically this is not necessary, and '1' is enough.
[Header("Dynamic Adjustment")] [Header("Dynamic Adjustment")]
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
public bool dynamicAdjustment = true; public bool dynamicAdjustment = true;
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments) public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
// debugging /////////////////////////////////////////////////////////// // debugging ///////////////////////////////////////////////////////////
[Header("Debug")] [Header("Debug")]
public Color catchupColor = Color.green; // green traffic light = go fast public Color catchupColor = Color.green; // green traffic light = go fast
public Color slowdownColor = Color.red; // red traffic light = go slow public Color slowdownColor = Color.red; // red traffic light = go slow
Color defaultColor; Color defaultColor;
void Awake() void Awake()
{ {
defaultColor = render.sharedMaterial.color; defaultColor = render.sharedMaterial.color;
// initialize EMA with 'emaDuration' seconds worth of history. // initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values. // 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds. // multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(server.sendRate * driftEmaDuration); driftEma = new ExponentialMovingAverage(server.sendRate * driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration); deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration);
} }
// add snapshot & initialize client interpolation time if needed // add snapshot & initialize client interpolation time if needed
public void OnMessage(Snapshot3D snap) public void OnMessage(Snapshot3D snap)
{ {
// set local timestamp (= when it was received on our end) // set local timestamp (= when it was received on our end)
#if !UNITY_2020_3_OR_NEWER #if !UNITY_2020_3_OR_NEWER
snap.localTime = NetworkTime.localTime; snap.localTime = NetworkTime.localTime;
#else #else
snap.localTime = Time.timeAsDouble; snap.localTime = Time.timeAsDouble;
#endif #endif
// (optional) dynamic adjustment // (optional) dynamic adjustment
if (dynamicAdjustment) if (dynamicAdjustment)
{ {
// set bufferTime on the fly. // set bufferTime on the fly.
// shows in inspector for easier debugging :) // shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
server.sendInterval, server.sendInterval,
deliveryTimeEma.StandardDeviation, deliveryTimeEma.StandardDeviation,
dynamicAdjustmentTolerance dynamicAdjustmentTolerance
); );
} }
// insert into the buffer & initialize / adjust / catchup // insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust( SnapshotInterpolation.InsertAndAdjust(
snapshots, snapshots,
snap, snap,
ref localTimeline, ref localTimeline,
ref localTimescale, ref localTimescale,
server.sendInterval, server.sendInterval,
bufferTime, bufferTime,
bufferTimeMultiplierForClamping, bufferTimeMultiplierForClamping,
catchupSpeed, catchupSpeed,
slowdownSpeed, slowdownSpeed,
ref driftEma, ref driftEma,
catchupNegativeThreshold, catchupNegativeThreshold,
catchupPositiveThreshold, catchupPositiveThreshold,
ref deliveryTimeEma); ref deliveryTimeEma);
} }
void Update() void Update()
{ {
// only while we have snapshots. // only while we have snapshots.
// timeline starts when the first snapshot arrives. // timeline starts when the first snapshot arrives.
if (snapshots.Count > 0) if (snapshots.Count > 0)
{ {
// snapshot interpolation // snapshot interpolation
if (interpolate) if (interpolate)
{ {
// step // step
SnapshotInterpolation.Step( SnapshotInterpolation.Step(
snapshots, snapshots,
Time.unscaledDeltaTime, Time.unscaledDeltaTime,
ref localTimeline, ref localTimeline,
localTimescale, localTimescale,
out Snapshot3D fromSnapshot, out Snapshot3D fromSnapshot,
out Snapshot3D toSnapshot, out Snapshot3D toSnapshot,
out double t); out double t);
// interpolate & apply // interpolate & apply
Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t); Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t);
transform.position = computed.position; transform.position = computed.position;
} }
// apply raw // apply raw
else else
{ {
Snapshot3D snap = snapshots.Values[0]; Snapshot3D snap = snapshots.Values[0];
transform.position = snap.position; transform.position = snap.position;
snapshots.RemoveAt(0); snapshots.RemoveAt(0);
} }
} }
// color material while catching up / slowing down // color material while catching up / slowing down
if (localTimescale < 1) if (localTimescale < 1)
render.material.color = slowdownColor; render.material.color = slowdownColor;
else if (localTimescale > 1) else if (localTimescale > 1)
render.material.color = catchupColor; render.material.color = catchupColor;
else else
render.material.color = defaultColor; render.material.color = defaultColor;
} }
void OnGUI() void OnGUI()
{ {
// display buffer size as number for easier debugging. // display buffer size as number for easier debugging.
// catchup is displayed as color state in Update() already. // catchup is displayed as color state in Update() already.
const int width = 30; // fit 3 digits const int width = 30; // fit 3 digits
const int height = 20; const int height = 20;
Vector2 screen = Camera.main.WorldToScreenPoint(transform.position); Vector2 screen = Camera.main.WorldToScreenPoint(transform.position);
string str = $"{snapshots.Count}"; string str = $"{snapshots.Count}";
GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str); GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str);
} }
void OnValidate() void OnValidate()
{ {
// thresholds need to be <0 and >0 // thresholds need to be <0 and >0
catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0); catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0);
catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0); catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0);
} }
} }
} }

View File

@ -7,7 +7,7 @@ namespace Mirror.Tests
struct SimpleSnapshot : Snapshot struct SimpleSnapshot : Snapshot
{ {
public double remoteTime { get; set; } public double remoteTime { get; set; }
public double localTime { get; set; } public double localTime { get; set; }
public double value; public double value;
public SimpleSnapshot(double remoteTime, double localTime, double value) public SimpleSnapshot(double remoteTime, double localTime, double value)
@ -32,10 +32,10 @@ public class SnapshotInterpolationTests
SortedList<double, SimpleSnapshot> buffer; SortedList<double, SimpleSnapshot> buffer;
// some defaults // some defaults
const double catchupSpeed = 0.02; const double catchupSpeed = 0.02;
const double slowdownSpeed = 0.04; const double slowdownSpeed = 0.04;
const double negativeThresh = -0.10; const double negativeThresh = -0.10;
const double positiveThresh = 0.10; const double positiveThresh = 0.10;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@ -141,11 +141,11 @@ public void InsertIfNotExists()
public void InsertTwice() public void InsertTwice()
{ {
// defaults // defaults
ExponentialMovingAverage driftEma = default; ExponentialMovingAverage driftEma = default;
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
SimpleSnapshot snap = default; SimpleSnapshot snap = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// insert twice // insert twice
@ -160,10 +160,10 @@ public void InsertTwice()
public void Insert_Sorts() public void Insert_Sorts()
{ {
// defaults // defaults
ExponentialMovingAverage driftEma = default; ExponentialMovingAverage driftEma = default;
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps
@ -184,10 +184,10 @@ public void Insert_Sorts()
public void Insert_InitializesLocalTimeline() public void Insert_InitializesLocalTimeline()
{ {
// defaults // defaults
ExponentialMovingAverage driftEma = default; ExponentialMovingAverage driftEma = default;
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps
@ -207,10 +207,10 @@ public void Insert_InitializesLocalTimeline()
public void Insert_ComputesAverageDrift() public void Insert_ComputesAverageDrift()
{ {
// defaults: drift ema with 3 values // defaults: drift ema with 3 values
ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3); ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3);
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps
@ -233,10 +233,10 @@ public void Insert_ComputesAverageDrift()
public void Insert_ComputesAverageDrift_Scrambled() public void Insert_ComputesAverageDrift_Scrambled()
{ {
// defaults: drift ema with 3 values // defaults: drift ema with 3 values
ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3); ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3);
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps
@ -264,10 +264,10 @@ public void Insert_ComputesAverageDeliveryInterval()
// defaults: delivery ema with 2 values // defaults: delivery ema with 2 values
// because delivery time ema is always between 2 snaps. // because delivery time ema is always between 2 snaps.
// so for 3 values, it's only computed twice. // so for 3 values, it's only computed twice.
ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2); ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2);
ExponentialMovingAverage deliveryIntervalEma = new ExponentialMovingAverage(2); ExponentialMovingAverage deliveryIntervalEma = new ExponentialMovingAverage(2);
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps with local arrival times // example snaps with local arrival times
@ -295,10 +295,10 @@ public void Insert_ComputesAverageDeliveryInterval_Scrambled()
// defaults: delivery ema with 2 values // defaults: delivery ema with 2 values
// because delivery time ema is always between 2 snaps. // because delivery time ema is always between 2 snaps.
// so for 3 values, it's only computed twice. // so for 3 values, it's only computed twice.
ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2); ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2);
ExponentialMovingAverage deliveryIntervalEma = new ExponentialMovingAverage(2); ExponentialMovingAverage deliveryIntervalEma = new ExponentialMovingAverage(2);
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps with local arrival times // example snaps with local arrival times
@ -324,10 +324,10 @@ public void Insert_ComputesAverageDeliveryInterval_Scrambled()
public void Sample() public void Sample()
{ {
// defaults // defaults
ExponentialMovingAverage driftEma = default; ExponentialMovingAverage driftEma = default;
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps
@ -361,10 +361,10 @@ public void Sample()
public void Step() public void Step()
{ {
// defaults // defaults
ExponentialMovingAverage driftEma = default; ExponentialMovingAverage driftEma = default;
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps
@ -386,10 +386,10 @@ public void Step()
public void Step_RemovesOld() public void Step_RemovesOld()
{ {
// defaults // defaults
ExponentialMovingAverage driftEma = default; ExponentialMovingAverage driftEma = default;
ExponentialMovingAverage deliveryIntervalEma = default; ExponentialMovingAverage deliveryIntervalEma = default;
double localTimeline = 0; double localTimeline = 0;
double localTimescale = 0; double localTimescale = 0;
// example snaps // example snaps