mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
fix(NetworkTransformReliable): Fix stutter and recovery
This commit is contained in:
parent
0cd06db1f2
commit
b4e689ccc0
@ -1,4 +1,5 @@
|
||||
// NetworkTransform V3 (reliable) by mischa (2022-10)
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
@ -8,12 +9,22 @@ namespace Mirror
|
||||
[AddComponentMenu("Network/Network Transform (Reliable)")]
|
||||
public class NetworkTransformReliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
|
||||
float onlySyncOnChangeInterval => onlySyncOnChangeCorrectionMultiplier * sendIntervalMultiplier;
|
||||
|
||||
[Header("Sync Only If Changed")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
[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;
|
||||
|
||||
// uint so non negative.
|
||||
[Header("Send Interval Multiplier")]
|
||||
[Tooltip("Send every multiple of Network Manager send interval (= 1 / NM Send Rate).")]
|
||||
public uint sendIntervalMultiplier = 3;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
@ -43,7 +54,7 @@ public class NetworkTransformReliable : NetworkTransformBase
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
int lastClientCount = 0;
|
||||
protected int lastClientCount = 1;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
@ -55,7 +66,35 @@ void Update()
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
void LateUpdate()
|
||||
{
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
|
||||
// the possibility of Update() running first before the object's movement
|
||||
// script's Update(), which then causes NT to send every alternate frame
|
||||
// instead.
|
||||
if (isServer || (IsClientWithAuthority && NetworkClient.ready)) // is NetworkClient.ready even needed?
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
|
||||
SetDirty();
|
||||
|
||||
CheckLastSendTime();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
if (isOwned && TryGetComponent(out CharacterController cc))
|
||||
{
|
||||
cc.enabled = false;
|
||||
base.OnTeleport(destination);
|
||||
cc.enabled = true;
|
||||
}
|
||||
else
|
||||
base.OnTeleport(destination);
|
||||
}
|
||||
|
||||
protected virtual void UpdateServer()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
@ -65,9 +104,7 @@ void UpdateServer()
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
if (syncDirection == SyncDirection.ClientToServer && connectionToClient != null && !isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
@ -82,42 +119,20 @@ void UpdateServer()
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// technically snapshot interpolation requires constant sending.
|
||||
// however, with reliable it should be fine without constant sends.
|
||||
//
|
||||
// detect changes _after_ all changes were applied above.
|
||||
if (!onlySyncOnChange || Changed(Construct()))
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
protected virtual void UpdateClient()
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (IsClientWithAuthority)
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// technically snapshot interpolation requires constant sending.
|
||||
// however, with reliable it should be fine without constant sends.
|
||||
if (!onlySyncOnChange || Changed(Construct()))
|
||||
SetDirty();
|
||||
}
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer
|
||||
else
|
||||
{
|
||||
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
@ -129,8 +144,8 @@ void UpdateClient()
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
// 'only sync if moved'
|
||||
@ -147,6 +162,26 @@ void UpdateClient()
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
#else
|
||||
if (AccurateInterval.Elapsed(Time.timeAsDouble, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// check if position / rotation / scale changed since last sync
|
||||
protected virtual bool Changed(TransformSnapshot current) =>
|
||||
// position is quantized and delta compressed.
|
||||
@ -191,7 +226,21 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(snapshot.position);
|
||||
// If there is a last serialized snapshot, we use it.
|
||||
// This prevents the new client getting a snapshot that is different
|
||||
// from what the older clients last got. If this happens, and on the next
|
||||
// regular serialisation the delta compression will get wrong values.
|
||||
// Notes:
|
||||
// 1. Interestingly only the older clients have it wrong, because at the end
|
||||
// of the function, last = snapshot which is the initial state's snapshot
|
||||
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
|
||||
// snapshot constructed would have been the same as the last anyway.
|
||||
if (last.remoteTime > 0)
|
||||
snapshot = last;
|
||||
|
||||
if (syncPosition)
|
||||
writer.WriteVector3(snapshot.position);
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
@ -200,7 +249,9 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
|
||||
if (syncScale)
|
||||
writer.WriteVector3(snapshot.scale);
|
||||
}
|
||||
// delta
|
||||
else
|
||||
@ -213,6 +264,7 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
@ -221,20 +273,20 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
// quantize -> delta -> varint
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
|
||||
}
|
||||
|
||||
// int written = writer.Position - before;
|
||||
// Debug.Log($"{name} compressed to {written} bytes");
|
||||
}
|
||||
|
||||
// save serialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
if (syncPosition)
|
||||
Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncScale)
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
|
||||
// set 'last'
|
||||
last = snapshot;
|
||||
@ -249,7 +301,9 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) position = reader.ReadVector3();
|
||||
if (syncPosition)
|
||||
position = reader.ReadVector3();
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
@ -258,7 +312,9 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
|
||||
if (syncScale)
|
||||
scale = reader.ReadVector3();
|
||||
}
|
||||
// delta
|
||||
else
|
||||
@ -269,6 +325,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
|
||||
position = Compression.ScaleToFloat(quantized, positionPrecision);
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
// (optional) smallest three compression for now. no delta.
|
||||
@ -277,6 +334,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
|
||||
@ -286,12 +344,16 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
|
||||
// handle depending on server / client / host.
|
||||
// server has priority for host mode.
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient) OnServerToClientSync(position, rotation, scale);
|
||||
if (isServer)
|
||||
OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient)
|
||||
OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// save deserialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
if (syncPosition)
|
||||
Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncScale)
|
||||
Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
}
|
||||
|
||||
// sync ////////////////////////////////////////////////////////////////
|
||||
@ -306,21 +368,19 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval, onlySyncOnChangeCorrectionMultiplier))
|
||||
if (onlySyncOnChange && NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval, onlySyncOnChangeInterval))
|
||||
{
|
||||
RewriteHistory(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale);
|
||||
// Debug.Log($"{name}: corrected history on server to fix initial stutter after not sending for a while.");
|
||||
}
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp, position, rotation, scale);
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + NetworkServer.sendInterval * sendIntervalMultiplier, position, rotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
@ -331,20 +391,19 @@ 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, onlySyncOnChangeCorrectionMultiplier))
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeInterval))
|
||||
{
|
||||
RewriteHistory(
|
||||
clientSnapshots,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval,
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale);
|
||||
// Debug.Log($"{name}: corrected history on client to fix initial stutter after not sending for a while.");
|
||||
}
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp, position, rotation, scale);
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + NetworkClient.sendInterval * sendIntervalMultiplier, position, rotation, scale);
|
||||
}
|
||||
|
||||
// only sync on change /////////////////////////////////////////////////
|
||||
@ -359,9 +418,12 @@ static bool NeedsCorrection(
|
||||
SortedList<double, TransformSnapshot> snapshots,
|
||||
double remoteTimestamp,
|
||||
double bufferTime,
|
||||
double toleranceMultiplier) =>
|
||||
snapshots.Count == 1 &&
|
||||
double toleranceMultiplier)
|
||||
{
|
||||
bool value = snapshots.Count == 1 &&
|
||||
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
|
||||
return value;
|
||||
}
|
||||
|
||||
// 2. insert a fake snapshot at current position,
|
||||
// exactly one 'sendInterval' behind the newly received one.
|
||||
@ -380,6 +442,7 @@ static void RewriteHistory(
|
||||
|
||||
// insert a fake one at where we used to be,
|
||||
// 'sendInterval' behind the new one.
|
||||
|
||||
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
|
||||
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
|
||||
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
@ -399,6 +462,8 @@ public override void Reset()
|
||||
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user