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

View File

@ -5,253 +5,253 @@
namespace Mirror
{
public class NetworkConnectionToClient : NetworkConnection
{
// rpcs are collected in a buffer, and then flushed out together.
// this way we don't need one NetworkMessage per rpc.
// => prepares for LocalWorldState as well.
// ensure max size when adding!
readonly NetworkWriter reliableRpcs = new NetworkWriter();
readonly NetworkWriter unreliableRpcs = new NetworkWriter();
public class NetworkConnectionToClient : NetworkConnection
{
// rpcs are collected in a buffer, and then flushed out together.
// this way we don't need one NetworkMessage per rpc.
// => prepares for LocalWorldState as well.
// ensure max size when adding!
readonly NetworkWriter reliableRpcs = new NetworkWriter();
readonly NetworkWriter unreliableRpcs = new NetworkWriter();
public override string address =>
Transport.active.ServerGetClientAddress(connectionId);
public override string address =>
Transport.active.ServerGetClientAddress(connectionId);
/// <summary>NetworkIdentities that this connection can see</summary>
// TODO move to server's NetworkConnectionToClient?
public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();
/// <summary>NetworkIdentities that this connection can see</summary>
// TODO move to server's NetworkConnectionToClient?
public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();
// Deprecated 2022-10-13
[Obsolete(".clientOwnedObjects was renamed to .owned :)")]
public HashSet<NetworkIdentity> clientOwnedObjects => owned;
// Deprecated 2022-10-13
[Obsolete(".clientOwnedObjects was renamed to .owned :)")]
public HashSet<NetworkIdentity> clientOwnedObjects => owned;
// unbatcher
public Unbatcher unbatcher = new Unbatcher();
// unbatcher
public Unbatcher unbatcher = new Unbatcher();
// 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
// server for host mode.
// TODO move them along server's timeline in the future.
// perhaps with an offset.
// for now, keep compatibility by manually constructing a timeline.
ExponentialMovingAverage driftEma;
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
public double remoteTimeline;
public double remoteTimescale;
double bufferTimeMultiplier = 2;
double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
// 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
// server for host mode.
// TODO move them along server's timeline in the future.
// perhaps with an offset.
// for now, keep compatibility by manually constructing a timeline.
ExponentialMovingAverage driftEma;
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
public double remoteTimeline;
public double remoteTimescale;
double bufferTimeMultiplier = 2;
double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
// <clienttime, snaps>
readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
// <clienttime, snaps>
readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
// Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
public int snapshotBufferSizeLimit = 64;
// Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
public int snapshotBufferSizeLimit = 64;
public NetworkConnectionToClient(int networkConnectionId)
: base(networkConnectionId)
{
// initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration);
public NetworkConnectionToClient(int networkConnectionId)
: base(networkConnectionId)
{
// initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration);
// buffer limit should be at least multiplier to have enough in there
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit);
}
// buffer limit should be at least multiplier to have enough in there
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit);
}
public void OnTimeSnapshot(TimeSnapshot snapshot)
{
// protect against ever growing buffer size attacks
if (snapshots.Count >= snapshotBufferSizeLimit) return;
public void OnTimeSnapshot(TimeSnapshot snapshot)
{
// protect against ever growing buffer size attacks
if (snapshots.Count >= snapshotBufferSizeLimit) return;
// (optional) dynamic adjustment
if (NetworkClient.dynamicAdjustment)
{
// set bufferTime on the fly.
// shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
NetworkServer.sendInterval,
deliveryTimeEma.StandardDeviation,
NetworkClient.dynamicAdjustmentTolerance
);
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
}
// (optional) dynamic adjustment
if (NetworkClient.dynamicAdjustment)
{
// set bufferTime on the fly.
// shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
NetworkServer.sendInterval,
deliveryTimeEma.StandardDeviation,
NetworkClient.dynamicAdjustmentTolerance
);
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
}
// insert into the server buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snapshot,
ref remoteTimeline,
ref remoteTimescale,
NetworkServer.sendInterval,
bufferTime,
NetworkServer.bufferTimeMultiplierForClamping,
NetworkClient.catchupSpeed,
NetworkClient.slowdownSpeed,
ref driftEma,
NetworkClient.catchupNegativeThreshold,
NetworkClient.catchupPositiveThreshold,
ref deliveryTimeEma
);
}
// insert into the server buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snapshot,
ref remoteTimeline,
ref remoteTimescale,
NetworkServer.sendInterval,
bufferTime,
NetworkServer.bufferTimeMultiplierForClamping,
NetworkClient.catchupSpeed,
NetworkClient.slowdownSpeed,
ref driftEma,
NetworkClient.catchupNegativeThreshold,
NetworkClient.catchupPositiveThreshold,
ref deliveryTimeEma
);
}
public void UpdateTimeInterpolation()
{
// timeline starts when the first snapshot arrives.
if (snapshots.Count > 0)
{
// progress local timeline.
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale);
public void UpdateTimeInterpolation()
{
// timeline starts when the first snapshot arrives.
if (snapshots.Count > 0)
{
// progress local timeline.
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale);
// progress local interpolation.
// TimeSnapshot doesn't interpolate anything.
// this is merely to keep removing older snapshots.
SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _);
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
}
}
// progress local interpolation.
// TimeSnapshot doesn't interpolate anything.
// this is merely to keep removing older snapshots.
SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _);
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
}
}
// Send stage three: hand off to transport
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
Transport.active.ServerSend(connectionId, segment, channelId);
// Send stage three: hand off to transport
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
Transport.active.ServerSend(connectionId, segment, channelId);
void FlushRpcs(NetworkWriter buffer, int channelId)
{
if (buffer.Position > 0)
{
Send(new RpcBufferMessage{ payload = buffer }, channelId);
buffer.Position = 0;
}
}
void FlushRpcs(NetworkWriter buffer, int channelId)
{
if (buffer.Position > 0)
{
Send(new RpcBufferMessage { payload = buffer }, channelId);
buffer.Position = 0;
}
}
// helper for both channels
void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize)
{
// calculate buffer limit. we can only fit so much into a message.
// max - message header - WriteArraySegment size header - batch header
int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize;
// helper for both channels
void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize)
{
// calculate buffer limit. we can only fit so much into a message.
// max - message header - WriteArraySegment size header - batch header
int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize;
// remember previous valid position
int before = buffer.Position;
// remember previous valid position
int before = buffer.Position;
// serialize the message without header
buffer.Write(message);
// serialize the message without header
buffer.Write(message);
// before we potentially flush out old messages,
// let's ensure this single message can even fit the limit.
// otherwise no point in flushing.
int messageSize = buffer.Position - before;
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}");
return;
}
// before we potentially flush out old messages,
// let's ensure this single message can even fit the limit.
// otherwise no point in flushing.
int messageSize = buffer.Position - before;
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}");
return;
}
// too much to fit into max message size?
// then flush first, then write it again.
// (message + message header + 4 bytes WriteArraySegment header)
if (buffer.Position > bufferLimit)
{
buffer.Position = before;
FlushRpcs(buffer, channelId); // this resets position
buffer.Write(message);
}
}
// too much to fit into max message size?
// then flush first, then write it again.
// (message + message header + 4 bytes WriteArraySegment header)
if (buffer.Position > bufferLimit)
{
buffer.Position = before;
FlushRpcs(buffer, channelId); // this resets position
buffer.Write(message);
}
}
internal void BufferRpc(RpcMessage message, int channelId)
{
int maxMessageSize = Transport.active.GetMaxPacketSize(channelId);
if (channelId == Channels.Reliable)
{
BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize);
}
else if (channelId == Channels.Unreliable)
{
BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize);
}
}
internal void BufferRpc(RpcMessage message, int channelId)
{
int maxMessageSize = Transport.active.GetMaxPacketSize(channelId);
if (channelId == Channels.Reliable)
{
BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize);
}
else if (channelId == Channels.Unreliable)
{
BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize);
}
}
internal override void Update()
{
// send rpc buffers
FlushRpcs(reliableRpcs, Channels.Reliable);
FlushRpcs(unreliableRpcs, Channels.Unreliable);
internal override void Update()
{
// send rpc buffers
FlushRpcs(reliableRpcs, Channels.Reliable);
FlushRpcs(unreliableRpcs, Channels.Unreliable);
// call base update to flush out batched messages
base.Update();
}
// call base update to flush out batched messages
base.Update();
}
/// <summary>Disconnects this connection.</summary>
public override void Disconnect()
{
// set not ready and handle clientscene disconnect in any case
// (might be client or host mode here)
isReady = false;
reliableRpcs.Position = 0;
unreliableRpcs.Position = 0;
Transport.active.ServerDisconnect(connectionId);
/// <summary>Disconnects this connection.</summary>
public override void Disconnect()
{
// set not ready and handle clientscene disconnect in any case
// (might be client or host mode here)
isReady = false;
reliableRpcs.Position = 0;
unreliableRpcs.Position = 0;
Transport.active.ServerDisconnect(connectionId);
// IMPORTANT: NetworkConnection.Disconnect() is NOT called for
// voluntary disconnects from the other end.
// -> so all 'on disconnect' cleanup code needs to be in
// OnTransportDisconnect, where it's called for both voluntary
// and involuntary disconnects!
}
// IMPORTANT: NetworkConnection.Disconnect() is NOT called for
// voluntary disconnects from the other end.
// -> so all 'on disconnect' cleanup code needs to be in
// OnTransportDisconnect, where it's called for both voluntary
// and involuntary disconnects!
}
internal void AddToObserving(NetworkIdentity netIdentity)
{
observing.Add(netIdentity);
internal void AddToObserving(NetworkIdentity netIdentity)
{
observing.Add(netIdentity);
// spawn identity for this conn
NetworkServer.ShowForConnection(netIdentity, this);
}
// spawn identity for this conn
NetworkServer.ShowForConnection(netIdentity, this);
}
internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed)
{
observing.Remove(netIdentity);
internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed)
{
observing.Remove(netIdentity);
if (!isDestroyed)
{
// hide identity for this conn
NetworkServer.HideForConnection(netIdentity, this);
}
}
if (!isDestroyed)
{
// hide identity for this conn
NetworkServer.HideForConnection(netIdentity, this);
}
}
internal void RemoveFromObservingsObservers()
{
foreach (NetworkIdentity netIdentity in observing)
{
netIdentity.RemoveObserver(this);
}
observing.Clear();
}
internal void RemoveFromObservingsObservers()
{
foreach (NetworkIdentity netIdentity in observing)
{
netIdentity.RemoveObserver(this);
}
observing.Clear();
}
internal void AddOwnedObject(NetworkIdentity obj)
{
owned.Add(obj);
}
internal void AddOwnedObject(NetworkIdentity obj)
{
owned.Add(obj);
}
internal void RemoveOwnedObject(NetworkIdentity obj)
{
owned.Remove(obj);
}
internal void RemoveOwnedObject(NetworkIdentity obj)
{
owned.Remove(obj);
}
internal void DestroyOwnedObjects()
{
// create a copy because the list might be modified when destroying
HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned);
foreach (NetworkIdentity netIdentity in tmp)
{
if (netIdentity != null)
{
NetworkServer.Destroy(netIdentity.gameObject);
}
}
internal void DestroyOwnedObjects()
{
// create a copy because the list might be modified when destroying
HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned);
foreach (NetworkIdentity netIdentity in tmp)
{
if (netIdentity != null)
{
NetworkServer.Destroy(netIdentity.gameObject);
}
}
// clear the hashset because we destroyed them all
owned.Clear();
}
}
// clear the hashset because we destroyed them all
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
{
public static class SortedListExtensions
{
// removes the first 'amount' elements from the sorted list
public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount)
{
// remove the first element 'amount' times.
// handles -1 and > count safely.
for (int i = 0; i < amount && i < list.Count; ++i)
list.RemoveAt(0);
}
}
public static class SortedListExtensions
{
// removes the first 'amount' elements from the sorted list
public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount)
{
// remove the first element 'amount' times.
// handles -1 and > count safely.
for (int i = 0; i < amount && i < list.Count; ++i)
list.RemoveAt(0);
}
}
public static class SnapshotInterpolation
{
// calculate timescale for catch-up / slow-down
// note that negative threshold should be <0.
// caller should verify (i.e. Unity OnValidate).
// improves branch prediction.
public static double Timescale(
double drift, // how far we are off from bufferTime
double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1]
double catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
double catchupPositiveThreshold) // in % of sendInterval)
{
// if the drift time is too large, it means we are behind more time.
// so we need to speed up the timescale.
// note the threshold should be sendInterval * catchupThreshold.
if (drift > catchupPositiveThreshold)
{
// localTimeline += 0.001; // too simple, this would ping pong
return 1 + catchupSpeed; // n% faster
}
public static class SnapshotInterpolation
{
// calculate timescale for catch-up / slow-down
// note that negative threshold should be <0.
// caller should verify (i.e. Unity OnValidate).
// improves branch prediction.
public static double Timescale(
double drift, // how far we are off from bufferTime
double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1]
double catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
double catchupPositiveThreshold) // in % of sendInterval)
{
// if the drift time is too large, it means we are behind more time.
// so we need to speed up the timescale.
// note the threshold should be sendInterval * catchupThreshold.
if (drift > catchupPositiveThreshold)
{
// localTimeline += 0.001; // too simple, this would ping pong
return 1 + catchupSpeed; // n% faster
}
// if the drift time is too small, it means we are ahead of time.
// so we need to slow down the timescale.
// note the threshold should be sendInterval * catchupThreshold.
if (drift < catchupNegativeThreshold)
{
// localTimeline -= 0.001; // too simple, this would ping pong
return 1 - slowdownSpeed; // n% slower
}
// if the drift time is too small, it means we are ahead of time.
// so we need to slow down the timescale.
// note the threshold should be sendInterval * catchupThreshold.
if (drift < catchupNegativeThreshold)
{
// localTimeline -= 0.001; // too simple, this would ping pong
return 1 - slowdownSpeed; // n% slower
}
// keep constant timescale while within threshold.
// this way we have perfectly smooth speed most of the time.
return 1;
}
// keep constant timescale while within threshold.
// this way we have perfectly smooth speed most of the time.
return 1;
}
// calculate dynamic buffer time adjustment
public static double DynamicAdjustment(
double sendInterval,
double jitterStandardDeviation,
double dynamicAdjustmentTolerance)
{
// jitter is equal to delivery time standard variation.
// delivery time is made up of 'sendInterval+jitter'.
// .Average would be dampened by the constant sendInterval
// .StandardDeviation is the changes in 'jitter' that we want
// so add it to send interval again.
double intervalWithJitter = sendInterval + jitterStandardDeviation;
// calculate dynamic buffer time adjustment
public static double DynamicAdjustment(
double sendInterval,
double jitterStandardDeviation,
double dynamicAdjustmentTolerance)
{
// jitter is equal to delivery time standard variation.
// delivery time is made up of 'sendInterval+jitter'.
// .Average would be dampened by the constant sendInterval
// .StandardDeviation is the changes in 'jitter' that we want
// so add it to send interval again.
double intervalWithJitter = sendInterval + jitterStandardDeviation;
// how many multiples of sendInterval is that?
// we want to convert to bufferTimeMultiplier later.
double multiples = intervalWithJitter / sendInterval;
// how many multiples of sendInterval is that?
// we want to convert to bufferTimeMultiplier later.
double multiples = intervalWithJitter / sendInterval;
// add the tolerance
double safezone = multiples + dynamicAdjustmentTolerance;
// Console.WriteLine($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
return safezone;
}
// add the tolerance
double safezone = multiples + dynamicAdjustmentTolerance;
// Console.WriteLine($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
return safezone;
}
// helper function to insert a snapshot if it doesn't exist yet.
// extra function so we can use it for both cases:
// NetworkClient global timeline insertions & adjustments via Insert<T>.
// NetworkBehaviour local insertion without any time adjustments.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool InsertIfNotExists<T>(
SortedList<double, T> buffer, // snapshot buffer
T snapshot) // the newly received snapshot
where T : Snapshot
{
// SortedList does not allow duplicates.
// we don't need to check ContainsKey (which is expensive).
// simply add and compare count before/after for the return value.
// helper function to insert a snapshot if it doesn't exist yet.
// extra function so we can use it for both cases:
// NetworkClient global timeline insertions & adjustments via Insert<T>.
// NetworkBehaviour local insertion without any time adjustments.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool InsertIfNotExists<T>(
SortedList<double, T> buffer, // snapshot buffer
T snapshot) // the newly received snapshot
where T : Snapshot
{
// SortedList does not allow duplicates.
// we don't need to check ContainsKey (which is expensive).
// simply add and compare count before/after for the return value.
//if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive
// buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists
//if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive
// buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists
int before = buffer.Count;
buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists
return buffer.Count > before;
}
int before = buffer.Count;
buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists
return buffer.Count > before;
}
// call this for every received snapshot.
// adds / inserts it to the list & initializes local time if needed.
public static void InsertAndAdjust<T>(
SortedList<double, T> buffer, // snapshot buffer
T snapshot, // the newly received snapshot
ref double localTimeline, // local interpolation time based on server time
ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
float sendInterval, // for debugging
double bufferTime, // offset for buffering
float clampMultiplier, // multiplier to check if time needs to be clamped
double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1]
ref ExponentialMovingAverage driftEma, // for catchup / slowdown
float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
float catchupPositiveThreshold, // in % of sendInterval
ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
where T : Snapshot
{
// first snapshot?
// initialize local timeline.
// we want it to be behind by 'offset'.
//
// note that the first snapshot may be a lagging packet.
// so we would always be behind by that lag.
// this requires catchup later.
if (buffer.Count == 0)
localTimeline = snapshot.remoteTime - bufferTime;
// call this for every received snapshot.
// adds / inserts it to the list & initializes local time if needed.
public static void InsertAndAdjust<T>(
SortedList<double, T> buffer, // snapshot buffer
T snapshot, // the newly received snapshot
ref double localTimeline, // local interpolation time based on server time
ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
float sendInterval, // for debugging
double bufferTime, // offset for buffering
float clampMultiplier, // multiplier to check if time needs to be clamped
double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1]
ref ExponentialMovingAverage driftEma, // for catchup / slowdown
float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
float catchupPositiveThreshold, // in % of sendInterval
ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
where T : Snapshot
{
// first snapshot?
// initialize local timeline.
// we want it to be behind by 'offset'.
//
// note that the first snapshot may be a lagging packet.
// so we would always be behind by that lag.
// this requires catchup later.
if (buffer.Count == 0)
localTimeline = snapshot.remoteTime - bufferTime;
// insert into the buffer.
//
// note that we might insert it between our current interpolation
// which is fine, it adds another data point for accuracy.
//
// note that insert may be called twice for the same key.
// by default, this would throw.
// need to handle it silently.
if (InsertIfNotExists(buffer, snapshot))
{
// dynamic buffer adjustment needs delivery interval jitter
if (buffer.Count >= 2)
{
// note that this is not entirely accurate for scrambled inserts.
//
// we always use the last two, not 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:
// => assume a buffer of AC, with delivery time C-A
// => we then insert B, with delivery time B-A
// => but then technically the first C-A wasn't correct,
// as it would have to be C-B
//
// in practice, scramble is rare and won't make much difference
double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
// insert into the buffer.
//
// note that we might insert it between our current interpolation
// which is fine, it adds another data point for accuracy.
//
// note that insert may be called twice for the same key.
// by default, this would throw.
// need to handle it silently.
if (InsertIfNotExists(buffer, snapshot))
{
// dynamic buffer adjustment needs delivery interval jitter
if (buffer.Count >= 2)
{
// note that this is not entirely accurate for scrambled inserts.
//
// we always use the last two, not 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:
// => assume a buffer of AC, with delivery time C-A
// => we then insert B, with delivery time B-A
// => but then technically the first C-A wasn't correct,
// as it would have to be C-B
//
// in practice, scramble is rare and won't make much difference
double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
// this is the delivery time since last snapshot
double localDeliveryTime = lastestLocalTime - previousLocalTime;
// this is the delivery time since last snapshot
double localDeliveryTime = lastestLocalTime - previousLocalTime;
// feed the local delivery time to the EMA.
// this is what the original stream did too.
// our final dynamic buffer adjustment is different though.
// we use standard deviation instead of average.
deliveryTimeEma.Add(localDeliveryTime);
}
// feed the local delivery time to the EMA.
// this is what the original stream did too.
// our final dynamic buffer adjustment is different though.
// we use standard deviation instead of average.
deliveryTimeEma.Add(localDeliveryTime);
}
// adjust timescale to catch up / slow down after each insertion
// because that is when we add new values to our EMA.
// adjust timescale to catch up / slow down after each insertion
// because that is when we add new values to our EMA.
// we want localTimeline to be about 'bufferTime' behind.
// for that, we need the delivery time EMA.
// snapshots may arrive out of order, we can not use last-timeline.
// we need to use the inserted snapshot's time - timeline.
double latestRemoteTime = snapshot.remoteTime;
TimeLineOverride(latestRemoteTime, bufferTime, clampMultiplier, ref localTimeline);
double timeDiff = latestRemoteTime - localTimeline;
if (buffer.Count > 1)
// next, calculate average of a few seconds worth of timediffs.
// this gives smoother results.
//
// to calculate the average, we could simply loop through the
// last 'n' seconds worth of timediffs, but:
// - our buffer may only store a few snapshots (bufferTime)
// - looping through seconds worth of snapshots every time is
// expensive
//
// to solve this, we use an exponential moving average.
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
// which is basically fancy math to do the same but faster.
// additionally, it allows us to look at more timeDiff values
// than we sould have access to in our buffer :)
driftEma.Add(timeDiff);
// we want localTimeline to be about 'bufferTime' behind.
// for that, we need the delivery time EMA.
// snapshots may arrive out of order, we can not use last-timeline.
// we need to use the inserted snapshot's time - timeline.
double latestRemoteTime = snapshot.remoteTime;
// next up, calculate how far we are currently away from bufferTime
double drift = driftEma.Value - bufferTime;
TimeLineOverride(latestRemoteTime, bufferTime, clampMultiplier, ref localTimeline);
// convert relative thresholds to absolute values based on sendInterval
double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
double timeDiff = latestRemoteTime - localTimeline;
if (buffer.Count > 1)
// next, calculate average of a few seconds worth of timediffs.
// this gives smoother results.
//
// to calculate the average, we could simply loop through the
// last 'n' seconds worth of timediffs, but:
// - our buffer may only store a few snapshots (bufferTime)
// - looping through seconds worth of snapshots every time is
// expensive
//
// to solve this, we use an exponential moving average.
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
// which is basically fancy math to do the same but faster.
// additionally, it allows us to look at more timeDiff values
// than we sould have access to in our buffer :)
driftEma.Add(timeDiff);
// next, set localTimescale to catchup consistently in Update().
// we quantize between default/catchup/slowdown,
// this way we have 'default' speed most of the time(!).
// and only catch up / slow down for a little bit occasionally.
// a consistent multiplier would never be exactly 1.0.
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
// next up, calculate how far we are currently away from bufferTime
double drift = driftEma.Value - bufferTime;
// debug logging
// 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
// targetTime +- x times of buffer.
private static void TimeLineOverride(double latestRemoteTime, double bufferTime, float clampMultiplier, ref double localTimeline)
{
// If we want local timeline to be around bufferTime slower,
// Then over her we want to clamp localTimeline to be:
// target +- multiplierCheck * bufferTime.
double targetTime = latestRemoteTime - bufferTime;
localTimeline = Math.Clamp(localTimeline, targetTime - clampMultiplier * bufferTime, targetTime + clampMultiplier * bufferTime);
}
// convert relative thresholds to absolute values based on sendInterval
double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
// sample snapshot buffer to find the pair around the given time.
// 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 only call this is we have > 0 snapshots.
public static void Sample<T>(
SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time
out int from, // the snapshot <= time
out int to, // the snapshot >= time
out double t) // interpolation factor
where T : Snapshot
{
from = -1;
to = -1;
t = 0;
// next, set localTimescale to catchup consistently in Update().
// we quantize between default/catchup/slowdown,
// this way we have 'default' speed most of the time(!).
// and only catch up / slow down for a little bit occasionally.
// a consistent multiplier would never be exactly 1.0.
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
// sample from [0,count-1] so we always have two at 'i' and 'i+1'.
for (int i = 0; i < buffer.Count - 1; ++i)
{
// is local time between these two?
T first = buffer.Values[i];
T second = buffer.Values[i + 1];
if (localTimeline >= first.remoteTime &&
localTimeline <= second.remoteTime)
{
// use these two snapshots
from = i;
to = i + 1;
t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
return;
}
}
// debug logging
// Console.WriteLine($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");
}
}
// didn't find two snapshots around local time.
// so pick either the first or last, depending on which is closer.
// If the time difference is more than X times of buffer, we will override time to be
// targetTime +- x times of buffer.
private static void TimeLineOverride(double latestRemoteTime, double bufferTime, float clampMultiplier, ref double localTimeline)
{
// If we want local timeline to be around bufferTime slower,
// Then over her we want to clamp localTimeline to be:
// target +- multiplierCheck * bufferTime.
double targetTime = latestRemoteTime - bufferTime;
// oldest snapshot ahead of local time?
if (buffer.Values[0].remoteTime > localTimeline)
{
from = to = 0;
t = 0;
}
// otherwise initialize both to the last one
else
{
from = to = buffer.Count - 1;
t = 0;
}
}
localTimeline = Math.Clamp(localTimeline, targetTime - clampMultiplier * bufferTime, targetTime + clampMultiplier * bufferTime);
}
// progress local timeline every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// decoupled from Step<T> for easier testing and so we can progress
// time only once in NetworkClient, while stepping for each component.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StepTime(
double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time
double localTimescale) // catchup / slowdown is applied to time every update)
{
// move local forward in time, scaled with catchup / slowdown applied
localTimeline += deltaTime * localTimescale;
}
// sample snapshot buffer to find the pair around the given time.
// 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 only call this is we have > 0 snapshots.
public static void Sample<T>(
SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time
out int from, // the snapshot <= time
out int to, // the snapshot >= time
out double t) // interpolation factor
where T : Snapshot
{
from = -1;
to = -1;
t = 0;
// sample, clear old.
// call this every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway.
public static void StepInterpolation<T>(
SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time
out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1]
where T : Snapshot
{
// check this in caller:
// nothing to do if there are no snapshots at all yet
// if (buffer.Count == 0) return false;
// sample from [0,count-1] so we always have two at 'i' and 'i+1'.
for (int i = 0; i < buffer.Count - 1; ++i)
{
// is local time between these two?
T first = buffer.Values[i];
T second = buffer.Values[i + 1];
if (localTimeline >= first.remoteTime &&
localTimeline <= second.remoteTime)
{
// use these two snapshots
from = i;
to = i + 1;
t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
return;
}
}
// sample snapshot buffer at local interpolation time
Sample(buffer, localTimeline, out int from, out int to, out t);
// didn't find two snapshots around local time.
// so pick either the first or last, depending on which is closer.
// save from/to
fromSnapshot = buffer.Values[from];
toSnapshot = buffer.Values[to];
// oldest snapshot ahead of local time?
if (buffer.Values[0].remoteTime > localTimeline)
{
from = to = 0;
t = 0;
}
// otherwise initialize both to the last one
else
{
from = to = buffer.Count - 1;
t = 0;
}
}
// remove older snapshots that we definitely don't need anymore.
// after(!) using the indices.
//
// if we have 3 snapshots and we are between 2nd and 3rd:
// from = 1, to = 2
// then we need to remove the first one, which is exactly 'from'.
// because 'from-1' = 0 would remove none.
buffer.RemoveRange(from);
}
// progress local timeline every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// decoupled from Step<T> for easier testing and so we can progress
// time only once in NetworkClient, while stepping for each component.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StepTime(
double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time
double localTimescale) // catchup / slowdown is applied to time every update)
{
// move local forward in time, scaled with catchup / slowdown applied
localTimeline += deltaTime * localTimescale;
}
// update time, sample, clear old.
// call this every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway.
public static void Step<T>(
SortedList<double, T> buffer, // snapshot buffer
double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time
double localTimescale, // catchup / slowdown is applied to time every update
out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1]
where T : Snapshot
{
StepTime(deltaTime, ref localTimeline, localTimescale);
StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t);
}
}
// sample, clear old.
// call this every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway.
public static void StepInterpolation<T>(
SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time
out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1]
where T : Snapshot
{
// check this in caller:
// nothing to do if there are no snapshots at all yet
// if (buffer.Count == 0) return false;
// sample snapshot buffer at local interpolation time
Sample(buffer, localTimeline, out int from, out int to, out t);
// save from/to
fromSnapshot = buffer.Values[from];
toSnapshot = buffer.Values[to];
// remove older snapshots that we definitely don't need anymore.
// after(!) using the indices.
//
// if we have 3 snapshots and we are between 2nd and 3rd:
// from = 1, to = 2
// then we need to remove the first one, which is exactly 'from'.
// because 'from-1' = 0 would remove none.
buffer.RemoveRange(from);
}
// update time, sample, clear old.
// call this every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway.
public static void Step<T>(
SortedList<double, T> buffer, // snapshot buffer
double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time
double localTimescale, // catchup / slowdown is applied to time every update
out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1]
where T : Snapshot
{
StepTime(deltaTime, ref localTimeline, localTimescale);
StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t);
}
}
}

View File

@ -4,206 +4,206 @@
namespace Mirror.Examples.SnapshotInterpolationDemo
{
public class ClientCube : MonoBehaviour
{
[Header("Components")]
public ServerCube server;
public Renderer render;
public class ClientCube : MonoBehaviour
{
[Header("Components")]
public ServerCube server;
public Renderer render;
[Header("Toggle")]
public bool interpolate = true;
[Header("Toggle")]
public bool interpolate = true;
// decrease bufferTime at runtime to see the catchup effect.
// increase to see slowdown.
// 'double' so we can have very precise dynamic adjustment without rounding
[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.")]
public double bufferTimeMultiplier = 2;
public double bufferTime => server.sendInterval * bufferTimeMultiplier;
// decrease bufferTime at runtime to see the catchup effect.
// increase to see slowdown.
// 'double' so we can have very precise dynamic adjustment without rounding
[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.")]
public double bufferTimeMultiplier = 2;
public double bufferTime => server.sendInterval * bufferTimeMultiplier;
// <servertime, snaps>
public SortedList<double, Snapshot3D> snapshots = new SortedList<double, Snapshot3D>();
// <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.
double localTimeline;
// 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.
double localTimeline;
// catchup / slowdown adjustments are applied to timescale,
// to be adjusted in every update instead of when receiving messages.
double localTimescale = 1;
// catchup / slowdown adjustments are applied to timescale,
// to be adjusted in every update instead of when receiving messages.
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.")]
public float bufferTimeMultiplierForClamping = 2;
// catchup /////////////////////////////////////////////////////////////
// catchup thresholds in 'frames'.
// half a frame might be too aggressive.
[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.")]
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
[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;
[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;
// catchup /////////////////////////////////////////////////////////////
// catchup thresholds in 'frames'.
// half a frame might be too aggressive.
[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.")]
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
[Tooltip("Local timeline acceleration in % while catching up.")]
[Range(0, 1)]
public double catchupSpeed = 0.01f; // 1%
[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;
[Tooltip("Local timeline slowdown in % while slowing down.")]
[Range(0, 1)]
public double slowdownSpeed = 0.01f; // 1%
[Tooltip("Local timeline acceleration in % while catching up.")]
[Range(0, 1)]
public double catchupSpeed = 0.01f; // 1%
[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
[Tooltip("Local timeline slowdown in % while slowing down.")]
[Range(0, 1)]
public double slowdownSpeed = 0.01f; // 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;
[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
// dynamic buffer time adjustment //////////////////////////////////////
// dynamically adjusts bufferTimeMultiplier for smooth results.
// to understand how this works, try this manually:
//
// - disable dynamic adjustment
// - set jitter = 0.2 (20% is a lot!)
// - notice some stuttering
// - disable interpolation to see just how much jitter this really is(!)
// - enable interpolation again
// - manually increase bufferTimeMultiplier to 3-4
// ... the cube slows down (blue) until it's smooth
// - with dynamic adjustment enabled, it will set 4 automatically
// ... the cube slows down (blue) until it's smooth as well
//
// note that 20% jitter is extreme.
// for this to be perfectly smooth, set the safety tolerance to '2'.
// but realistically this is not necessary, and '1' is enough.
[Header("Dynamic Adjustment")]
[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;
// 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;
[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)
// dynamic buffer time adjustment //////////////////////////////////////
// dynamically adjusts bufferTimeMultiplier for smooth results.
// to understand how this works, try this manually:
//
// - disable dynamic adjustment
// - set jitter = 0.2 (20% is a lot!)
// - notice some stuttering
// - disable interpolation to see just how much jitter this really is(!)
// - enable interpolation again
// - manually increase bufferTimeMultiplier to 3-4
// ... the cube slows down (blue) until it's smooth
// - with dynamic adjustment enabled, it will set 4 automatically
// ... the cube slows down (blue) until it's smooth as well
//
// note that 20% jitter is extreme.
// for this to be perfectly smooth, set the safety tolerance to '2'.
// but realistically this is not necessary, and '1' is enough.
[Header("Dynamic Adjustment")]
[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;
[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
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
[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)
// debugging ///////////////////////////////////////////////////////////
[Header("Debug")]
public Color catchupColor = Color.green; // green traffic light = go fast
public Color slowdownColor = Color.red; // red traffic light = go slow
Color defaultColor;
[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
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
void Awake()
{
defaultColor = render.sharedMaterial.color;
// debugging ///////////////////////////////////////////////////////////
[Header("Debug")]
public Color catchupColor = Color.green; // green traffic light = go fast
public Color slowdownColor = Color.red; // red traffic light = go slow
Color defaultColor;
// 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 * driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration);
}
void Awake()
{
defaultColor = render.sharedMaterial.color;
// add snapshot & initialize client interpolation time if needed
public void OnMessage(Snapshot3D snap)
{
// set local timestamp (= when it was received on our end)
// 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 * driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration);
}
// add snapshot & initialize client interpolation time if needed
public void OnMessage(Snapshot3D snap)
{
// set local timestamp (= when it was received on our end)
#if !UNITY_2020_3_OR_NEWER
snap.localTime = NetworkTime.localTime;
#else
snap.localTime = Time.timeAsDouble;
snap.localTime = Time.timeAsDouble;
#endif
// (optional) dynamic adjustment
if (dynamicAdjustment)
{
// set bufferTime on the fly.
// shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
server.sendInterval,
deliveryTimeEma.StandardDeviation,
dynamicAdjustmentTolerance
);
}
// (optional) dynamic adjustment
if (dynamicAdjustment)
{
// set bufferTime on the fly.
// shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
server.sendInterval,
deliveryTimeEma.StandardDeviation,
dynamicAdjustmentTolerance
);
}
// insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snap,
ref localTimeline,
ref localTimescale,
server.sendInterval,
bufferTime,
bufferTimeMultiplierForClamping,
catchupSpeed,
slowdownSpeed,
ref driftEma,
catchupNegativeThreshold,
catchupPositiveThreshold,
ref deliveryTimeEma);
}
// insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snap,
ref localTimeline,
ref localTimescale,
server.sendInterval,
bufferTime,
bufferTimeMultiplierForClamping,
catchupSpeed,
slowdownSpeed,
ref driftEma,
catchupNegativeThreshold,
catchupPositiveThreshold,
ref deliveryTimeEma);
}
void Update()
{
// only while we have snapshots.
// timeline starts when the first snapshot arrives.
if (snapshots.Count > 0)
{
// snapshot interpolation
if (interpolate)
{
// step
SnapshotInterpolation.Step(
snapshots,
Time.unscaledDeltaTime,
ref localTimeline,
localTimescale,
out Snapshot3D fromSnapshot,
out Snapshot3D toSnapshot,
out double t);
void Update()
{
// only while we have snapshots.
// timeline starts when the first snapshot arrives.
if (snapshots.Count > 0)
{
// snapshot interpolation
if (interpolate)
{
// step
SnapshotInterpolation.Step(
snapshots,
Time.unscaledDeltaTime,
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);
}
}
// 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);
}
}
// color material while catching up / slowing down
if (localTimescale < 1)
render.material.color = slowdownColor;
else if (localTimescale > 1)
render.material.color = catchupColor;
else
render.material.color = defaultColor;
}
// color material while catching up / slowing down
if (localTimescale < 1)
render.material.color = slowdownColor;
else if (localTimescale > 1)
render.material.color = catchupColor;
else
render.material.color = defaultColor;
}
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);
}
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);
}
void OnValidate()
{
// thresholds need to be <0 and >0
catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0);
catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0);
}
}
void OnValidate()
{
// thresholds need to be <0 and >0
catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0);
catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0);
}
}
}

View File

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