feat: NT-UR bit flag changed detection to lower bandwidth usage. (#3721)

* feat: NT-UR bit flag changed detection to lower bandwidth usage.

Also major credits to our Ninja.

* Tooltip updated

* fix: NT-Unreliable Quaternion Compression Fix

Credits to ninja of course :D

* NT-U new improvements

Credits to Ninja

* Nothing to see here..

* Added comment to Quat Rotation Fix

* Sensitivity check to improve value comparisons.

Without this, X 0 and X -4.955753E-07 (0) would trigger as a change of value.
Helps epsilon/floating point inaccuracies.

* Moved around checks.

rotationChanged not needed now for non-compressed bool, as we check individual rotation sensitivity changes.
We can move this inside quat compress check.

* Use Rot/All, not just RotX as a compress changed flag.

* Set Just Rot.

* Updated Reset to ResetState

* Fixing PR 3571/3572/3572 in this new bitflag branch

---------

Co-authored-by: ninjakickja <80569286+ninjakickja@users.noreply.github.com>
This commit is contained in:
JesusLuvsYooh 2024-02-01 16:04:23 +00:00 committed by GitHub
parent 3e0a6ae64e
commit fbd64dfb79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 484 additions and 49 deletions

View File

@ -1,4 +1,5 @@
// NetworkTransform V2 by mischa (2021-07)
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
@ -12,6 +13,8 @@ public class NetworkTransformUnreliable : NetworkTransformBase
// Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover.
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results,.")]
public float bufferResetMultiplier = 3;
[Tooltip("Detect and send only changed data, such as Position X and Z, not the full Vector3 of X Y Z. Lowers network data at cost of extra calculations.")]
public bool changedDetection = true;
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float positionSensitivity = 0.01f;
@ -25,6 +28,7 @@ public class NetworkTransformUnreliable : NetworkTransformBase
// Used to store last sent snapshots
protected TransformSnapshot lastSnapshot;
protected bool cachedSnapshotComparison;
protected Changed cachedChangedComparison;
protected bool hasSentUnchangedPosition;
// update //////////////////////////////////////////////////////////////
@ -61,7 +65,9 @@ protected virtual void CheckLastSendTime()
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
// because intervalCounter is always = 1 in the previous version.
if (sendIntervalCounter == sendIntervalMultiplier)
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
if (sendIntervalCounter >= sendIntervalMultiplier)
sendIntervalCounter = 0;
// timeAsDouble not available in older Unity versions.
@ -109,36 +115,68 @@ void UpdateServerBroadcast()
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
if (changedDetection)
{
RpcServerToClientSyncCompressRotation(
cachedChangedComparison = CompareChangedSnapshots(snapshot);
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
RpcServerToClientSync(syncData);
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
}
}
else
{
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
{
RpcServerToClientSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
RpcServerToClientSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
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?)
);
}
);
}
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
// Fixes https://github.com/MirrorNetworking/Mirror/issues/3572
// This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573
// with the exception of Quaternion.Angle sensitivity has to be > 0.16.
// Unity issue, we are leaving it as is.
if (positionChanged) lastSnapshot.position = snapshot.position;
if (rotationChanged) lastSnapshot.rotation = snapshot.rotation;
if (positionChanged) lastSnapshot.scale = snapshot.scale;
}
}
}
}
@ -205,36 +243,67 @@ void UpdateClientBroadcast()
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
if (changedDetection)
{
CmdClientToServerSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
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);
}
}
else
{
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?)
);
}
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
if (compressRotation)
{
CmdClientToServerSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
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?)
);
}
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
// Fixes https://github.com/MirrorNetworking/Mirror/issues/3572
// This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573
// with the exception of Quaternion.Angle sensitivity has to be > 0.16.
// Unity issue, we are leaving it as is.
if (positionChanged) lastSnapshot.position = snapshot.position;
if (rotationChanged) lastSnapshot.rotation = snapshot.rotation;
if (positionChanged) lastSnapshot.scale = snapshot.scale;
}
}
}
}
@ -406,5 +475,204 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot)
{
if (change == Changed.None || change == Changed.CompressRot) return;
if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x;
if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y;
if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z;
if (compressRotation)
{
if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation;
}
else
{
Vector3 newRotation;
newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x;
newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y;
newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z;
lastSnapshot.rotation = Quaternion.Euler(newRotation);
}
if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale;
}
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
// Note the sensitivity comparison are different for pos, rot and scale.
protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot)
{
Changed change = Changed.None;
if (syncPosition)
{
bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
if (positionChanged)
{
if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX;
if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY;
if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ;
}
}
if (syncRotation)
{
if (compressRotation)
{
bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
if (rotationChanged)
{
// Here we set all Rot enum flags, to tell us if there was a change in rotation
// when using compression. If no change, we don't write the compressed Quat.
change |= Changed.CompressRot;
change |= Changed.Rot;
}
else
{
change |= Changed.CompressRot;
}
}
else
{
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX;
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY;
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ;
}
}
if (syncScale)
{
if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale;
}
return change;
}
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSync(SyncData syncData)
{
OnClientToServerSync(syncData);
//For client authority, immediately pass on the client snapshot to all other
//clients instead of waiting for server to send its snapshots.
if (syncDirection == SyncDirection.ClientToServer)
RpcServerToClientSync(syncData);
}
protected virtual void OnClientToServerSync(SyncData syncData)
{
// only apply if in client authority mode
if (syncDirection != SyncDirection.ClientToServer) return;
// protect against ever growing buffer size attacks
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
// only player owned objects (with a connection) can send to
// server. we can get the timestamp from the connection.
double timestamp = connectionToClient.remoteTimeStamp;
if (onlySyncOnChange)
{
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
ResetState();
}
UpdateSyncData(ref syncData, serverSnapshots);
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
}
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientSync(SyncData syncData) =>
OnServerToClientSync(syncData);
protected virtual void OnServerToClientSync(SyncData syncData)
{
// in host mode, the server sends rpcs to all clients.
// the host client itself will receive them too.
// -> host server is always the source of truth
// -> we can ignore any rpc on the host client
// => otherwise host objects would have ever growing clientBuffers
// (rpc goes to clients. if isServer is true too then we are host)
if (isServer) return;
// don't apply for local player with authority
if (IsClientWithAuthority) return;
// on the client, we receive rpcs for all entities.
// not all of them have a connectionToServer.
// but all of them go through NetworkClient.connection.
// we can get the timestamp from there.
double timestamp = NetworkClient.connection.remoteTimeStamp;
if (onlySyncOnChange)
{
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
ResetState();
}
UpdateSyncData(ref syncData, clientSnapshots);
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
}
protected virtual void UpdateSyncData(ref SyncData syncData, SortedList<double, TransformSnapshot> snapshots)
{
if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot)
{
syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
}
else
{
// Just going to update these without checking if syncposition or not,
// because if not syncing position, NT will not apply any position data
// to the target during Apply().
syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x);
syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y);
syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z);
// If compressRot is true, we already have the Quat in syncdata.
if ((syncData.changedDataByte & Changed.CompressRot) == 0)
{
syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x);
syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ;
syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z);
syncData.quatRotation = Quaternion.Euler(syncData.vecRotation);
}
else
{
syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation());
}
syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale());
}
}
// This is to extract position/rotation/scale data from payload. Override
// Construct and Deconstruct if you are implementing a different SyncData logic.
// Note however that snapshot interpolation still requires the basic 3 data
// position, rotation and scale, which are computed from here.
protected virtual void DeconstructSyncData(System.ArraySegment<byte> receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale)
{
using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload))
{
SyncData syncData = reader.Read<SyncData>();
changedFlagData = (byte)syncData.changedDataByte;
position = syncData.position;
rotation = syncData.quatRotation;
scale = syncData.scale;
}
}
}
}

View File

@ -0,0 +1,156 @@
using UnityEngine;
using System;
using Mirror;
namespace Mirror
{
[Serializable]
public struct SyncData
{
public Changed changedDataByte;
public Vector3 position;
public Quaternion quatRotation;
public Vector3 vecRotation;
public Vector3 scale;
public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale)
{
this.changedDataByte = _dataChangedByte;
this.position = _position;
this.quatRotation = _rotation;
this.vecRotation = quatRotation.eulerAngles;
this.scale = _scale;
}
public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot)
{
this.changedDataByte = _dataChangedByte;
this.position = _snapshot.position;
this.quatRotation = _snapshot.rotation;
this.vecRotation = quatRotation.eulerAngles;
this.scale = _snapshot.scale;
}
public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale)
{
this.changedDataByte = _dataChangedByte;
this.position = _position;
this.vecRotation = _vecRotation;
this.quatRotation = Quaternion.Euler(vecRotation);
this.scale = _scale;
}
}
[Flags]
public enum Changed : byte
{
None = 0,
PosX = 1 << 0,
PosY = 1 << 1,
PosZ = 1 << 2,
CompressRot = 1 << 3,
RotX = 1 << 4,
RotY = 1 << 5,
RotZ = 1 << 6,
Scale = 1 << 7,
Pos = PosX | PosY | PosZ,
Rot = RotX | RotY | RotZ
}
public static class SyncDataReaderWriter
{
public static void WriteSyncData(this NetworkWriter writer, SyncData syncData)
{
writer.WriteByte((byte)syncData.changedDataByte);
// Write position
if ((syncData.changedDataByte & Changed.PosX) > 0)
{
writer.WriteFloat(syncData.position.x);
}
if ((syncData.changedDataByte & Changed.PosY) > 0)
{
writer.WriteFloat(syncData.position.y);
}
if ((syncData.changedDataByte & Changed.PosZ) > 0)
{
writer.WriteFloat(syncData.position.z);
}
// Write rotation
if ((syncData.changedDataByte & Changed.CompressRot) > 0)
{
if((syncData.changedDataByte & Changed.Rot) > 0)
{
writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation));
}
}
else
{
if ((syncData.changedDataByte & Changed.RotX) > 0)
{
writer.WriteFloat(syncData.quatRotation.eulerAngles.x);
}
if ((syncData.changedDataByte & Changed.RotY) > 0)
{
writer.WriteFloat(syncData.quatRotation.eulerAngles.y);
}
if ((syncData.changedDataByte & Changed.RotZ) > 0)
{
writer.WriteFloat(syncData.quatRotation.eulerAngles.z);
}
}
// Write scale
if ((syncData.changedDataByte & Changed.Scale) > 0)
{
writer.WriteVector3(syncData.scale);
}
}
public static SyncData ReadSyncData(this NetworkReader reader)
{
Changed changedData = (Changed)reader.ReadByte();
// If we have nothing to read here, let's say because posX is unchanged, then we can write anything
// for now, but in the NT, we will need to check changedData again, to put the right values of the axis
// back. We don't have it here.
Vector3 position =
new Vector3(
(changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0,
(changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0,
(changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0
);
Vector3 vecRotation = new Vector3();
Quaternion quatRotation = new Quaternion();
if ((changedData & Changed.CompressRot) > 0)
{
quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion();
}
else
{
vecRotation =
new Vector3(
(changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0,
(changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0,
(changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0
);
}
Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3();
SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale);
return _syncData;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1c0832ca88e749ff96fe04cebb617ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant: