This commit is contained in:
mischa 2023-05-07 23:10:51 +09:00
parent 4367430b29
commit 318b8846bf
3 changed files with 1 additions and 223 deletions

View File

@ -29,7 +29,6 @@ public abstract class NetworkTransformBase : NetworkBehaviour
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
public Transform target; public Transform target;
// TODO SyncDirection { ClientToServer, ServerToClient } is easier?
// Deprecated 2022-10-25 // Deprecated 2022-10-25
[Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")] [Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
[Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually. [Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually.

View File

@ -1,6 +1,4 @@
// NetworkTransform V3 (reliable) by mischa (2022-10) // NetworkTransform V3 (reliable) by mischa (2022-10)
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine; using UnityEngine;
namespace Mirror namespace Mirror
@ -8,15 +6,7 @@ namespace Mirror
[AddComponentMenu("Network/Network Transform (Reliable)")] [AddComponentMenu("Network/Network Transform (Reliable)")]
public class NetworkTransformReliable : NetworkTransformBase public class NetworkTransformReliable : NetworkTransformBase
{ {
[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;
[Header("Rotation")] [Header("Rotation")]
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float rotationSensitivity = 0.01f;
[Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")] [Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")]
public bool compressRotation = false; public bool compressRotation = false;
@ -44,9 +34,6 @@ public class NetworkTransformReliable : NetworkTransformBase
protected Vector3Long lastSerializedScale = Vector3Long.zero; protected Vector3Long lastSerializedScale = Vector3Long.zero;
protected Vector3Long lastDeserializedScale = Vector3Long.zero; protected Vector3Long lastDeserializedScale = Vector3Long.zero;
// Used to store last sent snapshots
protected TransformSnapshot last;
// update ////////////////////////////////////////////////////////////// // update //////////////////////////////////////////////////////////////
// Update applies interpolation. // Update applies interpolation.
void Update() void Update()
@ -68,8 +55,7 @@ void LateUpdate()
// set dirty to trigger OnSerialize. either always, or only if changed. // set dirty to trigger OnSerialize. either always, or only if changed.
if (isServer || (IsClientWithAuthority && NetworkClient.ready)) // is NetworkClient.ready even needed? if (isServer || (IsClientWithAuthority && NetworkClient.ready)) // is NetworkClient.ready even needed?
{ {
if (!onlySyncOnChange || Changed(Construct())) SetDirty();
SetDirty();
} }
} }
@ -129,29 +115,6 @@ protected virtual void UpdateClient()
} }
} }
// check if position / rotation / scale changed since last sync
protected virtual bool Changed(TransformSnapshot current) =>
// position is quantized and delta compressed.
// only consider it changed if the quantized representation is changed.
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
QuantizedChanged(last.position, current.position, positionPrecision) ||
// rotation isn't quantized / delta compressed.
// check with sensitivity.
Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity ||
// scale is quantized and delta compressed.
// only consider it changed if the quantized representation is changed.
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
QuantizedChanged(last.scale, current.scale, scalePrecision);
// helper function to compare quantized representations of a Vector3
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision)
{
Compression.ScaleToLong(u, precision, out Vector3Long uQuantized);
Compression.ScaleToLong(v, precision, out Vector3Long vQuantized);
return uQuantized != vQuantized;
}
// NT may be used on client/server/host to Owner/Observers with // NT may be used on client/server/host to Owner/Observers with
// ServerToClient or ClientToServer. // ServerToClient or ClientToServer.
// however, OnSerialize should always delta against last. // however, OnSerialize should always delta against last.
@ -217,9 +180,6 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
// save serialized as 'last' for next delta compression // save serialized as 'last' for next delta compression
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
// set 'last'
last = snapshot;
} }
public override void OnDeserialize(NetworkReader reader, bool initialState) public override void OnDeserialize(NetworkReader reader, bool initialState)
@ -287,21 +247,6 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
// protect against ever growing buffer size attacks // protect against ever growing buffer size attacks
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; 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))
{
RewriteHistory(
serverSnapshots,
connectionToClient.remoteTimeStamp,
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
NetworkServer.sendInterval, // 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.");
}
// add a small timeline offset to account for decoupled arrival of // add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots. // NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it. // needs to be sendInterval. half sendInterval doesn't solve it.
@ -317,21 +262,6 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
// don't apply for local player with authority // don't apply for local player with authority
if (IsClientWithAuthority) return; if (IsClientWithAuthority) return;
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
clientSnapshots,
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
NetworkClient.sendInterval,
target.localPosition,
target.localRotation,
target.localScale);
// Debug.Log($"{name}: corrected history on client to fix initial stutter after not sending for a while.");
}
// add a small timeline offset to account for decoupled arrival of // add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots. // NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it. // needs to be sendInterval. half sendInterval doesn't solve it.
@ -342,47 +272,6 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
} }
// only sync on change ///////////////////////////////////////////////// // only sync on change /////////////////////////////////////////////////
// snap interp. needs a continous flow of packets.
// 'only sync on change' interrupts it while not changed.
// once it restarts, snap interp. will interp from the last old position.
// this will cause very noticeable stutter for the first move each time.
// the fix is quite simple.
// 1. detect if the remaining snapshot is too old from a past move.
static bool NeedsCorrection(
SortedList<double, TransformSnapshot> snapshots,
double remoteTimestamp,
double bufferTime,
double toleranceMultiplier) =>
snapshots.Count == 1 &&
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
// 2. insert a fake snapshot at current position,
// exactly one 'sendInterval' behind the newly received one.
static void RewriteHistory(
SortedList<double, TransformSnapshot> snapshots,
// timestamp of packet arrival, not interpolated remote time!
double remoteTimeStamp,
double localTime,
double sendInterval,
Vector3 position,
Quaternion rotation,
Vector3 scale)
{
// clear the previous snapshot
snapshots.Clear();
// 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
position,
rotation,
scale
));
}
public override void Reset() public override void Reset()
{ {
base.Reset(); base.Reset();
@ -393,9 +282,6 @@ public override void Reset()
lastSerializedScale = Vector3Long.zero; lastSerializedScale = Vector3Long.zero;
lastDeserializedScale = Vector3Long.zero; lastDeserializedScale = Vector3Long.zero;
// reset 'last' for delta too
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
} }
} }
} }

View File

@ -1,6 +1,5 @@
// NetworkTransform V2 by mischa (2021-07) // NetworkTransform V2 by mischa (2021-07)
// comment out the below line to quickly revert the onlySyncOnChange feature // comment out the below line to quickly revert the onlySyncOnChange feature
#define onlySyncOnChange_BANDWIDTH_SAVING
using UnityEngine; using UnityEngine;
namespace Mirror namespace Mirror
@ -8,31 +7,6 @@ namespace Mirror
[AddComponentMenu("Network/Network Transform (Unreliable)")] [AddComponentMenu("Network/Network Transform (Unreliable)")]
public class NetworkTransform : NetworkTransformBase public class NetworkTransform : NetworkTransformBase
{ {
// only sync when changed hack /////////////////////////////////////////
#if onlySyncOnChange_BANDWIDTH_SAVING
[Header("Sync Only If Changed")]
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
public bool onlySyncOnChange = true;
// 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching.
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")]
public float bufferResetMultiplier = 5;
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float positionSensitivity = 0.01f;
public float rotationSensitivity = 0.01f;
public float scaleSensitivity = 0.01f;
protected bool positionChanged;
protected bool rotationChanged;
protected bool scaleChanged;
// Used to store last sent snapshots
protected TransformSnapshot lastSnapshot;
protected bool cachedSnapshotComparison;
protected bool hasSentUnchangedPosition;
#endif
double lastClientSendTime; double lastClientSendTime;
double lastServerSendTime; double lastServerSendTime;
@ -84,39 +58,15 @@ void UpdateServerBroadcast()
// send snapshot without timestamp. // send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth. // receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct(); TransformSnapshot snapshot = Construct();
#if onlySyncOnChange_BANDWIDTH_SAVING
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
#endif
#if onlySyncOnChange_BANDWIDTH_SAVING
RpcServerToClientSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
#else
RpcServerToClientSync( RpcServerToClientSync(
// only sync what the user wants to sync // only sync what the user wants to sync
syncPosition ? snapshot.position : default(Vector3?), syncPosition ? snapshot.position : default(Vector3?),
syncRotation ? snapshot.rotation : default(Quaternion?), syncRotation ? snapshot.rotation : default(Quaternion?),
syncScale ? snapshot.scale : default(Vector3?) syncScale ? snapshot.scale : default(Vector3?)
); );
#endif
lastServerSendTime = NetworkTime.localTime; lastServerSendTime = NetworkTime.localTime;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
}
#endif
} }
} }
@ -190,39 +140,15 @@ void UpdateClientBroadcast()
// send snapshot without timestamp. // send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth. // receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct(); TransformSnapshot snapshot = Construct();
#if onlySyncOnChange_BANDWIDTH_SAVING
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
#endif
#if onlySyncOnChange_BANDWIDTH_SAVING
CmdClientToServerSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
#else
CmdClientToServerSync( CmdClientToServerSync(
// only sync what the user wants to sync // only sync what the user wants to sync
syncPosition ? snapshot.position : default(Vector3?), syncPosition ? snapshot.position : default(Vector3?),
syncRotation ? snapshot.rotation : default(Quaternion?), syncRotation ? snapshot.rotation : default(Quaternion?),
syncScale ? snapshot.scale : default(Vector3?) syncScale ? snapshot.scale : default(Vector3?)
); );
#endif
lastClientSendTime = NetworkTime.localTime; lastClientSendTime = NetworkTime.localTime;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
}
#endif
} }
} }
@ -286,17 +212,6 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
} }
} }
#if onlySyncOnChange_BANDWIDTH_SAVING
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot)
{
positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity;
return (!positionChanged && !rotationChanged && !scaleChanged);
}
#endif
// cmd ///////////////////////////////////////////////////////////////// // cmd /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file. // only unreliable. see comment above of this file.
[Command(channel = Channels.Unreliable)] [Command(channel = Channels.Unreliable)]
@ -323,17 +238,6 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
// only player owned objects (with a connection) can send to // only player owned objects (with a connection) can send to
// server. we can get the timestamp from the connection. // server. we can get the timestamp from the connection.
double timestamp = connectionToClient.remoteTimeStamp; double timestamp = connectionToClient.remoteTimeStamp;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (onlySyncOnChange)
{
double timeIntervalCheck = bufferResetMultiplier * NetworkClient.sendInterval;
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
{
Reset();
}
}
#endif
AddSnapshot(serverSnapshots, timestamp, position, rotation, scale); AddSnapshot(serverSnapshots, timestamp, position, rotation, scale);
} }
@ -362,17 +266,6 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
// but all of them go through NetworkClient.connection. // but all of them go through NetworkClient.connection.
// we can get the timestamp from there. // we can get the timestamp from there.
double timestamp = NetworkClient.connection.remoteTimeStamp; double timestamp = NetworkClient.connection.remoteTimeStamp;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (onlySyncOnChange)
{
double timeIntervalCheck = bufferResetMultiplier * NetworkServer.sendInterval;
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
{
Reset();
}
}
#endif
AddSnapshot(clientSnapshots, timestamp, position, rotation, scale); AddSnapshot(clientSnapshots, timestamp, position, rotation, scale);
} }
} }