NetworkTransform syncInterval WIP

This commit is contained in:
mischa 2024-08-29 11:07:05 +02:00
parent e51c996fa6
commit 74e66accc6
3 changed files with 41 additions and 103 deletions

View File

@ -67,31 +67,6 @@ public abstract class NetworkTransformBase : NetworkBehaviour
[Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")]
public CoordinateSpace coordinateSpace = CoordinateSpace.Local;
[Header("Send Interval Multiplier")]
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.\n(30 NM send rate, and 3 interval, is a send every 0.1 seconds)\nA larger interval means less network sends, which has a variety of upsides. The drawbacks are delays and lower accuracy, you should find a nice balance between not sending too much, but the results looking good for your particular scenario.")]
[Range(1, 120)]
public uint sendIntervalMultiplier = 1;
[Header("Timeline Offset")]
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
public bool timelineOffset = false;
// Ninja's Notes on offset & mulitplier:
//
// In a no multiplier scenario:
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
//
// In a multiplier scenario:
// 1. Snapshots are sent every 10 frames.
// 2. Time Interpolation remains 'behind by 2 frames'.
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
//
protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
// debugging ///////////////////////////////////////////////////////////
[Header("Debug")]
public bool showGizmos;
@ -116,13 +91,6 @@ protected virtual void Configure()
// set target to self if none yet
if (target == null) target = transform;
// time snapshot interpolation happens globally.
// value (transform) happens in here.
// both always need to be on the same send interval.
// force the setting to '0' in OnValidate to make it obvious that we
// actually use NetworkServer.sendInterval.
syncInterval = 0;
// Unity doesn't support setting world scale.
// OnValidate force disables syncScale in world mode.
if (coordinateSpace == CoordinateSpace.World) syncScale = false;

View File

@ -8,9 +8,6 @@ namespace Mirror
[AddComponentMenu("Network/Network Transform (Reliable)")]
public class NetworkTransformReliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
[Header("Additional Settings")]
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
public float onlySyncOnChangeCorrectionMultiplier = 2;
@ -59,12 +56,11 @@ void LateUpdate()
// the possibility of Update() running first before the object's movement
// script's Update(), which then causes NT to send every alternate frame
// instead.
// note that even if we SetDirty every update, it only syncs every syncInterval.
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
{
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
if (!onlySyncOnChange || Changed(Construct()))
SetDirty();
CheckLastSendTime();
}
}
@ -124,17 +120,6 @@ protected virtual void UpdateClient()
}
}
protected virtual void CheckLastSendTime()
{
// timeAsDouble not available in older Unity versions.
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
{
if (sendIntervalCounter == sendIntervalMultiplier)
sendIntervalCounter = 0;
sendIntervalCounter++;
}
}
// check if position / rotation / scale changed since last sync
protected virtual bool Changed(TransformSnapshot current) =>
// position is quantized and delta compressed.
@ -302,13 +287,13 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, syncInterval, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
serverSnapshots,
connectionToClient.remoteTimeStamp,
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
connectionToClient.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
syncInterval,
GetPosition(),
GetRotation(),
GetScale());
@ -330,13 +315,13 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, syncInterval, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
clientSnapshots,
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
NetworkClient.sendInterval * sendIntervalMultiplier,
syncInterval,
GetPosition(),
GetRotation(),
GetScale());

View File

@ -7,7 +7,6 @@ namespace Mirror
[AddComponentMenu("Network/Network Transform (Unreliable)")]
public class NetworkTransformUnreliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
[Header("Additional Settings")]
@ -56,27 +55,9 @@ void LateUpdate()
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
}
protected virtual void CheckLastSendTime()
{
// We check interval every frame, and then send if interval is reached.
// So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent,
// thus we reset the counter here.
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
// because intervalCounter is always = 1 in the previous version.
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
if (sendIntervalCounter >= sendIntervalMultiplier)
sendIntervalCounter = 0;
// timeAsDouble not available in older Unity versions.
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
sendIntervalCounter++;
}
void UpdateServerBroadcast()
{
// broadcast to all clients each 'sendInterval'
// broadcast to all clients each 'syncInterval'
// (client with authority will drop the rpc)
// NetworkTime.localTime for double precision until Unity has it too
//
@ -106,10 +87,12 @@ void UpdateServerBroadcast()
// authoritative movement done by the host will have to be broadcasted
// here by checking IsClientWithAuthority.
// TODO send same time that NetworkServer sends time snapshot?
CheckLastSendTime();
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
// only send every syncInterval
if (NetworkTime.localTime < lastSendIntervalTime + syncInterval) return;
lastSendIntervalTime = NetworkTime.localTime;
if (syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority)
{
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
@ -191,30 +174,32 @@ void UpdateClientBroadcast()
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
CheckLastSendTime();
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
// only send every syncInterval
if (NetworkTime.localTime < lastSendIntervalTime + syncInterval) return;
lastSendIntervalTime = NetworkTime.localTime;
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
cachedChangedComparison = CompareChangedSnapshots(snapshot);
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
CmdClientToServerSync(syncData);
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
{
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
cachedChangedComparison = CompareChangedSnapshots(snapshot);
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
CmdClientToServerSync(syncData);
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
}
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
}
}