Keep local version of files

This commit is contained in:
2026-10-08 00:49:10 +05:30
parent 7fa0621ff8
commit 053665c690
7343 changed files with 5470578 additions and 5470528 deletions
File diff suppressed because it is too large Load Diff
@@ -1,18 +1,18 @@
fileFormatVersion: 2
guid: 7c44135fde488424eaf28566206ce473
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs
uploadId: 736421
fileFormatVersion: 2
guid: 7c44135fde488424eaf28566206ce473
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs
uploadId: 736421
File diff suppressed because it is too large Load Diff
@@ -1,18 +1,18 @@
fileFormatVersion: 2
guid: 8f63ea2e505fd484193fb31c5c55ca73
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs
uploadId: 736421
fileFormatVersion: 2
guid: 8f63ea2e505fd484193fb31c5c55ca73
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs
uploadId: 736421
@@ -1,448 +1,448 @@
// NetworkTransform V3 (reliable) by mischa (2022-10)
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Reliable)")]
public class NetworkTransformReliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
TransformSnapshot? pendingSnapshot;
[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;
public bool useFixedUpdate;
[Header("Rotation")]
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float rotationSensitivity = 0.01f;
// delta compression is capable of detecting byte-level changes.
// if we scale float position to bytes,
// then small movements will only change one byte.
// this gives optimal bandwidth.
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
[Header("Precision")]
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
public float positionPrecision = 0.01f; // 1 cm
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
public float scalePrecision = 0.01f; // 1 cm
// delta compression needs to remember 'last' to compress against
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
protected Vector3Long lastSerializedScale = Vector3Long.zero;
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
// Used to store last sent snapshots
protected TransformSnapshot last;
// update //////////////////////////////////////////////////////////////
void Update()
{
// if server then always sync to others.
if (isServer) UpdateServer();
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
else if (isClient) UpdateClient();
}
void FixedUpdate()
{
if (!useFixedUpdate) return;
if (pendingSnapshot.HasValue && !IsClientWithAuthority)
{
// Apply via base method, but in FixedUpdate
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
pendingSnapshot = null;
}
}
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))
{
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
SetDirty();
CheckLastSendTime();
}
}
protected virtual void UpdateServer()
{
// apply buffered snapshots IF client authority
// -> in server authority, server moves the object
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// 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 (serverSnapshots.Count > 0)
{
// step the transform interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
serverSnapshots,
connectionToClient.remoteTimeline,
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
}
}
}
protected virtual void UpdateClient()
{
if (useFixedUpdate)
{
if (!IsClientWithAuthority && clientSnapshots.Count > 0)
{
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time,
out TransformSnapshot from,
out TransformSnapshot to,
out double t
);
pendingSnapshot = TransformSnapshot.Interpolate(from, to, t);
}
}
else
{
// client authority, and local player (= allowed to move myself)?
if (!IsClientWithAuthority)
{
// only while we have snapshots
if (clientSnapshots.Count > 0)
{
// step the interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
}
}
}
}
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.
// 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
// ServerToClient or ClientToServer.
// however, OnSerialize should always delta against last.
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// get current snapshot for broadcasting.
TransformSnapshot snapshot = Construct();
// ClientToServer optimization:
// for interpolated client owned identities,
// always broadcast the latest known snapshot so other clients can
// interpolate immediately instead of catching up too
// TODO dirty mask? [compression is very good w/o it already]
// each vector's component is delta compressed.
// an unchanged component would still require 1 byte.
// let's use a dirty bit mask to filter those out as well.
// initial
if (initialState)
{
// 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 this 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.
if (compressRotation)
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
else
writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale) writer.WriteVector3(snapshot.scale);
}
// delta
else
{
// int before = writer.Position;
if (syncPosition)
{
// quantize -> delta -> varint
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
else
writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale)
{
// quantize -> delta -> varint
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
}
}
// 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);
// set 'last'
last = snapshot;
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
Vector3? position = null;
Quaternion? rotation = null;
Vector3? scale = null;
// initial
if (initialState)
{
if (syncPosition) position = reader.ReadVector3();
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
else
rotation = reader.ReadQuaternion();
}
if (syncScale) scale = reader.ReadVector3();
}
// delta
else
{
// varint -> delta -> quantize
if (syncPosition)
{
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
position = Compression.ScaleToFloat(quantized, positionPrecision);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
else
rotation = reader.ReadQuaternion();
}
if (syncScale)
{
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
scale = Compression.ScaleToFloat(quantized, scalePrecision);
}
}
// 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);
// 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);
}
// sync ////////////////////////////////////////////////////////////////
// local authority client sends sync message to server for broadcasting
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// 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 sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
serverSnapshots,
connectionToClient.remoteTimeStamp,
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
GetPosition(),
GetRotation(),
GetScale());
}
// add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it.
// https://github.com/MirrorNetworking/Mirror/issues/3427
// remove this after LocalWorldState.
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// server broadcasts sync message to all clients
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// don't apply for local player with authority
if (IsClientWithAuthority) return;
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
clientSnapshots,
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
NetworkClient.sendInterval * sendIntervalMultiplier,
GetPosition(),
GetRotation(),
GetScale());
}
// add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it.
// https://github.com/MirrorNetworking/Mirror/issues/3427
// remove this after LocalWorldState.
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// 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,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
position,
rotation,
scale
)
);
}
// reset state for next session.
// do not ever call this during a session (i.e. after teleport).
// calling this will break delta compression.
public override void ResetState()
{
base.ResetState();
// reset delta
lastSerializedPosition = Vector3Long.zero;
lastDeserializedPosition = Vector3Long.zero;
lastSerializedScale = Vector3Long.zero;
lastDeserializedScale = Vector3Long.zero;
// reset 'last' for delta too
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
}
}
}
// NetworkTransform V3 (reliable) by mischa (2022-10)
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Reliable)")]
public class NetworkTransformReliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
TransformSnapshot? pendingSnapshot;
[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;
public bool useFixedUpdate;
[Header("Rotation")]
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float rotationSensitivity = 0.01f;
// delta compression is capable of detecting byte-level changes.
// if we scale float position to bytes,
// then small movements will only change one byte.
// this gives optimal bandwidth.
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
[Header("Precision")]
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
public float positionPrecision = 0.01f; // 1 cm
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
public float scalePrecision = 0.01f; // 1 cm
// delta compression needs to remember 'last' to compress against
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
protected Vector3Long lastSerializedScale = Vector3Long.zero;
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
// Used to store last sent snapshots
protected TransformSnapshot last;
// update //////////////////////////////////////////////////////////////
void Update()
{
// if server then always sync to others.
if (isServer) UpdateServer();
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
else if (isClient) UpdateClient();
}
void FixedUpdate()
{
if (!useFixedUpdate) return;
if (pendingSnapshot.HasValue && !IsClientWithAuthority)
{
// Apply via base method, but in FixedUpdate
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
pendingSnapshot = null;
}
}
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))
{
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
SetDirty();
CheckLastSendTime();
}
}
protected virtual void UpdateServer()
{
// apply buffered snapshots IF client authority
// -> in server authority, server moves the object
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// 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 (serverSnapshots.Count > 0)
{
// step the transform interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
serverSnapshots,
connectionToClient.remoteTimeline,
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
}
}
}
protected virtual void UpdateClient()
{
if (useFixedUpdate)
{
if (!IsClientWithAuthority && clientSnapshots.Count > 0)
{
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time,
out TransformSnapshot from,
out TransformSnapshot to,
out double t
);
pendingSnapshot = TransformSnapshot.Interpolate(from, to, t);
}
}
else
{
// client authority, and local player (= allowed to move myself)?
if (!IsClientWithAuthority)
{
// only while we have snapshots
if (clientSnapshots.Count > 0)
{
// step the interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
}
}
}
}
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.
// 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
// ServerToClient or ClientToServer.
// however, OnSerialize should always delta against last.
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// get current snapshot for broadcasting.
TransformSnapshot snapshot = Construct();
// ClientToServer optimization:
// for interpolated client owned identities,
// always broadcast the latest known snapshot so other clients can
// interpolate immediately instead of catching up too
// TODO dirty mask? [compression is very good w/o it already]
// each vector's component is delta compressed.
// an unchanged component would still require 1 byte.
// let's use a dirty bit mask to filter those out as well.
// initial
if (initialState)
{
// 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 this 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.
if (compressRotation)
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
else
writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale) writer.WriteVector3(snapshot.scale);
}
// delta
else
{
// int before = writer.Position;
if (syncPosition)
{
// quantize -> delta -> varint
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
else
writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale)
{
// quantize -> delta -> varint
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
}
}
// 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);
// set 'last'
last = snapshot;
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
Vector3? position = null;
Quaternion? rotation = null;
Vector3? scale = null;
// initial
if (initialState)
{
if (syncPosition) position = reader.ReadVector3();
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
else
rotation = reader.ReadQuaternion();
}
if (syncScale) scale = reader.ReadVector3();
}
// delta
else
{
// varint -> delta -> quantize
if (syncPosition)
{
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
position = Compression.ScaleToFloat(quantized, positionPrecision);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
else
rotation = reader.ReadQuaternion();
}
if (syncScale)
{
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
scale = Compression.ScaleToFloat(quantized, scalePrecision);
}
}
// 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);
// 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);
}
// sync ////////////////////////////////////////////////////////////////
// local authority client sends sync message to server for broadcasting
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// 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 sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
serverSnapshots,
connectionToClient.remoteTimeStamp,
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
GetPosition(),
GetRotation(),
GetScale());
}
// add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it.
// https://github.com/MirrorNetworking/Mirror/issues/3427
// remove this after LocalWorldState.
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// server broadcasts sync message to all clients
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// don't apply for local player with authority
if (IsClientWithAuthority) return;
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
clientSnapshots,
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
NetworkClient.sendInterval * sendIntervalMultiplier,
GetPosition(),
GetRotation(),
GetScale());
}
// add a small timeline offset to account for decoupled arrival of
// NetworkTime and NetworkTransform snapshots.
// needs to be sendInterval. half sendInterval doesn't solve it.
// https://github.com/MirrorNetworking/Mirror/issues/3427
// remove this after LocalWorldState.
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
// 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,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
position,
rotation,
scale
)
);
}
// reset state for next session.
// do not ever call this during a session (i.e. after teleport).
// calling this will break delta compression.
public override void ResetState()
{
base.ResetState();
// reset delta
lastSerializedPosition = Vector3Long.zero;
lastDeserializedPosition = Vector3Long.zero;
lastSerializedScale = Vector3Long.zero;
lastDeserializedScale = Vector3Long.zero;
// reset 'last' for delta too
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
}
}
}
@@ -1,18 +1,18 @@
fileFormatVersion: 2
guid: 8ff3ba0becae47b8b9381191598957c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs
uploadId: 736421
fileFormatVersion: 2
guid: 8ff3ba0becae47b8b9381191598957c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs
uploadId: 736421
@@ -1,462 +1,462 @@
// NetworkTransform V2 by mischa (2021-07)
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Unreliable)")]
public class NetworkTransformUnreliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
TransformSnapshot? pendingSnapshot;
[Header("Additional Settings")]
// 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;
public bool useFixedUpdate;
[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;
// Used to store last sent snapshots
protected TransformSnapshot lastSnapshot;
protected Changed cachedChangedComparison;
protected bool hasSentUnchangedPosition;
// update //////////////////////////////////////////////////////////////
// Update applies interpolation
void Update()
{
if (isServer) UpdateServerInterpolation();
// for all other clients (and for local player if !authority),
// we need to apply snapshots from the buffer.
// 'else if' because host mode shouldn't interpolate client
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
}
void FixedUpdate()
{
if (!useFixedUpdate) return;
if (pendingSnapshot.HasValue)
{
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
pendingSnapshot = null;
}
}
// LateUpdate broadcasts.
// movement scripts may change positions in Update.
// use LateUpdate to ensure changes are detected in the same frame.
// otherwise this may run before user update, delaying detection until next frame.
// this could cause visible jitter.
void LateUpdate()
{
// if server then always sync to others.
if (isServer) UpdateServerBroadcast();
// client authority, and local player (= allowed to move myself)?
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
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'
// (client with authority will drop the rpc)
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// 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.
//
// Checks to ensure server only sends snapshots if object is
// on server authority(!clientAuthority) mode because on client
// authority mode snapshots are broadcasted right after the authoritative
// client updates server in the command function(see above), OR,
// since host does not send anything to update the server, any client
// 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))
{
// 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);
RpcServerToClientSync(syncData);
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
}
}
}
void UpdateServerInterpolation()
{
// apply buffered snapshots IF client authority
// -> in server authority, server moves the object
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// 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 (serverSnapshots.Count == 0) return;
// step the transform interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
serverSnapshots,
connectionToClient.remoteTimeline,
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
if (useFixedUpdate)
pendingSnapshot = computed;
else
Apply(computed, to);
}
}
void UpdateClientBroadcast()
{
// https://github.com/vis2k/Mirror/pull/2992/
if (!NetworkClient.ready) return;
// send to server each 'sendInterval'
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// 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!
{
// 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);
}
}
}
void UpdateClientInterpolation()
{
// only while we have snapshots
if (clientSnapshots.Count == 0) return;
// step the interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
if (useFixedUpdate)
pendingSnapshot = computed;
else
Apply(computed, to);
}
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// sync target component's position on spawn.
// fixes https://github.com/vis2k/Mirror/pull/3051/
// (Spawn message wouldn't sync NTChild positions either)
if (initialState)
{
if (syncPosition) writer.WriteVector3(GetPosition());
if (syncRotation) writer.WriteQuaternion(GetRotation());
if (syncScale) writer.WriteVector3(GetScale());
}
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
// sync target component's position on spawn.
// fixes https://github.com/vis2k/Mirror/pull/3051/
// (Spawn message wouldn't sync NTChild positions either)
if (initialState)
{
if (syncPosition) SetPosition(reader.ReadVector3());
if (syncRotation) SetRotation(reader.ReadQuaternion());
if (syncScale) SetScale(reader.ReadVector3());
}
}
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());
}
}
}
}
// NetworkTransform V2 by mischa (2021-07)
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Unreliable)")]
public class NetworkTransformUnreliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
TransformSnapshot? pendingSnapshot;
[Header("Additional Settings")]
// 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;
public bool useFixedUpdate;
[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;
// Used to store last sent snapshots
protected TransformSnapshot lastSnapshot;
protected Changed cachedChangedComparison;
protected bool hasSentUnchangedPosition;
// update //////////////////////////////////////////////////////////////
// Update applies interpolation
void Update()
{
if (isServer) UpdateServerInterpolation();
// for all other clients (and for local player if !authority),
// we need to apply snapshots from the buffer.
// 'else if' because host mode shouldn't interpolate client
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
}
void FixedUpdate()
{
if (!useFixedUpdate) return;
if (pendingSnapshot.HasValue)
{
Apply(pendingSnapshot.Value, pendingSnapshot.Value);
pendingSnapshot = null;
}
}
// LateUpdate broadcasts.
// movement scripts may change positions in Update.
// use LateUpdate to ensure changes are detected in the same frame.
// otherwise this may run before user update, delaying detection until next frame.
// this could cause visible jitter.
void LateUpdate()
{
// if server then always sync to others.
if (isServer) UpdateServerBroadcast();
// client authority, and local player (= allowed to move myself)?
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
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'
// (client with authority will drop the rpc)
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// 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.
//
// Checks to ensure server only sends snapshots if object is
// on server authority(!clientAuthority) mode because on client
// authority mode snapshots are broadcasted right after the authoritative
// client updates server in the command function(see above), OR,
// since host does not send anything to update the server, any client
// 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))
{
// 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);
RpcServerToClientSync(syncData);
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
}
}
}
void UpdateServerInterpolation()
{
// apply buffered snapshots IF client authority
// -> in server authority, server moves the object
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// 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 (serverSnapshots.Count == 0) return;
// step the transform interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
serverSnapshots,
connectionToClient.remoteTimeline,
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
if (useFixedUpdate)
pendingSnapshot = computed;
else
Apply(computed, to);
}
}
void UpdateClientBroadcast()
{
// https://github.com/vis2k/Mirror/pull/2992/
if (!NetworkClient.ready) return;
// send to server each 'sendInterval'
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// 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!
{
// 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);
}
}
}
void UpdateClientInterpolation()
{
// only while we have snapshots
if (clientSnapshots.Count == 0) return;
// step the interpolation without touching time.
// NetworkClient is responsible for time globally.
SnapshotInterpolation.StepInterpolation(
clientSnapshots,
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
out TransformSnapshot from,
out TransformSnapshot to,
out double t);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
if (useFixedUpdate)
pendingSnapshot = computed;
else
Apply(computed, to);
}
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// sync target component's position on spawn.
// fixes https://github.com/vis2k/Mirror/pull/3051/
// (Spawn message wouldn't sync NTChild positions either)
if (initialState)
{
if (syncPosition) writer.WriteVector3(GetPosition());
if (syncRotation) writer.WriteQuaternion(GetRotation());
if (syncScale) writer.WriteVector3(GetScale());
}
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
// sync target component's position on spawn.
// fixes https://github.com/vis2k/Mirror/pull/3051/
// (Spawn message wouldn't sync NTChild positions either)
if (initialState)
{
if (syncPosition) SetPosition(reader.ReadVector3());
if (syncRotation) SetRotation(reader.ReadQuaternion());
if (syncScale) SetScale(reader.ReadVector3());
}
}
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());
}
}
}
}
@@ -1,18 +1,18 @@
fileFormatVersion: 2
guid: a553cb17010b2403e8523b558bffbc14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs
uploadId: 736421
fileFormatVersion: 2
guid: a553cb17010b2403e8523b558bffbc14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs
uploadId: 736421
@@ -1,68 +1,68 @@
// snapshot for snapshot interpolation
// https://gafferongames.com/post/snapshot_interpolation/
// position, rotation, scale for compatibility for now.
using UnityEngine;
namespace Mirror
{
// NetworkTransform Snapshot
public struct TransformSnapshot : Snapshot
{
// time or sequence are needed to throw away older snapshots.
//
// glenn fiedler starts with a 16 bit sequence number.
// supposedly this is meant as a simplified example.
// in the end we need the remote timestamp for accurate interpolation
// and buffering over time.
//
// note: in theory, IF server sends exactly(!) at the same interval then
// the 16 bit ushort timestamp would be enough to calculate the
// remote time (sequence * sendInterval). but Unity's update is
// not guaranteed to run on the exact intervals / do catchup.
// => remote timestamp is better for now
//
// [REMOTE TIME, NOT LOCAL TIME]
// => DOUBLE for long term accuracy & batching gives us double anyway
public double remoteTime { get; set; }
// the local timestamp (when we received it)
// used to know if the first two snapshots are old enough to start.
public double localTime { get; set; }
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
{
this.remoteTime = remoteTime;
this.localTime = localTime;
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t)
{
// NOTE:
// Vector3 & Quaternion components are float anyway, so we can
// keep using the functions with 't' as float instead of double.
return new TransformSnapshot(
// interpolated snapshot is applied directly. don't need timestamps.
0, 0,
// lerp position/rotation/scale unclamped in case we ever need
// to extrapolate. atm SnapshotInterpolation never does.
Vector3.LerpUnclamped(from.position, to.position, (float)t),
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
// (0, 90, 1.5) is even worse. for Lerp.
// => Slerp works way better for our euler angles.
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
);
}
public override string ToString() =>
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
}
}
// snapshot for snapshot interpolation
// https://gafferongames.com/post/snapshot_interpolation/
// position, rotation, scale for compatibility for now.
using UnityEngine;
namespace Mirror
{
// NetworkTransform Snapshot
public struct TransformSnapshot : Snapshot
{
// time or sequence are needed to throw away older snapshots.
//
// glenn fiedler starts with a 16 bit sequence number.
// supposedly this is meant as a simplified example.
// in the end we need the remote timestamp for accurate interpolation
// and buffering over time.
//
// note: in theory, IF server sends exactly(!) at the same interval then
// the 16 bit ushort timestamp would be enough to calculate the
// remote time (sequence * sendInterval). but Unity's update is
// not guaranteed to run on the exact intervals / do catchup.
// => remote timestamp is better for now
//
// [REMOTE TIME, NOT LOCAL TIME]
// => DOUBLE for long term accuracy & batching gives us double anyway
public double remoteTime { get; set; }
// the local timestamp (when we received it)
// used to know if the first two snapshots are old enough to start.
public double localTime { get; set; }
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
{
this.remoteTime = remoteTime;
this.localTime = localTime;
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t)
{
// NOTE:
// Vector3 & Quaternion components are float anyway, so we can
// keep using the functions with 't' as float instead of double.
return new TransformSnapshot(
// interpolated snapshot is applied directly. don't need timestamps.
0, 0,
// lerp position/rotation/scale unclamped in case we ever need
// to extrapolate. atm SnapshotInterpolation never does.
Vector3.LerpUnclamped(from.position, to.position, (float)t),
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
// (0, 90, 1.5) is even worse. for Lerp.
// => Slerp works way better for our euler angles.
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
);
}
public override string ToString() =>
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
}
}
@@ -1,18 +1,18 @@
fileFormatVersion: 2
guid: d3dae77b43dc4e1dbb2012924b2da79c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs
uploadId: 736421
fileFormatVersion: 2
guid: d3dae77b43dc4e1dbb2012924b2da79c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs
uploadId: 736421
@@ -1,156 +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;
}
}
}
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;
}
}
}
@@ -1,18 +1,18 @@
fileFormatVersion: 2
guid: a1c0832ca88e749ff96fe04cebb617ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
uploadId: 736421
fileFormatVersion: 2
guid: a1c0832ca88e749ff96fe04cebb617ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
uploadId: 736421