From 195c288e140387c84773c77c84cd2385262a8aae Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 20:19:05 +0300 Subject: [PATCH 1/9] feat: first part of QOLs --- .../Runtime/CodeGenerating/Attributes.cs | 2 +- .../Connection/NetworkConnection.QOL.cs | 8 +- .../NetworkAnimator/NetworkAnimator.cs | 93 +- .../NetworkTransform/NetworkTransform.cs | 922 ++++++++++-------- .../Dependencies/Utilities/Dictionaries.cs | 10 +- .../Runtime/Utility/Extension/Transforms.cs | 332 ++++++- 6 files changed, 896 insertions(+), 471 deletions(-) diff --git a/Assets/FishNet/Runtime/CodeGenerating/Attributes.cs b/Assets/FishNet/Runtime/CodeGenerating/Attributes.cs index 4eae338d..bfa4f8a2 100644 --- a/Assets/FishNet/Runtime/CodeGenerating/Attributes.cs +++ b/Assets/FishNet/Runtime/CodeGenerating/Attributes.cs @@ -30,7 +30,7 @@ public class NotSerializerAttribute : Attribute { } /// /// Method or type will be made public by codegen. /// - internal class MakePublicAttribute : Attribute { } + public class MakePublicAttribute : Attribute { } /// /// Method is a comparer for a value type. diff --git a/Assets/FishNet/Runtime/Connection/NetworkConnection.QOL.cs b/Assets/FishNet/Runtime/Connection/NetworkConnection.QOL.cs index 43c45e12..0c63fa69 100644 --- a/Assets/FishNet/Runtime/Connection/NetworkConnection.QOL.cs +++ b/Assets/FishNet/Runtime/Connection/NetworkConnection.QOL.cs @@ -43,10 +43,10 @@ public string GetAddress() /// Reason client is being kicked. /// How to print logging as. /// Optional message to be debug logged. - public void Kick(KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "") + public void Kick(KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "", bool immediately = true) { if (CanKick()) - NetworkManager.ServerManager.Kick(this, kickReason, loggingType, log); + NetworkManager.ServerManager.Kick(this, kickReason, loggingType, log, immediately); } /// @@ -56,10 +56,10 @@ public void Kick(KickReason kickReason, LoggingType loggingType = LoggingType.Co /// Reason client is being kicked. /// How to print logging as. /// Optional message to be debug logged. - public void Kick(Reader reader, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "") + public void Kick(Reader reader, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "", bool immediately = true) { if (CanKick()) - NetworkManager.ServerManager.Kick(this, reader, kickReason, loggingType, log); + NetworkManager.ServerManager.Kick(this, reader, kickReason, loggingType, log, immediately); } private bool CanKick() diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs index 4c312763..71670295 100644 --- a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs +++ b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs @@ -15,8 +15,9 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using FishNet.Managing; -using Unity.Profiling; using UnityEngine; +using UnityEngine.Profiling; +using Unity.Profiling; using TimeManagerCls = FishNet.Managing.Timing.TimeManager; namespace FishNet.Component.Animating @@ -33,6 +34,7 @@ private struct ReceivedServerData /// /// Gets an Arraysegment of received data. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySegment GetArraySegment() => new(_data, 0, _length); /// @@ -51,6 +53,7 @@ public ReceivedServerData(ArraySegment segment) Buffer.BlockCopy(segment.Array, segment.Offset, _data, 0, _length); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { if (_data != null) @@ -210,6 +213,7 @@ public void GetBuffer(int index, ref byte[] buffer, ref int length) /// /// Resets buffers. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { BufferCount = 0; @@ -274,6 +278,23 @@ public ParameterDetail(AnimatorControllerParameter controllerParameter, byte typ } #endregion + #region Private + + #region Private Profiler Markers + + private static readonly ProfilerMarker _pm_OnPreTick = new ProfilerMarker("NetworkAnimator.TimeManager_OnPreTick()"); + private static readonly ProfilerMarker _pm_OnPostTick = new ProfilerMarker("NetworkAnimator.TimeManager_OnPostTick()"); + private static readonly ProfilerMarker _pm_OnUpdate = new ProfilerMarker("NetworkAnimator.TimeManager_OnUpdate()"); + private static readonly ProfilerMarker _pm_CheckSendToServer = new ProfilerMarker("NetworkAnimator.CheckSendToServer()"); + private static readonly ProfilerMarker _pm_CheckSendToClients = new ProfilerMarker("NetworkAnimator.CheckSendToClients()"); + private static readonly ProfilerMarker _pm_SmoothFloats = new ProfilerMarker("NetworkAnimator.SmoothFloats()"); + private static readonly ProfilerMarker _pm_AnimatorUpdated = new ProfilerMarker("NetworkAnimator.AnimatorUpdated(ref ArraySegment, bool)"); + private static readonly ProfilerMarker _pm_ApplyParametersUpdated = new ProfilerMarker("NetworkAnimator.ApplyParametersUpdated(ref ArraySegment)"); + + #endregion + + #endregion + #region Public. /// /// Parameters which will not be synchronized. @@ -304,6 +325,14 @@ public Animator Animator [SerializeField] private bool _synchronizeWhenDisabled; /// + /// True to synchronize changes even when the animator component is disabled. + /// + public bool SynchronizeWhenDisabled + { + get { return _synchronizeWhenDisabled; } + set { _synchronizeWhenDisabled = value; } + } + /// /// True to smooth float value changes for spectators. /// [Tooltip("True to smooth float value changes for spectators.")] @@ -334,6 +363,11 @@ public bool ClientAuthoritative [Tooltip("True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations.")] [SerializeField] private bool _sendToOwner; + /// + /// True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations. + /// + public bool SendToOwner => _sendToOwner; + #endregion #region Private. @@ -370,12 +404,19 @@ public bool ClientAuthoritative // /// // private List _toClientsBuffer = new(); /// + /// Synchronization enabled state. True by default + /// + private bool _isSynchronizationEnabled = true; + /// /// Returns if the animator is exist and can be synchronized. /// private bool _canSynchronizeAnimator { get { + if (!_isSynchronizationEnabled) + return false; + if (!_isAnimatorSet) return false; @@ -459,17 +500,6 @@ private bool _canSmoothFloats private bool _subscribedToTicks; #endregion - #region Private Profiler Markers - private static readonly ProfilerMarker _pm_OnPreTick = new("NetworkAnimator.TimeManager_OnPreTick()"); - private static readonly ProfilerMarker _pm_OnPostTick = new("NetworkAnimator.TimeManager_OnPostTick()"); - private static readonly ProfilerMarker _pm_OnUpdate = new("NetworkAnimator.TimeManager_OnUpdate()"); - private static readonly ProfilerMarker _pm_CheckSendToServer = new("NetworkAnimator.CheckSendToServer()"); - private static readonly ProfilerMarker _pm_CheckSendToClients = new("NetworkAnimator.CheckSendToClients()"); - private static readonly ProfilerMarker _pm_SmoothFloats = new("NetworkAnimator.SmoothFloats()"); - private static readonly ProfilerMarker _pm_AnimatorUpdated = new("NetworkAnimator.AnimatorUpdated(ref ArraySegment, bool)"); - private static readonly ProfilerMarker _pm_ApplyParametersUpdated = new("NetworkAnimator.ApplyParametersUpdated(ref ArraySegment)"); - #endregion - #region Const. ///// ///// How much time to fall behind when using smoothing. Only increase value if the smoothing is sometimes jittery. Recommended values are between 0 and 0.04. @@ -515,6 +545,7 @@ public override void OnSpawnServer(NetworkConnection connection) public override void OnStartNetwork() { ChangeTickSubscription(true); + _isSynchronizationEnabled = true; } [APIExclude] @@ -584,6 +615,7 @@ private void TimeManager_OnPreTick() _fromServerBuffer.Clear(); return; } + //Disabled/cannot start. if (_startTick == 0) return; @@ -593,6 +625,7 @@ private void TimeManager_OnPreTick() _startTick = 0; return; } + //Not enough time has passed to start queue. if (TimeManager.LocalTick < _startTick) return; @@ -645,7 +678,7 @@ private void InitializeOnce() //Don't run the rest if not in play mode. if (!ApplicationState.IsPlaying()) return; - + if (!_canSynchronizeAnimator) { //Debug.LogWarning("Animator is null or not enabled; unable to initialize for animator. Use SetAnimator if animator was changed or enable the animator."); @@ -668,6 +701,12 @@ private void InitializeOnce() foreach (AnimatorControllerParameter item in _animator.parameters) { bool process = !_animator.IsParameterControlledByCurve(item.name); + //PROSTART + /* This is done in a weird way for processing + * to work with the pro tool stripper. */ + if (IgnoredParameters.Contains(item.name)) + process = false; + //PROEND if (process) { //Over 250 parameters; who would do this!? @@ -708,6 +747,15 @@ private void InitializeOnce() } } } + + /// + /// Sets synchronization state to NetworkAnimator. Enabled by default. + /// + /// + public void SetSynchronizationState(bool state) + { + _isSynchronizationEnabled = state; + } /// /// Sets which animator to use. You must call this with the appropriate animator on all clients and server. This change is not automatically synchronized. @@ -846,6 +894,7 @@ private void CheckSendToClients() SendSegment(new(buffer, 0, bufferLength)); } + //Reset client auth buffer. _clientAuthoritativeUpdates.Reset(); } @@ -978,6 +1027,7 @@ private bool AnimatorUpdated(out ArraySegment updatedBytes, bool forceAll _writer.WriteUInt8Unpacked(_triggerUpdates[i].ParameterIndex); _writer.WriteBoolean(_triggerUpdates[i].Setting); } + _triggerUpdates.Clear(); /* States. */ @@ -1069,7 +1119,7 @@ private bool AnimatorUpdated(out ArraySegment updatedBytes, bool forceAll //Nothing to update. if (_writer.Position == 0) return false; - + updatedBytes = _writer.GetArraySegment(); return true; } @@ -1225,6 +1275,7 @@ private bool ReturnCurrentLayerState(out int stateHash, out float normalizedTime /// Immediately sends all variables and states of layers. /// This is a very bandwidth intensive operation. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SendAll() { _forceAllOnTimed = true; @@ -1234,6 +1285,7 @@ public void SendAll() /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Play(string name) { Play(Animator.StringToHash(name)); @@ -1242,6 +1294,7 @@ public void Play(string name) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Play(int hash) { for (int i = 0; i < _animator.layerCount; i++) @@ -1251,6 +1304,7 @@ public void Play(int hash) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Play(string name, int layer) { Play(Animator.StringToHash(name), layer); @@ -1259,6 +1313,7 @@ public void Play(string name, int layer) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Play(int hash, int layer) { Play(hash, layer, 0f); @@ -1267,6 +1322,7 @@ public void Play(int hash, int layer) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Play(string name, int layer, float normalizedTime) { Play(Animator.StringToHash(name), layer, normalizedTime); @@ -1289,6 +1345,7 @@ public void Play(int hash, int layer, float normalizedTime) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PlayInFixedTime(string name, float fixedTime) { PlayInFixedTime(Animator.StringToHash(name), fixedTime); @@ -1297,6 +1354,7 @@ public void PlayInFixedTime(string name, float fixedTime) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PlayInFixedTime(int hash, float fixedTime) { for (int i = 0; i < _animator.layerCount; i++) @@ -1306,6 +1364,7 @@ public void PlayInFixedTime(int hash, float fixedTime) /// /// Plays a state. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PlayInFixedTime(string name, int layer, float fixedTime) { PlayInFixedTime(Animator.StringToHash(name), layer, fixedTime); @@ -1335,6 +1394,7 @@ public void PlayInFixedTime(int hash, int layer, float fixedTime) /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CrossFade(string stateName, float normalizedTransitionDuration, int layer, float normalizedTimeOffset = float.NegativeInfinity, float normalizedTransitionTime = 0.0f) { CrossFade(Animator.StringToHash(stateName), normalizedTransitionDuration, layer, normalizedTimeOffset, normalizedTransitionTime); @@ -1367,6 +1427,7 @@ public void CrossFade(int hash, float normalizedTransitionDuration, int layer, f /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CrossFadeInFixedTime(string stateName, float fixedTransitionDuration, int layer, float fixedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) { CrossFadeInFixedTime(Animator.StringToHash(stateName), fixedTransitionDuration, layer, fixedTimeOffset, normalizedTransitionTime); @@ -1397,6 +1458,7 @@ public void CrossFadeInFixedTime(int hash, float fixedTransitionDuration, int la /// Sets a trigger on the animator and sends it over the network. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetTrigger(int hash) { if (!_canSynchronizeAnimator) @@ -1408,6 +1470,7 @@ public void SetTrigger(int hash) /// Sets a trigger on the animator and sends it over the network. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetTrigger(string name) { SetTrigger(Animator.StringToHash(name)); @@ -1417,6 +1480,7 @@ public void SetTrigger(string name) /// Resets a trigger on the animator and sends it over the network. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetTrigger(int hash) { UpdateTrigger(hash, false); @@ -1426,6 +1490,7 @@ public void ResetTrigger(int hash) /// Resets a trigger on the animator and sends it over the network. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetTrigger(string name) { ResetTrigger(Animator.StringToHash(name)); diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs b/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs index 5e8daa71..1f2de518 100644 --- a/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs +++ b/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs @@ -12,9 +12,10 @@ using GameKit.Dependencies.Utilities; using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using FishNet.Managing.Timing; -using Unity.Profiling; using UnityEngine; +using Unity.Profiling; using UnityEngine.Scripting; using static FishNet.Object.NetworkObject; @@ -74,12 +75,14 @@ public void Update(ArraySegment data, Channel channel, bool updateHasData, /// /// Will cause this data to send on the reliable channel once even if data is unchanged. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SendReliably() { HasData = true; Channel = Channel.Reliable; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetState() { HasData = false; @@ -159,6 +162,7 @@ public class GoalData : IResettable [Preserve] public GoalData() { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetState() { ReceivedTick = 0; @@ -166,6 +170,7 @@ public void ResetState() Rates.ResetState(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InitializeState() { } } @@ -200,6 +205,7 @@ public class RateData : IResettable [Preserve] public RateData() { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(RateData rd) { Update(rd.Position, rd.Rotation, rd.Scale, rd.LastUnalteredPositionRate, rd.TickSpan, rd.TimeRemaining); @@ -208,6 +214,7 @@ public void Update(RateData rd) /// /// Updates rates. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(float position, float rotation, float scale, float unalteredPositionRate, uint tickSpan, float timeRemaining) { Position = position; @@ -218,6 +225,7 @@ public void Update(float position, float rotation, float scale, float unalteredP TimeRemaining = timeRemaining; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetState() { Position = 0f; @@ -228,6 +236,7 @@ public void ResetState() TimeRemaining = 0f; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InitializeState() { } } @@ -282,13 +291,16 @@ public enum ExtrapolateState : byte [Preserve] public TransformData() { } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SetIsDefaultToFalse() => IsDefault = false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Update(TransformData copy) { Update(copy.Tick, copy.Position, copy.Rotation, copy.Scale, copy.ExtrapolatedPosition, copy.ParentBehaviour); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Update(uint tick, Vector3 position, Quaternion rotation, Vector3 scale, Vector3 extrapolatedPosition, NetworkBehaviour parentBehaviour) { IsDefault = false; @@ -300,6 +312,7 @@ internal void Update(uint tick, Vector3 position, Quaternion rotation, Vector3 s ParentBehaviour = parentBehaviour; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetState() { IsDefault = true; @@ -313,6 +326,7 @@ public void ResetState() ParentBehaviour = null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void InitializeState() { } } #endregion @@ -371,13 +385,13 @@ public void InitializeState() { } Rotation = AutoPackType.Packed, Scale = AutoPackType.Unpacked }; - /// /// True to use scaled deltaTime when smoothing. /// [Tooltip("True to use scaled deltaTime when smoothing.")] [SerializeField] private bool _useScaledTime = true; /// + /// /// How many ticks to interpolate. /// [Tooltip("How many ticks to interpolate.")] @@ -536,6 +550,23 @@ public void SetSendToOwner(bool value) #endregion #region Private. + + #region Private Profiler Markers + + private static readonly ProfilerMarker _pm_OnUpdate = new ProfilerMarker("NetworkTransform.TimeManager_OnUpdate()"); + private static readonly ProfilerMarker _pm_OnPostTick = new ProfilerMarker("NetworkTransform.TimeManager_OnPostTick()"); + private static readonly ProfilerMarker _pm_MoveToTarget = new ProfilerMarker("NetworkTransform.MoveToTarget(float)"); + private static readonly ProfilerMarker _pm_UpdateTransformData = new ProfilerMarker("NetworkTransform.UpdateTransformData(ArraySegment, TransformData, TransformData, ref ChangedFull)"); + private static readonly ProfilerMarker _pm_ForceSend0 = new ProfilerMarker("NetworkTransform.ForceSend()"); + private static readonly ProfilerMarker _pm_ForceSend1 = new ProfilerMarker("NetworkTransform.ForceSend(uint)"); + private static readonly ProfilerMarker _pm_SendToClients = new ProfilerMarker("NetworkTransform.SendToClients()"); + private static readonly ProfilerMarker _pm_SendToServer = new ProfilerMarker("NetworkTransform.SendToServer(TransformData)"); + private static readonly ProfilerMarker _pm_GetChanged = new ProfilerMarker("NetworkTransform.GetChanged(Vector3, Quaternion, Vector3, NetworkBehaviour)"); + private static readonly ProfilerMarker _pm_SerializeChanged = new ProfilerMarker("NetworkTransform.SerializeChanged(ChangedDelta, PooledWriter, TransformData)"); + private static readonly ProfilerMarker _pm_DataReceived = new ProfilerMarker("NetworkTransform.DataReceived(ArraySegment, Channel, bool)"); + + #endregion + /// /// Packing data with all values set to uncompressed. /// @@ -646,19 +677,6 @@ public void SetSendToOwner(bool value) /// private TimeManager _timeManager; #endregion - - #region Private Profiler Markers - - private static readonly ProfilerMarker _pm_OnUpdate = new("NetworkTransform.TimeManager_OnUpdate()"); - private static readonly ProfilerMarker _pm_OnPostTick = new("NetworkTransform.TimeManager_OnPostTick()"); - private static readonly ProfilerMarker _pm_MoveToTarget = new("NetworkTransform.MoveToTarget(float)"); - private static readonly ProfilerMarker _pm_UpdateTransformData = new("NetworkTransform.UpdateTransformData(ArraySegment, TransformData, TransformData, ref ChangedFull)"); - private static readonly ProfilerMarker _pm_ForceSend0 = new("NetworkTransform.ForceSend()"); - private static readonly ProfilerMarker _pm_ForceSend1 = new("NetworkTransform.ForceSend(uint)"); - private static readonly ProfilerMarker _pm_SendToClients = new("NetworkTransform.SendToClients()"); - private static readonly ProfilerMarker _pm_SendToServer = new("NetworkTransform.SendToServer(TransformData)"); - - #endregion #region Const. /// @@ -779,7 +797,6 @@ private void TryClearGoalDatas_OwnershipChange(NetworkConnection prevOwner, bool * follow the queue. */ } - private void TimeManager_OnUpdate() { using (_pm_OnUpdate.Auto()) @@ -1026,6 +1043,7 @@ public void SetExtrapolation(ushort value) /// Returns if controlling logic can be run. This may be the server when there is no owner, even if client authoritative, and more. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool CanControl() { //Client auth. @@ -1044,6 +1062,7 @@ private bool CanControl() /// /// When called by the controller of this object the next changed data will be teleported to by spectators. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Teleport() { if (CanControl()) @@ -1063,9 +1082,12 @@ private void ObserversSetSendToOwner(bool value) /// /// Resets last sent information to force a resend of current values after a number of ticks. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ForceSend(uint ticks) { +#if UNITY_EDITOR || DEVELOPMENT_BUILD using (_pm_ForceSend1.Auto()) +#endif { /* If there is a pending delayed force send then queue it * immediately and set a new delay tick. */ @@ -1078,6 +1100,7 @@ public void ForceSend(uint ticks) /// /// Resets last sent information to force a resend of current values. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ForceSend() { using (_pm_ForceSend0.Auto()) @@ -1109,6 +1132,7 @@ public void SetInterval(byte value) /// Updates the interval value. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetIntervalInternal(byte value) { value = (byte)Mathf.Max(value, 1); @@ -1165,17 +1189,19 @@ private void SetDefaultGoalData() } _teleport = false; - SetLastReceived(_lastReceivedServerTransformData); - SetLastReceived(_lastReceivedClientTransformData); + SetLastReceived(t, _lastReceivedServerTransformData, parentBehaviour); + SetLastReceived(t, _lastReceivedClientTransformData, parentBehaviour); //SetInstantRates(_currentGoalData.Rates, 0, -1f); - void SetLastReceived(TransformData td) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void SetLastReceived(Transform t, TransformData td, NetworkBehaviour parentBehaviour) { //Could be null if not initialized due to server or client side not being used. if (td == null) return; - td.Update(0, t.localPosition, t.localRotation, t.localScale, t.localPosition, parentBehaviour); + t.GetLocalPositionAndRotation(out var localPosition, out var localRotation); + td.Update(0, localPosition, localRotation, t.localScale, localPosition, parentBehaviour); } } @@ -1192,220 +1218,226 @@ private void LogInvalidParent() /// private void SerializeChanged(ChangedDelta changed, PooledWriter writer, TransformData dataToUpdate = null) { - bool canUpdateData = dataToUpdate != null; - if (canUpdateData && changed != ChangedDelta.Unset) - dataToUpdate.SetIsDefaultToFalse(); - - UpdateFlagA flagsA = UpdateFlagA.Unset; - UpdateFlagB flagsB = UpdateFlagB.Unset; - /* Do not use compression when childed. Depending - * on the scale of the parent compression may - * not be accurate enough. */ - TransformPackingData packing = ChangedContains(changed, ChangedDelta.Nested) ? _unpacked : _packing; - - int startIndexA = writer.Position; - writer.Skip(1); - //Original axis value. - float original; - //Compressed axis value. - float compressed; - //Multiplier for compression. - float multiplier = 100f; - /* Maximum value compressed may be - * to send as compressed. */ - float maxValue = short.MaxValue - 1; - - Transform t = _cachedTransform; - /* Position. */ - if (_synchronizePosition) + using (_pm_SerializeChanged.Auto()) { - AutoPackType localPacking = packing.Position; - //PositionX - if (ChangedContains(changed, ChangedDelta.PositionX)) - { - original = t.localPosition.x; + bool canUpdateData = dataToUpdate != null; + if (canUpdateData && changed != ChangedDelta.Unset) + dataToUpdate.SetIsDefaultToFalse(); - if (canUpdateData) - dataToUpdate.Position.x = original; - - compressed = original * multiplier; - if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) - { - flagsA |= UpdateFlagA.X2; - writer.WriteInt16((short)compressed); - } - else - { - flagsA |= UpdateFlagA.X4; - writer.WriteSingle(original); - } - } - - //PositionY - if (ChangedContains(changed, ChangedDelta.PositionY)) - { - original = t.localPosition.y; + UpdateFlagA flagsA = UpdateFlagA.Unset; + UpdateFlagB flagsB = UpdateFlagB.Unset; + /* Do not use compression when childed. Depending + * on the scale of the parent compression may + * not be accurate enough. */ + TransformPackingData packing = ChangedContains(changed, ChangedDelta.Nested) ? _unpacked : _packing; - if (canUpdateData) - dataToUpdate.Position.y = original; - - compressed = original * multiplier; - if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) - { - flagsA |= UpdateFlagA.Y2; - writer.WriteInt16((short)compressed); - } - else - { - flagsA |= UpdateFlagA.Y4; - writer.WriteSingle(original); - } - } - - //PositionZ - if (ChangedContains(changed, ChangedDelta.PositionZ)) - { - original = t.localPosition.z; - - if (canUpdateData) - dataToUpdate.Position.z = original; - - compressed = original * multiplier; - if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) - { - flagsA |= UpdateFlagA.Z2; - writer.WriteInt16((short)compressed); - } - else - { - flagsA |= UpdateFlagA.Z4; - writer.WriteSingle(original); - } - } - } - - /* Rotation. */ - if (_synchronizeRotation) - { - if (ChangedContains(changed, ChangedDelta.Rotation)) - { - if (canUpdateData) - dataToUpdate.Rotation = t.localRotation; - - flagsA |= UpdateFlagA.Rotation; - /* Rotation can always use pack settings even - * if childed. Unsual transform scale shouldn't affect rotation. */ - writer.WriteQuaternion(t.localRotation, _packing.Rotation); - } - } - - /* If there is a teleport pending then apply - * extended flag since thats where teleport resides. */ - bool teleport = _teleport; - if (teleport) - changed |= ChangedDelta.Extended; - - if (ChangedContains(changed, ChangedDelta.Extended)) - { - AutoPackType localPacking = packing.Scale; - flagsA |= UpdateFlagA.Extended; - int startIndexB = writer.Position; + int startIndexA = writer.Position; writer.Skip(1); + //Original axis value. + float original; + //Compressed axis value. + float compressed; + //Multiplier for compression. + float multiplier = 100f; + /* Maximum value compressed may be + * to send as compressed. */ + float maxValue = short.MaxValue - 1; - /* Redundant to do the teleport check here since it was done - * just above, but for code consistency the teleport updateflag - * is set within this conditional with rest of the extended - * data. */ - if (teleport) - { - flagsB |= UpdateFlagB.Teleport; - _teleport = false; - } - - /* Scale. */ - if (_synchronizeScale) + Transform t = _cachedTransform; + t.GetLocalPositionAndRotation(out Vector3 localPosition, out Quaternion localRotation); + Vector3 localScale = t.localScale; + + /* Position. */ + if (_synchronizePosition) { - //ScaleX - if (ChangedContains(changed, ChangedDelta.ScaleX)) + AutoPackType localPacking = packing.Position; + //PositionX + if (ChangedContains(changed, ChangedDelta.PositionX)) { - original = t.localScale.x; + original = localPosition.x; if (canUpdateData) - dataToUpdate.Scale.x = original; + dataToUpdate.Position.x = original; compressed = original * multiplier; if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) { - flagsB |= UpdateFlagB.X2; + flagsA |= UpdateFlagA.X2; writer.WriteInt16((short)compressed); } else { - flagsB |= UpdateFlagB.X4; + flagsA |= UpdateFlagA.X4; writer.WriteSingle(original); } } - //ScaleY - if (ChangedContains(changed, ChangedDelta.ScaleY)) + //PositionY + if (ChangedContains(changed, ChangedDelta.PositionY)) { - original = t.localScale.y; + original = localPosition.y; if (canUpdateData) - dataToUpdate.Scale.y = original; + dataToUpdate.Position.y = original; compressed = original * multiplier; if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) { - flagsB |= UpdateFlagB.Y2; + flagsA |= UpdateFlagA.Y2; writer.WriteInt16((short)compressed); } else { - flagsB |= UpdateFlagB.Y4; + flagsA |= UpdateFlagA.Y4; writer.WriteSingle(original); } } - //ScaleZ - if (ChangedContains(changed, ChangedDelta.ScaleZ)) + //PositionZ + if (ChangedContains(changed, ChangedDelta.PositionZ)) { - original = t.localScale.z; + original = localPosition.z; if (canUpdateData) - dataToUpdate.Scale.z = original; + dataToUpdate.Position.z = original; compressed = original * multiplier; if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) { - flagsB |= UpdateFlagB.Z2; + flagsA |= UpdateFlagA.Z2; writer.WriteInt16((short)compressed); } else { - flagsB |= UpdateFlagB.Z4; + flagsA |= UpdateFlagA.Z4; writer.WriteSingle(original); } } } - //Childed. - if (ChangedContains(changed, ChangedDelta.Nested) && ParentBehaviour != null) + /* Rotation. */ + if (_synchronizeRotation) { - if (canUpdateData) - dataToUpdate.ParentBehaviour = ParentBehaviour; + if (ChangedContains(changed, ChangedDelta.Rotation)) + { + if (canUpdateData) + dataToUpdate.Rotation = localRotation; - flagsB |= UpdateFlagB.Child; - writer.WriteNetworkBehaviour(ParentBehaviour); + flagsA |= UpdateFlagA.Rotation; + /* Rotation can always use pack settings even + * if childed. Unsual transform scale shouldn't affect rotation. */ + writer.WriteQuaternion(localRotation, _packing.Rotation); + } } - writer.InsertUInt8Unpacked((byte)flagsB, startIndexB); - } + /* If there is a teleport pending then apply + * extended flag since thats where teleport resides. */ + bool teleport = _teleport; + if (teleport) + changed |= ChangedDelta.Extended; + + if (ChangedContains(changed, ChangedDelta.Extended)) + { + AutoPackType localPacking = packing.Scale; + flagsA |= UpdateFlagA.Extended; + int startIndexB = writer.Position; + writer.Skip(1); + + /* Redundant to do the teleport check here since it was done + * just above, but for code consistency the teleport updateflag + * is set within this conditional with rest of the extended + * data. */ + if (teleport) + { + flagsB |= UpdateFlagB.Teleport; + _teleport = false; + } + + /* Scale. */ + if (_synchronizeScale) + { + //ScaleX + if (ChangedContains(changed, ChangedDelta.ScaleX)) + { + original = localScale.x; + + if (canUpdateData) + dataToUpdate.Scale.x = original; + + compressed = original * multiplier; + if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) + { + flagsB |= UpdateFlagB.X2; + writer.WriteInt16((short)compressed); + } + else + { + flagsB |= UpdateFlagB.X4; + writer.WriteSingle(original); + } + } + + //ScaleY + if (ChangedContains(changed, ChangedDelta.ScaleY)) + { + original = localScale.y; + + if (canUpdateData) + dataToUpdate.Scale.y = original; + + compressed = original * multiplier; + if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) + { + flagsB |= UpdateFlagB.Y2; + writer.WriteInt16((short)compressed); + } + else + { + flagsB |= UpdateFlagB.Y4; + writer.WriteSingle(original); + } + } + + //ScaleZ + if (ChangedContains(changed, ChangedDelta.ScaleZ)) + { + original = localScale.z; + + if (canUpdateData) + dataToUpdate.Scale.z = original; - //Insert flags. - writer.InsertUInt8Unpacked((byte)flagsA, startIndexA); + compressed = original * multiplier; + if (localPacking != AutoPackType.Unpacked && Math.Abs(compressed) <= maxValue) + { + flagsB |= UpdateFlagB.Z2; + writer.WriteInt16((short)compressed); + } + else + { + flagsB |= UpdateFlagB.Z4; + writer.WriteSingle(original); + } + } + } + + //Childed. + if (ChangedContains(changed, ChangedDelta.Nested) && ParentBehaviour != null) + { + if (canUpdateData) + dataToUpdate.ParentBehaviour = ParentBehaviour; - bool ChangedContains(ChangedDelta whole, ChangedDelta part) + flagsB |= UpdateFlagB.Child; + writer.WriteNetworkBehaviour(ParentBehaviour); + } + + writer.InsertUInt8Unpacked((byte)flagsB, startIndexB); + } + + //Insert flags. + writer.InsertUInt8Unpacked((byte)flagsA, startIndexA); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool ChangedContains(ChangedDelta whole, ChangedDelta part) { return (whole & part) == part; } @@ -1501,29 +1533,32 @@ private void DeserializePacket(ArraySegment data, TransformData prevTransf } else { - Unnest(); + Unnest(nextTransformData); } } //No extended settings. else { nextTransformData.Scale = prevTransformData.Scale; - Unnest(); + Unnest(nextTransformData); } - void Unnest() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void Unnest(TransformData nextTransformData) { nextTransformData.ParentBehaviour = null; } //Returns if whole contains part. - bool UpdateFlagAContains(UpdateFlagA whole, UpdateFlagA part) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool UpdateFlagAContains(UpdateFlagA whole, UpdateFlagA part) { return (whole & part) == part; } //Returns if whole contains part. - bool UpdateFlagBContains(UpdateFlagB whole, UpdateFlagB part) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool UpdateFlagBContains(UpdateFlagB whole, UpdateFlagB part) { return (whole & part) == part; } @@ -1614,118 +1649,147 @@ private void MoveToTarget(float delta) { using (_pm_MoveToTarget.Auto()) { - if (_currentGoalData == null) - return; - - //Cannot move if neither is active. - if (!IsServerInitialized && !IsClientInitialized) - return; - - //If client auth and the owner don't move towards target. - if (_clientAuthoritative) - { - if (IsOwner || TakenOwnership) + if (_currentGoalData == null) return; - } - else - { - //If not client authoritative, is owner, and don't sync to owner. - if (IsOwner && !_sendToOwner) - return; - } - - //True if not client controlled. - bool controlledByClient = _clientAuthoritative && Owner.IsActive; - //If not controlled by client and is server then no reason to move. - if (!controlledByClient && IsServerInitialized) - return; - /* Once here it's safe to assume the object will be moving. - * Any checks which would stop it from moving be it client - * auth and owner, or server controlled and server, ect, - * would have already been run. */ - TransformData td = _currentGoalData.Transforms; - RateData rd = _currentGoalData.Rates; - - //Set parent. - if (_synchronizeParent) - SetParent(td.ParentBehaviour, rd); - - float multiplier = 1f; - int queueCount = _goalDataQueue.Count; - //Increase move rate slightly if over queue count. - if (queueCount > _interpolation + 1) - multiplier += 0.05f; + //Cannot move if neither is active. + if (!IsServerInitialized && !IsClientInitialized) + return; - //Rate to update. Changes per property. - float rate; - Transform t = _cachedTransform; + //If client auth and the owner don't move towards target. + if (_clientAuthoritative) + { + if (IsOwner || TakenOwnership) + return; + } + else + { + //If not client authoritative, is owner, and don't sync to owner. + if (IsOwner && !_sendToOwner) + return; + } - //Snap any bits of the transform that should be. - SnapProperties(td); + //True if not client controlled. + bool controlledByClient = _clientAuthoritative && Owner.IsActive; + //If not controlled by client and is server then no reason to move. + if (!controlledByClient && IsServerInitialized) + return; - //Position. - if (_synchronizePosition) - { - rate = rd.Position; - Vector3 posGoal = td.ExtrapolationState == TransformData.ExtrapolateState.Active && !_lastReceiveReliable ? td.ExtrapolatedPosition : td.Position; - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (rate == -1f) - t.localPosition = td.Position; - else - t.localPosition = Vector3.MoveTowards(t.localPosition, posGoal, rate * delta * multiplier); - } + /* Once here it's safe to assume the object will be moving. + * Any checks which would stop it from moving be it client + * auth and owner, or server controlled and server, ect, + * would have already been run. */ + TransformData td = _currentGoalData.Transforms; + RateData rd = _currentGoalData.Rates; + + //Set parent. + if (_synchronizeParent) + SetParent(td.ParentBehaviour, rd); + + float multiplier = 1f; + int queueCount = _goalDataQueue.Count; + //Increase move rate slightly if over queue count. + if (queueCount > _interpolation + 1) + multiplier += 0.05f; + + //Rate to update. Changes per property. + float rate; + Transform t = _cachedTransform; + t.GetLocalPositionAndRotation(out Vector3 localPosition, out Quaternion localRotation); + Vector3 localScale = t.localScale; - //Rotation. - if (_synchronizeRotation) - { - rate = rd.Rotation; - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (rate == -1f) - t.localRotation = td.Rotation; - else - t.localRotation = Quaternion.RotateTowards(t.localRotation, td.Rotation, rate * delta); - } + //Snap any bits of the transform that should be. + SnapProperties(td); - //Scale. - if (_synchronizeScale) - { - rate = rd.Scale; - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (rate == -1f) - t.localScale = td.Scale; - else - t.localScale = Vector3.MoveTowards(t.localScale, td.Scale, rate * delta); - } + //Position. + if (_synchronizePosition) + { + rate = rd.Position; + Vector3 posGoal = + td.ExtrapolationState == TransformData.ExtrapolateState.Active && !_lastReceiveReliable + ? td.ExtrapolatedPosition + : td.Position; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (rate == -1f) + t.localPosition = td.Position; + else + t.localPosition = Vector3.MoveTowards(localPosition, posGoal, rate * delta * multiplier); + } - float timeRemaining = rd.TimeRemaining - delta * multiplier; - if (timeRemaining < -delta) - timeRemaining = -delta; - rd.TimeRemaining = timeRemaining; + //Rotation. + if (_synchronizeRotation) + { + rate = rd.Rotation; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (rate == -1f) + t.localRotation = td.Rotation; + else + t.localRotation = Quaternion.RotateTowards(localRotation, td.Rotation, rate * delta); + } - if (rd.TimeRemaining <= 0f) - { - float leftOver = Mathf.Abs(rd.TimeRemaining); - //If more in buffer then run next buffer. - if (queueCount > 0) + //Scale. + if (_synchronizeScale) { - SetCurrentGoalData(_goalDataQueue.Dequeue()); - if (leftOver > 0f) - MoveToTarget(leftOver); + rate = rd.Scale; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (rate == -1f) + t.localScale = td.Scale; + else + t.localScale = Vector3.MoveTowards(localScale, td.Scale, rate * delta); } - //No more in buffer, see if can extrapolate. - else + + float timeRemaining = rd.TimeRemaining - delta * multiplier; + if (timeRemaining < -delta) + timeRemaining = -delta; + rd.TimeRemaining = timeRemaining; + + if (rd.TimeRemaining <= 0f) { - /* If everything matches up then end queue. - * Otherwise let it play out until stuff - * aligns. Generally the time remaining is enough - * but every once in awhile something goes funky - * and it's thrown off. */ - if (!HasChanged(td)) - _currentGoalData = null; - OnInterpolationComplete?.Invoke(); + float leftOver = Mathf.Abs(rd.TimeRemaining); + //If more in buffer then run next buffer. + if (queueCount > 0) + { + SetCurrentGoalData(_goalDataQueue.Dequeue()); + if (leftOver > 0f) + MoveToTarget(leftOver); + } + //No more in buffer, see if can extrapolate. + else + { + //PROSTART + //Can extrapolate. + if (td.ExtrapolationState == TransformData.ExtrapolateState.Available) + { + rd.TimeRemaining = (float)(_extrapolation * _timeManager.TickDelta); + td.ExtrapolationState = TransformData.ExtrapolateState.Active; + if (leftOver > 0f) + MoveToTarget(leftOver); } - } + //Ran out of extrapolate. + else if (td.ExtrapolationState == TransformData.ExtrapolateState.Active) + { + rd.TimeRemaining = (float)(_extrapolation * _timeManager.TickDelta); + td.ExtrapolationState = TransformData.ExtrapolateState.Disabled; + if (leftOver > 0f) + MoveToTarget(leftOver); + } + //Extrapolation has ended or was never enabled. + else + { + //PROEND + /* If everything matches up then end queue. + * Otherwise let it play out until stuff + * aligns. Generally the time remaining is enough + * but every once in awhile something goes funky + * and it's thrown off. */ + if (!HasChanged(td)) + _currentGoalData = null; + OnInterpolationComplete?.Invoke(); + //PROSTART + } + //PROEND + } + } } } @@ -1748,7 +1812,8 @@ private void SendToClients() * then a packet maybe did not arrive when expected. See if we need * to force a reliable with the last data based on ticks passed since * last update.*/ - if (!_authoritativeClientData.HasData && _authoritativeClientData.Channel != Channel.Reliable && _authoritativeClientData.Writer != null) + if (!_authoritativeClientData.HasData && _authoritativeClientData.Channel != Channel.Reliable && + _authoritativeClientData.Writer != null) { /* If ticks have passed beyond interpolation then force * to send reliably. */ @@ -1765,7 +1830,8 @@ private void SendToClients() { _changedSinceStart = true; //Resend data from clients. - ObserversUpdateClientAuthoritativeTransform(_authoritativeClientData.Writer.GetArraySegment(), _authoritativeClientData.Channel); + ObserversUpdateClientAuthoritativeTransform(_authoritativeClientData.Writer.GetArraySegment(), + _authoritativeClientData.Channel); //Now being sent data can unset. _authoritativeClientData.HasData = false; } @@ -1804,7 +1870,8 @@ private void SendToClients() /* If here a send for transform values will occur. Update last values. * Tick doesn't need to be set for whoever controls transform. */ //Transform t = _cachedTransform; - //lastSentData.Update(0, t.localPosition, t.localRotation, t.localScale, t.localPosition, ParentBehaviour); + //t.GetLocalPositionAndRotation(out var localPosition, out var localRotation); + //lastSentData.Update(0, localPosition, localRotation, t.localScale, localPosition, ParentBehaviour); lastSentData.Tick = 0; SerializeChanged(changed, writer, lastSentData); @@ -1861,7 +1928,8 @@ private void SendToServer(TransformData lastSentTransformData) * Tick doesn't need to be set for whoever controls transform. */ Transform t = _cachedTransform; - //lastSentData.Update(0, t.localPosition, t.localRotation, t.localScale, t.localPosition, ParentBehaviour); + //t.GetLocalPositionAndRotation(out var localPosition, out var localRotation); + //lastSentData.Update(0, localPosition, localRotation, t.localScale, localPosition, ParentBehaviour); lastSentTransformData.Tick = 0; //Send latest. @@ -1878,10 +1946,12 @@ private void SendToServer(TransformData lastSentTransformData) /// /// Returns if the transform differs from td. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool HasChanged(TransformData td) { Transform t = _cachedTransform; - bool changed = td.Position != t.localPosition || td.Rotation != t.localRotation || td.Scale != t.localScale; + t.GetLocalPositionAndRotation(out var localPosition, out var localRotation); + bool changed = td.Position != localPosition || td.Rotation != localRotation || td.Scale != t.localScale; return changed; } @@ -1889,6 +1959,7 @@ private bool HasChanged(TransformData td) /// /// Returns if there is any change between two datas. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool HasChanged(TransformData a, TransformData b) { return a.Position != b.Position || a.Rotation != b.Rotation || a.Scale != b.Scale || a.ParentBehaviour != b.ParentBehaviour; @@ -1926,6 +1997,7 @@ private bool HasChanged(TransformData a, TransformData b) /// /// Gets transform values that have changed against goalData. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private ChangedDelta GetChanged(TransformData transformData) { //If default return full changed. @@ -1945,39 +2017,41 @@ private ChangedDelta GetChanged(TransformData transformData) /// private ChangedDelta GetChanged(Vector3 lastPosition, Quaternion lastRotation, Vector3 lastScale, NetworkBehaviour lastParentBehaviour) { - ChangedDelta changed = ChangedDelta.Unset; - Transform t = _cachedTransform; + using (_pm_GetChanged.Auto()) + { + ChangedDelta changed = ChangedDelta.Unset; + Transform t = _cachedTransform; + t.GetLocalPositionAndRotation(out Vector3 localPosition, out Quaternion localRotation); - Vector3 position = t.localPosition; - if (Mathf.Abs(position.x - lastPosition.x) >= _positionSensitivity) - changed |= ChangedDelta.PositionX; - if (Mathf.Abs(position.y - lastPosition.y) >= _positionSensitivity) - changed |= ChangedDelta.PositionY; - if (Mathf.Abs(position.z - lastPosition.z) >= _positionSensitivity) - changed |= ChangedDelta.PositionZ; + if (Mathf.Abs(localPosition.x - lastPosition.x) >= _positionSensitivity) + changed |= ChangedDelta.PositionX; + if (Mathf.Abs(localPosition.y - lastPosition.y) >= _positionSensitivity) + changed |= ChangedDelta.PositionY; + if (Mathf.Abs(localPosition.z - lastPosition.z) >= _positionSensitivity) + changed |= ChangedDelta.PositionZ; - Quaternion rotation = t.localRotation; - if (!rotation.Matches(lastRotation, true)) - changed |= ChangedDelta.Rotation; + if (!localRotation.Matches(lastRotation, true)) + changed |= ChangedDelta.Rotation; - ChangedDelta startChanged = changed; + ChangedDelta startChanged = changed; - Vector3 scale = t.localScale; - if (Mathf.Abs(scale.x - lastScale.x) >= _scaleSensitivity) - changed |= ChangedDelta.ScaleX; - if (Mathf.Abs(scale.y - lastScale.y) >= _scaleSensitivity) - changed |= ChangedDelta.ScaleY; - if (Mathf.Abs(scale.z - lastScale.z) >= _scaleSensitivity) - changed |= ChangedDelta.ScaleZ; - - if (changed != ChangedDelta.Unset && ParentBehaviour != null) - changed |= ChangedDelta.Nested; - - //If added scale or childed then also add extended. - if (startChanged != changed) - changed |= ChangedDelta.Extended; - - return changed; + Vector3 scale = t.localScale; + if (Mathf.Abs(scale.x - lastScale.x) >= _scaleSensitivity) + changed |= ChangedDelta.ScaleX; + if (Mathf.Abs(scale.y - lastScale.y) >= _scaleSensitivity) + changed |= ChangedDelta.ScaleY; + if (Mathf.Abs(scale.z - lastScale.z) >= _scaleSensitivity) + changed |= ChangedDelta.ScaleZ; + + if (changed != ChangedDelta.Unset && ParentBehaviour != null) + changed |= ChangedDelta.Nested; + + //If added scale or childed then also add extended. + if (startChanged != changed) + changed |= ChangedDelta.Extended; + + return changed; + } } #endregion @@ -1993,36 +2067,38 @@ private void SnapProperties(TransformData transformData, bool force = false) transformData.SnappingChecked = true; Transform t = _cachedTransform; - + t.GetLocalPositionAndRotation(out Vector3 startPosition, out Quaternion startRotation); + //Position. if (_synchronizePosition) { - Vector3 startPosition = t.localPosition; Vector3 position; - position.x = force || _positionSnapping.X ? transformData.Position.x : t.localPosition.x; - position.y = force || _positionSnapping.Y ? transformData.Position.y : t.localPosition.y; - position.z = force || _positionSnapping.Z ? transformData.Position.z : t.localPosition.z; + position.x = force || _positionSnapping.X ? transformData.Position.x : startPosition.x; + position.y = force || _positionSnapping.Y ? transformData.Position.y : startPosition.y; + position.z = force || _positionSnapping.Z ? transformData.Position.z : startPosition.z; t.localPosition = position; } //Rotation. if (_synchronizeRotation) { - Vector3 eulers; + Vector3 startEulers = startRotation.eulerAngles; Vector3 goalEulers = transformData.Rotation.eulerAngles; - eulers.x = force || _rotationSnapping.X ? goalEulers.x : t.localEulerAngles.x; - eulers.y = force || _rotationSnapping.Y ? goalEulers.y : t.localEulerAngles.y; - eulers.z = force || _rotationSnapping.Z ? goalEulers.z : t.localEulerAngles.z; + Vector3 eulers; + eulers.x = force || _rotationSnapping.X ? goalEulers.x : startEulers.x; + eulers.y = force || _rotationSnapping.Y ? goalEulers.y : startEulers.y; + eulers.z = force || _rotationSnapping.Z ? goalEulers.z : startEulers.z; t.localEulerAngles = eulers; } //Scale. if (_synchronizeScale) { + var startScale = t.localScale; Vector3 scale; - scale.x = force || _scaleSnapping.X ? transformData.Scale.x : t.localScale.x; - scale.y = force || _scaleSnapping.Y ? transformData.Scale.y : t.localScale.y; - scale.z = force || _scaleSnapping.Z ? transformData.Scale.z : t.localScale.z; + scale.x = force || _scaleSnapping.X ? transformData.Scale.x : startScale.x; + scale.y = force || _scaleSnapping.Y ? transformData.Scale.y : startScale.y; + scale.z = force || _scaleSnapping.Z ? transformData.Scale.z : startScale.z; t.localScale = scale; } } @@ -2030,6 +2106,7 @@ private void SnapProperties(TransformData transformData, bool force = false) /// /// Sets move rates which will occur instantly. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetInstantRates(RateData rd, uint tickDifference, float timeRemaining) { //Was default to 1 tickDiff and -1 time remaining. @@ -2192,7 +2269,8 @@ private void SetCalculatedRates(TransformData prevTd, RateData prevRd, GoalData rd.Update(positionRate, rotationRate, scaleRate, unalteredPositionRate, tickDifference, timePassed); //Returns if whole contains part. - bool ChangedFullContains(ChangedFull whole, ChangedFull part) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool ChangedFullContains(ChangedFull whole, ChangedFull part) { return (whole & part) == part; } @@ -2201,7 +2279,8 @@ bool ChangedFullContains(ChangedFull whole, ChangedFull part) * This is used to decide if a property should be teleported. * When distances are exceptionally small smoothing rate * calculations may result as an invalid value. */ - bool LowDistance(float dist, bool rotation) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool LowDistance(float dist, bool rotation) { if (rotation) return dist < 1f; @@ -2213,6 +2292,7 @@ bool LowDistance(float dist, bool rotation) /// /// Gets the tick difference between two GoalDatas. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private uint GetTickDifference(TransformData prevTd, GoalData nextGd, uint minimum, out float timePassed) { TransformData nextTd = nextGd.Transforms; @@ -2236,12 +2316,22 @@ private uint GetTickDifference(TransformData prevTd, GoalData nextGd, uint minim /// /// Sets extrapolation data on next. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetExtrapolatedData(TransformData prev, TransformData next, Channel channel) { //Default value. next.ExtrapolationState = TransformData.ExtrapolateState.Disabled; - } + //PROSTART + //Teleports cannot extrapolate. + if (_extrapolation == 0 || !_synchronizePosition || channel == Channel.Reliable || next.Position == prev.Position) + return; + + Vector3 offet = (next.Position - prev.Position) * _extrapolation; + next.ExtrapolatedPosition = next.Position + offet; + next.ExtrapolationState = TransformData.ExtrapolateState.Available; + //PROEND + } /// /// Updates a client with transform data. @@ -2308,108 +2398,112 @@ private void ServerUpdateTransform(ArraySegment data, Channel channel) } /// - /// Processes received data for lcients and server. + /// Processes received data for clients and server. /// private void DataReceived(ArraySegment data, Channel channel, bool asServer) { - if (IsDeinitializing) - return; - - TransformData prevTd = asServer ? _lastReceivedClientTransformData : _lastReceivedServerTransformData; - RateData prevRd = _lastCalculatedRateData; + using (_pm_DataReceived.Auto()) + { + if (IsDeinitializing) + return; - ChangedFull changedFull = ChangedFull.Unset; - GoalData nextGd = ResettableObjectCaches.Retrieve(); - TransformData nextTd = nextGd.Transforms; - UpdateTransformData(data, prevTd, nextTd, ref changedFull); + TransformData prevTd = asServer ? _lastReceivedClientTransformData : _lastReceivedServerTransformData; + RateData prevRd = _lastCalculatedRateData; - OnDataReceived?.Invoke(prevTd, nextTd); - SetExtrapolatedData(prevTd, nextTd, channel); + ChangedFull changedFull = ChangedFull.Unset; + GoalData nextGd = ResettableObjectCaches.Retrieve(); + TransformData nextTd = nextGd.Transforms; + UpdateTransformData(data, prevTd, nextTd, ref changedFull); - bool hasChanged = HasChanged(prevTd, nextTd); + OnDataReceived?.Invoke(prevTd, nextTd); + SetExtrapolatedData(prevTd, nextTd, channel); - //If server only teleport. - if (asServer && !IsClientStarted) - { - uint tickDifference = GetTickDifference(prevTd, nextGd, 1, out float timePassed); - SetInstantRates(nextGd.Rates, tickDifference, timePassed); - } - //Otherwise use timed. - else - { - SetCalculatedRates(prevTd, prevRd, nextGd, changedFull, hasChanged, channel); - } + bool hasChanged = HasChanged(prevTd, nextTd); - _lastReceiveReliable = channel == Channel.Reliable; - /* If channel is reliable then this is a settled packet. - * Set tick to UNSET. When this occurs time calculations - * assume only 1 tick has passed. */ - if (channel == Channel.Reliable) - nextTd.Tick = TimeManager.UNSET_TICK; - - prevTd.Update(nextTd); - prevRd.Update(nextGd.Rates); - - nextGd.ReceivedTick = _timeManager.LocalTick; - - bool currentDataNull = _currentGoalData == null; - /* If extrapolating then immediately break the extrapolation - * in favor of newest results. This will keep the buffer - * at 0 until the transform settles but the only other option is - * to stop the movement, which would defeat purpose of extrapolation, - * or slow down the transform while buffer rebuilds. Neither choice - * is great but later on I might try slowing down the transform slightly - * to give the buffer a chance to rebuild. */ - if (!currentDataNull && _currentGoalData.Transforms.ExtrapolationState == TransformData.ExtrapolateState.Active) - { - SetCurrentGoalData(nextGd); - } - /* If queue isn't started and its buffered enough - * to satisfy interpolation then set ready - * and set current data. - * - * Also if reliable then begin moving. */ - else if ((currentDataNull && _goalDataQueue.Count >= _interpolation) || channel == Channel.Reliable) - { - if (_goalDataQueue.Count > 0) + //If server only teleport. + if (asServer && !IsClientStarted) { - SetCurrentGoalData(_goalDataQueue.Dequeue()); - /* If is reliable and has changed then also - * enqueue latest. */ - if (hasChanged) - _goalDataQueue.Enqueue(nextGd); + uint tickDifference = GetTickDifference(prevTd, nextGd, 1, out float timePassed); + SetInstantRates(nextGd.Rates, tickDifference, timePassed); } + //Otherwise use timed. else { - SetCurrentGoalData(nextGd); + SetCalculatedRates(prevTd, prevRd, nextGd, changedFull, hasChanged, channel); } - } - /* If here then there's not enough in buffer to begin - * so add onto the buffer. */ - else - { - _goalDataQueue.Enqueue(nextGd); - } - /* If the queue is excessive beyond interpolation then - * dequeue extras to prevent from dropping behind too - * quickly. This shouldn't be an issue with normal movement - * as the NT speeds up if the buffer unexpectedly grows, but - * when connections are unstable results may come in chunks - * and for a better experience the older parts of the chunks - * will be dropped. */ - if (_goalDataQueue.Count > _interpolation + 3) - { - while (_goalDataQueue.Count > _interpolation) + _lastReceiveReliable = channel == Channel.Reliable; + /* If channel is reliable then this is a settled packet. + * Set tick to UNSET. When this occurs time calculations + * assume only 1 tick has passed. */ + if (channel == Channel.Reliable) + nextTd.Tick = TimeManager.UNSET_TICK; + + prevTd.Update(nextTd); + prevRd.Update(nextGd.Rates); + + nextGd.ReceivedTick = _timeManager.LocalTick; + + bool currentDataNull = _currentGoalData == null; + /* If extrapolating then immediately break the extrapolation + * in favor of newest results. This will keep the buffer + * at 0 until the transform settles but the only other option is + * to stop the movement, which would defeat purpose of extrapolation, + * or slow down the transform while buffer rebuilds. Neither choice + * is great but later on I might try slowing down the transform slightly + * to give the buffer a chance to rebuild. */ + if (!currentDataNull && _currentGoalData.Transforms.ExtrapolationState == + TransformData.ExtrapolateState.Active) { - GoalData tmpGd = _goalDataQueue.Dequeue(); - ResettableObjectCaches.Store(tmpGd); + SetCurrentGoalData(nextGd); + } + /* If queue isn't started and its buffered enough + * to satisfy interpolation then set ready + * and set current data. + * + * Also if reliable then begin moving. */ + else if ((currentDataNull && _goalDataQueue.Count >= _interpolation) || channel == Channel.Reliable) + { + if (_goalDataQueue.Count > 0) + { + SetCurrentGoalData(_goalDataQueue.Dequeue()); + /* If is reliable and has changed then also + * enqueue latest. */ + if (hasChanged) + _goalDataQueue.Enqueue(nextGd); + } + else + { + SetCurrentGoalData(nextGd); + } + } + /* If here then there's not enough in buffer to begin + * so add onto the buffer. */ + else + { + _goalDataQueue.Enqueue(nextGd); } - //Snap to the next data to fix any smoothing timings. - SetCurrentGoalData(_goalDataQueue.Dequeue()); - SetInstantRates(_currentGoalData!.Rates, 1, -1f); - SnapProperties(_currentGoalData.Transforms, true); + /* If the queue is excessive beyond interpolation then + * dequeue extras to prevent from dropping behind too + * quickly. This shouldn't be an issue with normal movement + * as the NT speeds up if the buffer unexpectedly grows, but + * when connections are unstable results may come in chunks + * and for a better experience the older parts of the chunks + * will be dropped. */ + if (_goalDataQueue.Count > _interpolation + 3) + { + while (_goalDataQueue.Count > _interpolation) + { + GoalData tmpGd = _goalDataQueue.Dequeue(); + ResettableObjectCaches.Store(tmpGd); + } + + //Snap to the next data to fix any smoothing timings. + SetCurrentGoalData(_goalDataQueue.Dequeue()); + SetInstantRates(_currentGoalData!.Rates, 1, -1f); + SnapProperties(_currentGoalData.Transforms, true); + } } } diff --git a/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/Utilities/Dictionaries.cs b/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/Utilities/Dictionaries.cs index b6a56652..d7d65070 100644 --- a/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/Utilities/Dictionaries.cs +++ b/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/Utilities/Dictionaries.cs @@ -8,7 +8,7 @@ public static class DictionaryFN /// Uses a hacky way to TryGetValue on a dictionary when using IL2CPP and on mobile. /// This is to support older devices that don't properly handle IL2CPP builds. /// - public static bool TryGetValueIL2CPP(this IDictionary dict, TKey key, out TValue value) + public static bool TryGetValueIL2CPP(this IReadOnlyDictionary dict, TKey key, out TValue value) { #if ENABLE_IL2CPP && UNITY_IOS || UNITY_ANDROID if (dict.ContainsKey(key)) @@ -30,7 +30,7 @@ public static bool TryGetValueIL2CPP(this IDictionary /// - public static List ValuesToList(this IDictionary dict, bool useCache) + public static List ValuesToList(this IReadOnlyDictionary dict, bool useCache) { List result = useCache ? CollectionCaches.RetrieveList() : new(dict.Count); @@ -43,7 +43,7 @@ public static List ValuesToList(this IDictionary /// Adds values to a list. /// - public static void ValuesToList(this IDictionary dict, ref List result, bool clearLst) + public static void ValuesToList(this IReadOnlyDictionary dict, ref List result, bool clearLst) { if (clearLst) result.Clear(); @@ -55,7 +55,7 @@ public static void ValuesToList(this IDictionary dic /// /// Returns keys as a list. /// - public static List KeysToList(this IDictionary dict, bool useCache) + public static List KeysToList(this IReadOnlyDictionary dict, bool useCache) { List result = useCache ? CollectionCaches.RetrieveList() : new(dict.Count); @@ -68,7 +68,7 @@ public static List KeysToList(this IDictionary /// /// Adds keys to a list. /// - public static void KeysToList(this IDictionary dict, ref List result, bool clearLst) + public static void KeysToList(this IReadOnlyDictionary dict, ref List result, bool clearLst) { result.Clear(); diff --git a/Assets/FishNet/Runtime/Utility/Extension/Transforms.cs b/Assets/FishNet/Runtime/Utility/Extension/Transforms.cs index 301b71e2..b92f9bd9 100644 --- a/Assets/FishNet/Runtime/Utility/Extension/Transforms.cs +++ b/Assets/FishNet/Runtime/Utility/Extension/Transforms.cs @@ -3,6 +3,7 @@ using FishNet.Object; using System.Runtime.CompilerServices; using UnityEngine; +using UnityEngine.Jobs; namespace FishNet.Utility.Extension { @@ -10,48 +11,124 @@ namespace FishNet.Utility.Extension public static class TransformFN { /// - /// Sets values of TransformProperties to a transforms world properties. + /// Gets correct values of Vector3 pos and Quaternion rot + /// + public static void GetCorrectLocalPositionAndRotation(this TransformAccess t, out Vector3 pos, out Quaternion rot) + { + // https://issuetracker.unity3d.com/issues/wrong-position-and-rotation-values-are-returned-when-using-transformaccess-dot-getlocalpositionandrotation + pos = t.localPosition; + rot = t.localRotation; + } + + /// + /// Sets correct values of Vector3 pos and Quaternion rot + /// + public static void SetCorrectLocalPositionAndRotation(this TransformAccess t, Vector3 pos, Quaternion rot) + { + // https://issuetracker.unity3d.com/issues/wrong-position-and-rotation-values-are-returned-when-using-transformaccess-dot-getlocalpositionandrotation + t.localPosition = pos; + t.localRotation = rot; + } + + /// + /// Gets values of TransformProperties from the transforms world properties. /// public static TransformProperties GetWorldProperties(this Transform t) { - TransformProperties tp = new(t.position, t.rotation, t.localScale); + t.GetPositionAndRotation(out var pos, out var rot); + TransformProperties tp = new(pos, rot, t.localScale); + return tp; + } + + /// + /// Gets values of TransformProperties from the transforms world properties. + /// + public static TransformProperties GetWorldProperties(this TransformAccess t) + { + t.GetPositionAndRotation(out var pos, out var rot); + TransformProperties tp = new(pos, rot, t.localScale); return tp; } /// - /// Sets values of TransformProperties to a transforms world properties. + /// Gets values of TransformProperties from the transforms world properties. /// public static TransformProperties GetWorldProperties(this Transform t, TransformProperties offset) { - TransformProperties tp = new(t.position, t.rotation, t.localScale); + t.GetPositionAndRotation(out var pos, out var rot); + TransformProperties tp = new(pos, rot, t.localScale); + tp.Add(offset); + return tp; + } + + /// + /// Gets values of TransformProperties from the transforms world properties. + /// + public static TransformProperties GetWorldProperties(this TransformAccess t, TransformProperties offset) + { + t.GetPositionAndRotation(out var pos, out var rot); + TransformProperties tp = new(pos, rot, t.localScale); tp.Add(offset); return tp; } /// - /// Sets values of TransformProperties to a transforms world properties. + /// Gets values of TransformProperties from the transforms world properties. /// public static TransformPropertiesCls GetWorldPropertiesCls(this Transform t) { - TransformPropertiesCls tp = new(t.position, t.rotation, t.localScale); + t.GetPositionAndRotation(out var pos, out var rot); + TransformPropertiesCls tp = new(pos, rot, t.localScale); + return tp; + } + + /// + /// Gets values of TransformProperties from the transforms world properties. + /// + public static TransformPropertiesCls GetWorldPropertiesCls(this TransformAccess t) + { + t.GetPositionAndRotation(out var pos, out var rot); + TransformPropertiesCls tp = new(pos, rot, t.localScale); return tp; } /// - /// Sets values of TransformProperties to a transforms world properties. + /// Gets values of TransformProperties from the transforms world properties. /// public static TransformProperties GetLocalProperties(this Transform t) { - TransformProperties tp = new(t.localPosition, t.localRotation, t.localScale); + t.GetLocalPositionAndRotation(out var pos, out var rot); + TransformProperties tp = new(pos, rot, t.localScale); + return tp; + } + + /// + /// Gets values of TransformProperties from the transforms world properties. + /// + public static TransformProperties GetLocalProperties(this TransformAccess t) + { + t.GetCorrectLocalPositionAndRotation(out var pos, out var rot); + TransformProperties tp = new(pos, rot, t.localScale); return tp; } /// - /// Sets values of TransformProperties to a transforms world properties. + /// Gets values of TransformProperties from the transforms world properties. /// public static TransformPropertiesCls GetLocalPropertiesCls(this Transform t) { - TransformPropertiesCls tp = new(t.localPosition, t.localRotation, t.localScale); + t.GetLocalPositionAndRotation(out var pos, out var rot); + TransformPropertiesCls tp = new(pos, rot, t.localScale); + return tp; + } + + /// + /// Gets values of TransformProperties from the transforms world properties. + /// + public static TransformPropertiesCls GetLocalPropertiesCls(this TransformAccess t) + { + t.GetCorrectLocalPositionAndRotation(out var pos, out var rot); + TransformPropertiesCls tp = new(pos, rot, t.localScale); return tp; } @@ -70,8 +147,10 @@ public static void SetTransformOffsets(this Transform t, Transform target, ref V { if (target == null) return; - pos = target.position - t.position; - rot = target.rotation * Quaternion.Inverse(t.rotation); + t.GetPositionAndRotation(out var tPos, out var tRot); + target.GetPositionAndRotation(out var targetPos, out var targetRot); + pos = targetPos - tPos; + rot = targetRot * Quaternion.Inverse(tRot); } /// @@ -83,7 +162,9 @@ public static TransformProperties GetTransformOffsets(this Transform t, Transfor if (target == null) return default; - return new(target.position - t.position, target.rotation * Quaternion.Inverse(t.rotation), target.localScale - t.localScale); + t.GetPositionAndRotation(out var tPos, out var tRot); + target.GetPositionAndRotation(out var targetPos, out var targetRot); + return new(targetPos - tPos, targetRot * Quaternion.Inverse(tRot), target.localScale - t.localScale); } /// @@ -91,8 +172,16 @@ public static TransformProperties GetTransformOffsets(this Transform t, Transfor /// public static void SetLocalProperties(this Transform t, TransformPropertiesCls tp) { - t.localPosition = tp.Position; - t.localRotation = tp.Rotation; + t.SetLocalPositionAndRotation(tp.Position, tp.Rotation); + t.localScale = tp.LocalScale; + } + + /// + /// Sets a transform to local properties. + /// + public static void SetLocalProperties(this TransformAccess t, TransformPropertiesCls tp) + { + t.SetCorrectLocalPositionAndRotation(tp.Position, tp.Rotation); t.localScale = tp.LocalScale; } @@ -101,8 +190,16 @@ public static void SetLocalProperties(this Transform t, TransformPropertiesCls t /// public static void SetLocalProperties(this Transform t, TransformProperties tp) { - t.localPosition = tp.Position; - t.localRotation = tp.Rotation; + t.SetLocalPositionAndRotation(tp.Position, tp.Rotation); + t.localScale = tp.Scale; + } + + /// + /// Sets a transform to local properties. + /// + public static void SetLocalProperties(this TransformAccess t, TransformProperties tp) + { + t.SetCorrectLocalPositionAndRotation(tp.Position, tp.Rotation); t.localScale = tp.Scale; } @@ -111,8 +208,16 @@ public static void SetLocalProperties(this Transform t, TransformProperties tp) /// public static void SetWorldProperties(this Transform t, TransformPropertiesCls tp) { - t.position = tp.Position; - t.rotation = tp.Rotation; + t.SetPositionAndRotation(tp.Position, tp.Rotation); + t.localScale = tp.LocalScale; + } + + /// + /// Sets a transform to world properties. + /// + public static void SetWorldProperties(this TransformAccess t, TransformPropertiesCls tp) + { + t.SetPositionAndRotation(tp.Position, tp.Rotation); t.localScale = tp.LocalScale; } @@ -121,8 +226,16 @@ public static void SetWorldProperties(this Transform t, TransformPropertiesCls t /// public static void SetWorldProperties(this Transform t, TransformProperties tp) { - t.position = tp.Position; - t.rotation = tp.Rotation; + t.SetPositionAndRotation(tp.Position, tp.Rotation); + t.localScale = tp.Scale; + } + + /// + /// Sets a transform to world properties. + /// + public static void SetWorldProperties(this TransformAccess t, TransformProperties tp) + { + t.SetPositionAndRotation(tp.Position, tp.Rotation); t.localScale = tp.Scale; } @@ -131,8 +244,15 @@ public static void SetWorldProperties(this Transform t, TransformProperties tp) /// public static void SetLocalPositionAndRotation(this Transform t, Vector3 pos, Quaternion rot) { - t.localPosition = pos; - t.localRotation = rot; + t.SetLocalPositionAndRotation(pos, rot); + } + + /// + /// Sets local position and rotation for a transform. + /// + public static void SetLocalPositionAndRotation(this TransformAccess t, Vector3 pos, Quaternion rot) + { + t.SetCorrectLocalPositionAndRotation(pos, rot); } /// @@ -140,8 +260,16 @@ public static void SetLocalPositionAndRotation(this Transform t, Vector3 pos, Qu /// public static void SetLocalPositionRotationAndScale(this Transform t, Vector3 pos, Quaternion rot, Vector3 scale) { - t.localPosition = pos; - t.localRotation = rot; + t.SetLocalPositionAndRotation(pos, rot); + t.localScale = scale; + } + + /// + /// Sets local position, rotation, and scale for a transform. + /// + public static void SetLocalPositionRotationAndScale(this TransformAccess t, Vector3 pos, Quaternion rot, Vector3 scale) + { + t.SetCorrectLocalPositionAndRotation(pos, rot); t.localScale = scale; } @@ -151,8 +279,29 @@ public static void SetLocalPositionRotationAndScale(this Transform t, Vector3 po public static void SetLocalPositionRotationAndScale(this Transform t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale) { if (nullablePos.HasValue) - t.localPosition = nullablePos.Value; - if (nullableRot.HasValue) + { + if (nullableRot.HasValue) + t.SetLocalPositionAndRotation(nullablePos.Value, nullableRot.Value); + else t.localPosition = nullablePos.Value; + } + else if (nullableRot.HasValue) + t.localRotation = nullableRot.Value; + if (nullableScale.HasValue) + t.localScale = nullableScale.Value; + } + + /// + /// Sets local position, rotation, and scale using nullables for a transform. If a value is null then that property is skipped. + /// + public static void SetLocalPositionRotationAndScale(this TransformAccess t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale) + { + if (nullablePos.HasValue) + { + if (nullableRot.HasValue) + t.SetCorrectLocalPositionAndRotation(nullablePos.Value, nullableRot.Value); + else t.localPosition = nullablePos.Value; + } + else if (nullableRot.HasValue) t.localRotation = nullableRot.Value; if (nullableScale.HasValue) t.localScale = nullableScale.Value; @@ -164,8 +313,29 @@ public static void SetLocalPositionRotationAndScale(this Transform t, Vector3? n public static void SetWorldPositionRotationAndScale(this Transform t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale) { if (nullablePos.HasValue) - t.position = nullablePos.Value; - if (nullableRot.HasValue) + { + if (nullableRot.HasValue) + t.SetPositionAndRotation(nullablePos.Value, nullableRot.Value); + else t.position = nullablePos.Value; + } + else if (nullableRot.HasValue) + t.rotation = nullableRot.Value; + if (nullableScale.HasValue) + t.localScale = nullableScale.Value; + } + + /// + /// Sets world position, rotation, and scale using nullables for a transform. If a value is null then that property is skipped. + /// + public static void SetWorldPositionRotationAndScale(this TransformAccess t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale) + { + if (nullablePos.HasValue) + { + if (nullableRot.HasValue) + t.SetPositionAndRotation(nullablePos.Value, nullableRot.Value); + else t.position = nullablePos.Value; + } + else if (nullableRot.HasValue) t.rotation = nullableRot.Value; if (nullableScale.HasValue) t.localScale = nullableScale.Value; @@ -176,8 +346,56 @@ public static void SetWorldPositionRotationAndScale(this Transform t, Vector3? n /// public static void OutLocalPropertyValues(this Transform t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale, out Vector3 pos, out Quaternion rot, out Vector3 scale) { - pos = nullablePos == null ? t.localPosition : nullablePos.Value; - rot = nullableRot == null ? t.localRotation : nullableRot.Value; + if (!nullablePos.HasValue) + { + if (!nullableRot.HasValue) + t.GetLocalPositionAndRotation(out pos, out rot); + else + { + pos = t.localPosition; + rot = nullableRot.Value; + } + } + else if (!nullableRot.HasValue) + { + pos = nullablePos.Value; + rot = t.localRotation; + } + else + { + pos = nullablePos.Value; + rot = nullableRot.Value; + } + + scale = nullableScale == null ? t.localScale : nullableScale.Value; + } + + /// + /// Oututs properties to use for a transform. When a nullable property has value that value is used, otherwise the transforms current property is used. + /// + public static void OutLocalPropertyValues(this TransformAccess t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale, out Vector3 pos, out Quaternion rot, out Vector3 scale) + { + if (!nullablePos.HasValue) + { + if (!nullableRot.HasValue) + t.GetCorrectLocalPositionAndRotation(out pos, out rot); + else + { + pos = t.localPosition; + rot = nullableRot.Value; + } + } + else if (!nullableRot.HasValue) + { + pos = nullablePos.Value; + rot = t.localRotation; + } + else + { + pos = nullablePos.Value; + rot = nullableRot.Value; + } + scale = nullableScale == null ? t.localScale : nullableScale.Value; } @@ -186,8 +404,56 @@ public static void OutLocalPropertyValues(this Transform t, Vector3? nullablePos /// public static void OutWorldPropertyValues(this Transform t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale, out Vector3 pos, out Quaternion rot, out Vector3 scale) { - pos = nullablePos == null ? t.position : nullablePos.Value; - rot = nullableRot == null ? t.rotation : nullableRot.Value; + if (!nullablePos.HasValue) + { + if (!nullableRot.HasValue) + t.GetPositionAndRotation(out pos, out rot); + else + { + pos = t.position; + rot = nullableRot.Value; + } + } + else if (!nullableRot.HasValue) + { + pos = nullablePos.Value; + rot = t.rotation; + } + else + { + pos = nullablePos.Value; + rot = nullableRot.Value; + } + + scale = nullableScale == null ? t.localScale : nullableScale.Value; + } + + /// + /// Oututs properties to use for a transform. When a nullable property has value that value is used, otherwise the transforms current property is used. + /// + public static void OutWorldPropertyValues(this TransformAccess t, Vector3? nullablePos, Quaternion? nullableRot, Vector3? nullableScale, out Vector3 pos, out Quaternion rot, out Vector3 scale) + { + if (!nullablePos.HasValue) + { + if (!nullableRot.HasValue) + t.GetPositionAndRotation(out pos, out rot); + else + { + pos = t.position; + rot = nullableRot.Value; + } + } + else if (!nullableRot.HasValue) + { + pos = nullablePos.Value; + rot = t.rotation; + } + else + { + pos = nullablePos.Value; + rot = nullableRot.Value; + } + scale = nullableScale == null ? t.localScale : nullableScale.Value; } } From f3b0638136e8112d0f7776d651e2e842d4d245d6 Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 20:23:52 +0300 Subject: [PATCH 2/9] feat second pard of QOLs [NonSerialized] required because in other case it cause infinity errors in Logs when you select networkmanager in hierarchy ManagedObjects now observable. --- .../NetworkAnimator/NetworkAnimator.cs | 6 - .../NetworkAnimator/NetworkAnimator.cs.bak | 1621 +++++++++++++++++ .../Runtime/Managing/Client/ClientManager.cs | 2 +- .../Managing/Client/Object/ClientObjects.cs | 2 +- .../Managing/Client/Object/ObjectCaching.cs | 2 +- .../Runtime/Managing/Object/ManagedObjects.cs | 41 +- .../Managing/Server/ServerManager.QOL.cs | 15 +- .../Runtime/Managing/Server/ServerManager.cs | 2 +- .../Runtime/Managing/Timing/TimeManager.cs | 184 +- .../Managing/Transporting/TransportManager.cs | 2 +- 10 files changed, 1774 insertions(+), 103 deletions(-) create mode 100644 Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs index 71670295..1bd718e9 100644 --- a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs +++ b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs @@ -701,12 +701,6 @@ private void InitializeOnce() foreach (AnimatorControllerParameter item in _animator.parameters) { bool process = !_animator.IsParameterControlledByCurve(item.name); - //PROSTART - /* This is done in a weird way for processing - * to work with the pro tool stripper. */ - if (IgnoredParameters.Contains(item.name)) - process = false; - //PROEND if (process) { //Over 250 parameters; who would do this!? diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak new file mode 100644 index 00000000..822d0ccd --- /dev/null +++ b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak @@ -0,0 +1,1621 @@ +#if UNITY_EDITOR || DEVELOPMENT_BUILD +#define DEVELOPMENT +#endif +using FishNet.Component.Transforming; +using FishNet.Connection; +using FishNet.Documenting; +using FishNet.Managing.Logging; +using FishNet.Managing.Server; +using FishNet.Object; +using FishNet.Serializing; +using FishNet.Utility; +using FishNet.Utility.Performance; +using GameKit.Dependencies.Utilities; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using FishNet.Managing; +using UnityEngine; +using UnityEngine.Profiling; +using Unity.Profiling; +using TimeManagerCls = FishNet.Managing.Timing.TimeManager; + +namespace FishNet.Component.Animating +{ + [AddComponentMenu("FishNet/Component/NetworkAnimator")] + public sealed class NetworkAnimator : NetworkBehaviour + { + #region Types. + /// + /// Data received from the server. + /// + private struct ReceivedServerData + { + /// + /// Gets an Arraysegment of received data. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment GetArraySegment() => new(_data, 0, _length); + + /// + /// How much data written. + /// + private int _length; + /// + /// Buffer which contains data. + /// + private byte[] _data; + + public ReceivedServerData(ArraySegment segment) + { + _length = segment.Count; + _data = ByteArrayPool.Retrieve(_length); + Buffer.BlockCopy(segment.Array, segment.Offset, _data, 0, _length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + if (_data != null) + ByteArrayPool.Store(_data); + } + } + + private struct StateChange + { + /// + /// Frame which the state was changed. + /// + public int FrameCount; + /// + /// True if a crossfade. + /// + public bool IsCrossfade; + /// + /// Hash to crossfade into. + /// + public int Hash; + /// + /// True if using FixedTime. + /// + public bool FixedTime; + /// + /// Duration of crossfade. + /// + public float DurationTime; + /// + /// Offset time of crossfade. + /// + public float OffsetTime; + /// + /// Normalized transition time of crossfade. + /// + public float NormalizedTransitionTime; + + public StateChange(int frame) + { + FrameCount = frame; + IsCrossfade = default; + Hash = default; + FixedTime = default; + DurationTime = default; + OffsetTime = default; + NormalizedTransitionTime = default; + } + + public StateChange(int frame, int hash, bool fixedTime, float duration, float offset, float normalizedTransition) + { + FrameCount = frame; + IsCrossfade = true; + Hash = hash; + FixedTime = fixedTime; + DurationTime = duration; + OffsetTime = offset; + NormalizedTransitionTime = normalizedTransition; + } + } + + /// + /// Animator updates received from clients when using Client Authoritative. + /// + private class ClientAuthoritativeUpdate + { + /// + /// + public ClientAuthoritativeUpdate() + { + // Start buffers off at 8 bytes nad grow them as needed. + for (int i = 0; i < MAXIMUM_BUFFER_COUNT; i++) + _buffers.Add(new byte[MAXIMUM_DATA_SIZE]); + + _bufferLengths = new int[MAXIMUM_BUFFER_COUNT]; + } + + #region Public. + /// + /// True to force all animator data and ignore buffers. + /// + public bool ForceAll { get; private set; } + /// + /// Number of entries in Buffers. + /// + public int BufferCount = 0; + #endregion + + #region Private. + /// + /// Length of buffers. + /// + private int[] _bufferLengths; + /// + /// Buffers. + /// + private List _buffers = new(); + #endregion + + #region Const. + /// + /// Maximum size data may be. + /// + private const int MAXIMUM_DATA_SIZE = 1000; + /// + /// Maximum number of allowed buffers. + /// + public const int MAXIMUM_BUFFER_COUNT = 2; + #endregion + + public void AddToBuffer(ref ArraySegment data) + { + int dataCount = data.Count; + /* Data will never get this large, it's quite impossible. + * Just ignore the data if it does, client is likely performing + * an attack. */ + if (dataCount > MAXIMUM_DATA_SIZE) + return; + + // If index exceeds buffer count. + if (BufferCount >= MAXIMUM_BUFFER_COUNT) + { + ForceAll = true; + return; + } + + /* If here, can write to buffer. */ + byte[] buffer = _buffers[BufferCount]; + Buffer.BlockCopy(data.Array, data.Offset, buffer, 0, dataCount); + _bufferLengths[BufferCount] = dataCount; + BufferCount++; + } + + /// + /// Sets referenced data to buffer and it's length for index. + /// + /// + /// + /// + public void GetBuffer(int index, ref byte[] buffer, ref int length) + { + if (index > _buffers.Count) + { + NetworkManagerExtensions.LogWarning("Index exceeds Buffers count."); + return; + } + if (index > _bufferLengths.Length) + { + NetworkManagerExtensions.LogWarning("Index exceeds BufferLengths count."); + return; + } + + buffer = _buffers[index]; + length = _bufferLengths[index]; + } + + /// + /// Resets buffers. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + BufferCount = 0; + ForceAll = false; + } + } + + /// + /// Information on how to smooth to a float value. + /// + private struct SmoothedFloat + { + public SmoothedFloat(float rate, float target) + { + Rate = rate; + Target = target; + } + + public readonly float Rate; + public readonly float Target; + } + + /// + /// Details about a trigger update. + /// + private struct TriggerUpdate + { + public byte ParameterIndex; + public bool Setting; + + public TriggerUpdate(byte parameterIndex, bool setting) + { + ParameterIndex = parameterIndex; + Setting = setting; + } + } + + /// + /// Details about an animator parameter. + /// + private class ParameterDetail + { + /// + /// Parameter information. + /// + public readonly AnimatorControllerParameter ControllerParameter = null; + /// + /// Index within the types collection for this parameters value. The exception is with triggers; if the parameter type is a trigger then a value of 1 is set, 0 is unset. + /// + public readonly byte TypeIndex = 0; + /// + /// Hash for the animator string. + /// + public readonly int Hash; + + public ParameterDetail(AnimatorControllerParameter controllerParameter, byte typeIndex) + { + ControllerParameter = controllerParameter; + TypeIndex = typeIndex; + Hash = controllerParameter.nameHash; + } + } + #endregion + + #region Private + + #region Private Profiler Markers + + private static readonly ProfilerMarker _pm_OnPreTick = new ProfilerMarker("NetworkAnimator.TimeManager_OnPreTick()"); + private static readonly ProfilerMarker _pm_OnPostTick = new ProfilerMarker("NetworkAnimator.TimeManager_OnPostTick()"); + private static readonly ProfilerMarker _pm_OnUpdate = new ProfilerMarker("NetworkAnimator.TimeManager_OnUpdate()"); + private static readonly ProfilerMarker _pm_CheckSendToServer = new ProfilerMarker("NetworkAnimator.CheckSendToServer()"); + private static readonly ProfilerMarker _pm_CheckSendToClients = new ProfilerMarker("NetworkAnimator.CheckSendToClients()"); + private static readonly ProfilerMarker _pm_SmoothFloats = new ProfilerMarker("NetworkAnimator.SmoothFloats()"); + private static readonly ProfilerMarker _pm_AnimatorUpdated = new ProfilerMarker("NetworkAnimator.AnimatorUpdated(ref ArraySegment, bool)"); + private static readonly ProfilerMarker _pm_ApplyParametersUpdated = new ProfilerMarker("NetworkAnimator.ApplyParametersUpdated(ref ArraySegment)"); + + #endregion + + #endregion + + #region Public. + /// + /// Parameters which will not be synchronized. + /// + [SerializeField] + [HideInInspector] + internal List IgnoredParameters = new(); + #endregion + + #region Serialized. + /// + /// The animator component to synchronize. + /// + [Tooltip("The animator component to synchronize.")] + [SerializeField] + private Animator _animator; + /// + /// The animator component to synchronize. + /// + public Animator Animator + { + get { return _animator; } + } + /// + /// True to synchronize changes even when the animator component is disabled. + /// + [Tooltip("True to synchronize changes even when the animator component is disabled.")] + [SerializeField] + private bool _synchronizeWhenDisabled; + /// + /// True to synchronize changes even when the animator component is disabled. + /// + public bool SynchronizeWhenDisabled + { + get { return _synchronizeWhenDisabled; } + set { _synchronizeWhenDisabled = value; } + } + /// + /// True to smooth float value changes for spectators. + /// + [Tooltip("True to smooth float value changes for spectators.")] + [SerializeField] + private bool _smoothFloats = true; + /// + /// How many ticks to interpolate. + /// + [Tooltip("How many ticks to interpolate.")] + [Range(1, NetworkTransform.MAX_INTERPOLATION)] + [SerializeField] + private ushort _interpolation = 2; + /// + /// + [Tooltip("True if using client authoritative animations.")] + [SerializeField] + private bool _clientAuthoritative = true; + /// + /// True if using client authoritative animations. + /// + public bool ClientAuthoritative + { + get { return _clientAuthoritative; } + } + /// + /// True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations. + /// + [Tooltip("True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations.")] + [SerializeField] + private bool _sendToOwner; + /// + /// True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations. + /// + public bool SendToOwner => _sendToOwner; + + #endregion + + #region Private. + /// + /// All parameter values, excluding triggers. + /// + private readonly List _parameterDetails = new(); + /// + /// Last int values. + /// + private readonly List _ints = new(); + /// + /// Last float values. + /// + private readonly List _floats = new(); + /// + /// Last bool values. + /// + private readonly List _bools = new(); + /// + /// Last layer weights. + /// + private float[] _layerWeights; + /// + /// Last speed. + /// + private float _speed; + /// + /// Trigger values set by using SetTrigger and ResetTrigger. + /// + private readonly List _triggerUpdates = new(); + // /// + // /// Updates going to clients. + // /// + // private List _toClientsBuffer = new(); + /// + /// Synchronization enabled state. True by default + /// + private bool _isSynchronizationEnabled = true; + /// + /// Returns if the animator is exist and can be synchronized. + /// + private bool _canSynchronizeAnimator + { + get + { + if (!_isSynchronizationEnabled) + return false; + + if (!_isAnimatorSet) + return false; + + if (_animator.enabled || _synchronizeWhenDisabled) + return true; + + return false; + } + } + /// + /// True if the animator is valid but not enabled. + /// + private bool _isAnimatorSet + { + get + { + bool failedChecks = _animator == null || _animator.runtimeAnimatorController == null; + return !failedChecks; + } + } + /// + /// Float valeus to smooth towards. + /// + private Dictionary _smoothedFloats = new(); + /// + /// Returns if floats can be smoothed for this client. + /// + private bool _canSmoothFloats + { + get + { + // Don't smooth on server only. + if (!IsClientStarted) + return false; + // Smoothing is disabled. + if (!_smoothFloats) + return false; + // No reason to smooth for self. + if (IsOwner && ClientAuthoritative) + return false; + + //Fall through. + return true; + } + } + /// + /// Layers which need to have their state synchronized. Key is the layer, Value is the state change information. + /// + private Dictionary _unsynchronizedLayerStates = new(); + /// + /// Last animator set. + /// + private Animator _lastAnimator; + /// + /// Last Controller set. + /// + private RuntimeAnimatorController _lastController; + /// + /// PooledWriter for this animator. + /// + private PooledWriter _writer = new(); + /// + /// Holds client authoritative updates received to send to other clients. + /// + private ClientAuthoritativeUpdate _clientAuthoritativeUpdates; + /// + /// True to forceAll next timed send. + /// + private bool _forceAllOnTimed; + /// + /// Animations received which should be applied. + /// + private Queue _fromServerBuffer = new(); + /// + /// Tick when the buffer may begin to run. + /// + private uint _startTick = TimeManagerCls.UNSET_TICK; + /// + /// True if subscribed to TimeManager for ticks. + /// + private bool _subscribedToTicks; + #endregion + + #region Const. + ///// + ///// How much time to fall behind when using smoothing. Only increase value if the smoothing is sometimes jittery. Recommended values are between 0 and 0.04. + ///// + //private const float INTERPOLATION = 0.02f; + /// + /// ParameterDetails index which indicates a layer weight change. + /// + private const byte LAYER_WEIGHT = 240; + /// + /// ParameterDetails index which indicates an animator speed change. + /// + private const byte SPEED = 241; + /// + /// ParameterDetails index which indicates a layer state change. + /// + private const byte STATE = 242; + /// + /// ParameterDetails index which indicates a crossfade change. + /// + private const byte CROSSFADE = 243; + #endregion + + private void Awake() + { + InitializeOnce(); + } + + private void OnDestroy() + { + ChangeTickSubscription(false); + } + + [APIExclude] + public override void OnSpawnServer(NetworkConnection connection) + { + if (!_canSynchronizeAnimator) + return; + if (AnimatorUpdated(out ArraySegment updatedBytes, true)) + TargetAnimatorUpdated(connection, updatedBytes); + } + + public override void OnStartNetwork() + { + ChangeTickSubscription(true); + _isSynchronizationEnabled = true; + } + + [APIExclude] + public override void OnStartServer() + { + //If using client authoritative then initialize clientAuthoritativeUpdates. + if (_clientAuthoritative) + { + _clientAuthoritativeUpdates = new(); + // //Expand to clients buffer count to however many buffers can be held. + // for (int i = 0; i < ClientAuthoritativeUpdate.MAXIMUM_BUFFER_COUNT; i++) + // _toClientsBuffer.Add(new byte[0]); + } + // else + // { + // _toClientsBuffer.Add(new byte[0]); + // } + } + + public override void OnStartClient() + { + TimeManager.OnUpdate += TimeManager_OnUpdate; + } + + public override void OnStopClient() + { + if (TimeManager != null) + TimeManager.OnUpdate -= TimeManager_OnUpdate; + } + + public override void OnStopNetwork() + { + _unsynchronizedLayerStates.Clear(); + ChangeTickSubscription(false); + } + + /// + /// Tries to subscribe to TimeManager ticks. + /// + private void ChangeTickSubscription(bool subscribe) + { + if (subscribe == _subscribedToTicks || NetworkManager == null) + return; + + _subscribedToTicks = subscribe; + if (subscribe) + { + NetworkManager.TimeManager.OnPreTick += TimeManager_OnPreTick; + NetworkManager.TimeManager.OnPostTick += TimeManager_OnPostTick; + } + else + { + NetworkManager.TimeManager.OnPreTick -= TimeManager_OnPreTick; + NetworkManager.TimeManager.OnPostTick -= TimeManager_OnPostTick; + } + } + + /// + /// Called right before a tick occurs, as well before data is read. + /// + private void TimeManager_OnPreTick() + { + using (_pm_OnPreTick.Auto()) + { + if (!_canSynchronizeAnimator) + { + _fromServerBuffer.Clear(); + return; + } + + //Disabled/cannot start. + if (_startTick == 0) + return; + //Nothing in queue. + if (_fromServerBuffer.Count == 0) + { + _startTick = 0; + return; + } + + //Not enough time has passed to start queue. + if (TimeManager.LocalTick < _startTick) + return; + + ReceivedServerData rd = _fromServerBuffer.Dequeue(); + ArraySegment segment = rd.GetArraySegment(); + ApplyParametersUpdated(ref segment); + rd.Dispose(); + } + } + + /* Use post tick values are checked after + * client has an opportunity to use OnTick. */ + /// + /// Called after a tick occurs; physics would have simulated if using PhysicsMode.TimeManager. + /// + private void TimeManager_OnPostTick() + { + using (_pm_OnPostTick.Auto()) + { + //One check rather than per each method. + if (!_canSynchronizeAnimator) + return; + + CheckSendToServer(); + CheckSendToClients(); + } + } + + private void TimeManager_OnUpdate() + { + using (_pm_OnUpdate.Auto()) + { + if (!_canSynchronizeAnimator) + return; + + if (IsClientStarted) + SmoothFloats(); + } + } + + /// + /// Initializes this script for use. + /// + private void InitializeOnce() + { + if (_animator == null) + _animator = GetComponent(); + + //Don't run the rest if not in play mode. + if (!ApplicationState.IsPlaying()) + return; + + if (!_canSynchronizeAnimator) + { + //Debug.LogWarning("Animator is null or not enabled; unable to initialize for animator. Use SetAnimator if animator was changed or enable the animator."); + return; + } + + //Speed. + _speed = _animator.speed; + + //Build layer weights. + _layerWeights = new float[_animator.layerCount]; + for (int i = 0; i < _layerWeights.Length; i++) + _layerWeights[i] = _animator.GetLayerWeight(i); + + _parameterDetails.Clear(); + _bools.Clear(); + _floats.Clear(); + _ints.Clear(); + //Create a parameter detail for each parameter that can be synchronized. + foreach (AnimatorControllerParameter item in _animator.parameters) + { + bool process = !_animator.IsParameterControlledByCurve(item.name); + //PROSTART + + //PROEND + if (process) + { + //Over 250 parameters; who would do this!? + if (_parameterDetails.Count == 240) + { + NetworkManager.LogError($"Parameter {item.name} exceeds the allowed 240 parameter count and is being ignored."); + continue; + } + + int typeIndex = 0; + //Bools. + if (item.type == AnimatorControllerParameterType.Bool) + { + typeIndex = _bools.Count; + _bools.Add(_animator.GetBool(item.nameHash)); + } + //Floats. + else if (item.type == AnimatorControllerParameterType.Float) + { + typeIndex = _floats.Count; + _floats.Add(_animator.GetFloat(item.name)); + } + //Ints. + else if (item.type == AnimatorControllerParameterType.Int) + { + typeIndex = _ints.Count; + _ints.Add(_animator.GetInteger(item.nameHash)); + } + //Triggers. + else if (item.type == AnimatorControllerParameterType.Trigger) + { + /* Triggers aren't persistent so they don't use stored values + * but I do need to make a parameter detail to track the hash. */ + typeIndex = -1; + } + + _parameterDetails.Add(new(item, (byte)typeIndex)); + } + } + } + + /// + /// Sets synchronization state to NetworkAnimator. Enabled by default. + /// + /// + public void SetSynchronizationState(bool state) + { + _isSynchronizationEnabled = state; + } + + /// + /// Sets which animator to use. You must call this with the appropriate animator on all clients and server. This change is not automatically synchronized. + /// + /// + public void SetAnimator(Animator animator) + { + //No update required. + if (animator == _lastAnimator) + return; + + _animator = animator; + InitializeOnce(); + _lastAnimator = animator; + } + + /// + /// Sets which controller to use. You must call this with the appropriate controller on all clients and server. This change is not automatically synchronized. + /// + /// + public void SetController(RuntimeAnimatorController controller) + { + //No update required. + if (controller == _lastController) + return; + + _animator.runtimeAnimatorController = controller; + InitializeOnce(); + _lastController = controller; + } + + /// + /// Checks to send animator data from server to clients. + /// + private void CheckSendToServer() + { + using (_pm_CheckSendToServer.Auto()) + { + //Cannot send to server if is server or not client. + if (IsServerStarted || !IsClientInitialized) + return; + //Cannot send to server if not client authoritative or don't have authority. + if (!ClientAuthoritative || !IsOwner) + return; + + /* If there are updated parameters to send. + * Don't really need to worry about mtu here + * because there's no way the sent bytes are + * ever going to come close to the mtu + * when sending a single update. */ + if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) + ServerAnimatorUpdated(updatedBytes); + + _forceAllOnTimed = false; + } + } + + /// + /// Checks to send animator data from server to clients. + /// + private void CheckSendToClients() + { + using (_pm_CheckSendToClients.Auto()) + { + //Cannot send to clients if not server initialized. + if (!IsServerInitialized) + return; + + bool sendFromServer; + //If client authoritative. + if (ClientAuthoritative) + { + //If has no owner then use latest values on server. + if (!Owner.IsValid) + { + sendFromServer = true; + } + //If has a owner. + else + { + //If is owner then send latest values on server. + if (IsOwner) + { + sendFromServer = true; + } + //Not owner. + else + { + //Haven't received any data from clients, cannot send yet. + if (_clientAuthoritativeUpdates.BufferCount == 0) + { + return; + } + //Data was received from client; check eligibility to send it. + else + { + /* If forceAll is true then the latest values on + * server must be used, rather than what was received + * from client. This can occur if the client is possibly + * trying to use an attack or if the client is + * excessively sending updates. To prevent relaying that + * same data to others the server will send it's current + * animator settings in this scenario. */ + if (_clientAuthoritativeUpdates.ForceAll) + { + sendFromServer = true; + _clientAuthoritativeUpdates.Reset(); + } + else + { + sendFromServer = false; + } + } + } + } + } + //Not client authoritative, always send from server. + else + { + sendFromServer = true; + } + + /* If client authoritative then use what was received from clients + * if data exist. */ + if (!sendFromServer) + { + byte[] buffer = null; + int bufferLength = 0; + for (int i = 0; i < _clientAuthoritativeUpdates.BufferCount; i++) + { + _clientAuthoritativeUpdates.GetBuffer(i, ref buffer, ref bufferLength); + + //If null was returned then something went wrong. + if (buffer == null || bufferLength == 0) + continue; + + SendSegment(new(buffer, 0, bufferLength)); + } + + //Reset client auth buffer. + _clientAuthoritativeUpdates.Reset(); + } + //Sending from server, send what's changed. + else + { + if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) + SendSegment(updatedBytes); + + _forceAllOnTimed = false; + } + + //Sends segment to clients + void SendSegment(ArraySegment data) + { + foreach (NetworkConnection nc in Observers) + { + //If to not send to owner. + if (!_sendToOwner && nc == Owner) + continue; + TargetAnimatorUpdated(nc, data); + } + } + } + } + + /// + /// Smooths floats on clients. + /// + private void SmoothFloats() + { + using (_pm_SmoothFloats.Auto()) + { + //Don't need to smooth on authoritative client. + if (!_canSmoothFloats) + return; + //Nothing to smooth. + if (_smoothedFloats.Count == 0) + return; + + float deltaTime = Time.deltaTime; + + List finishedEntries = new(); + + /* Cycle through each target float and move towards it. + * Once at a target float mark it to be removed from floatTargets. */ + foreach (KeyValuePair item in _smoothedFloats) + { + float current = _animator.GetFloat(item.Key); + float next = Mathf.MoveTowards(current, item.Value.Target, item.Value.Rate * deltaTime); + _animator.SetFloat(item.Key, next); + + if (next == item.Value.Target) + finishedEntries.Add(item.Key); + } + + //Remove finished entries from dictionary. + for (int i = 0; i < finishedEntries.Count; i++) + _smoothedFloats.Remove(finishedEntries[i]); + } + } + + /// + /// Returns if animator is updated and bytes of updated values. + /// + /// + private bool AnimatorUpdated(out ArraySegment updatedBytes, bool forceAll = false) + { + using (_pm_AnimatorUpdated.Auto()) + { + updatedBytes = default; + //Something isn't setup right. + if (_layerWeights == null) + return false; + //Reset the writer. + _writer.Clear(); + + /* Every time a parameter is updated a byte is added + * for it's index, this is why requiredBytes increases + * by 1 when a value updates. ChangedParameter contains + * the index updated and the new value. The requiresBytes + * is increased also by however many bytes are required + * for the type which has changed. Some types use special parameter + * detail indexes, such as layer weights; these can be found under const. */ + for (byte parameterIndex = 0; parameterIndex < _parameterDetails.Count; parameterIndex++) + { + ParameterDetail pd = _parameterDetails[parameterIndex]; + /* Bool. */ + if (pd.ControllerParameter.type == AnimatorControllerParameterType.Bool) + { + bool next = _animator.GetBool(pd.Hash); + //If changed. + if (forceAll || _bools[pd.TypeIndex] != next) + { + _writer.WriteUInt8Unpacked(parameterIndex); + _writer.WriteBoolean(next); + _bools[pd.TypeIndex] = next; + } + } + /* Float. */ + else if (pd.ControllerParameter.type == AnimatorControllerParameterType.Float) + { + float next = _animator.GetFloat(pd.Hash); + //If changed. + if (forceAll || _floats[pd.TypeIndex] != next) + { + _writer.WriteUInt8Unpacked(parameterIndex); + _writer.WriteSingle(next); + _floats[pd.TypeIndex] = next; + } + } + /* Int. */ + else if (pd.ControllerParameter.type == AnimatorControllerParameterType.Int) + { + int next = _animator.GetInteger(pd.Hash); + //If changed. + if (forceAll || _ints[pd.TypeIndex] != next) + { + _writer.WriteUInt8Unpacked(parameterIndex); + _writer.WriteInt32(next); + _ints[pd.TypeIndex] = next; + } + } + } + + /* Don't need to force trigger sends since + * they're one-shots. */ + for (int i = 0; i < _triggerUpdates.Count; i++) + { + _writer.WriteUInt8Unpacked(_triggerUpdates[i].ParameterIndex); + _writer.WriteBoolean(_triggerUpdates[i].Setting); + } + + _triggerUpdates.Clear(); + + /* States. */ + if (forceAll) + { + //Add all layers to layer states. + for (int i = 0; i < _animator.layerCount; i++) + _unsynchronizedLayerStates[i] = new(Time.frameCount); + } + + /* Only iterate if the collection has values. This is to avoid some + * unnecessary caching when collection is empty. */ + if (_unsynchronizedLayerStates.Count > 0) + { + int frameCount = Time.frameCount; + List sentLayers = CollectionCaches.RetrieveList(); + //Go through each layer which needs to be synchronized. + foreach (KeyValuePair item in _unsynchronizedLayerStates) + { + /* If a frame has not passed since the state was created + * then do not send it until next tick. State changes take 1 frame + * to be processed by Unity, this check ensures that. */ + if (frameCount == item.Value.FrameCount) + continue; + + //Add to layers being sent. This is so they can be removed from the collection later. + sentLayers.Add(item.Key); + int layerIndex = item.Key; + StateChange sc = item.Value; + //If a regular state change. + if (!sc.IsCrossfade) + { + if (ReturnCurrentLayerState(out int stateHash, out float normalizedTime, layerIndex)) + { + _writer.WriteUInt8Unpacked(STATE); + _writer.WriteUInt8Unpacked((byte)layerIndex); + //Current hash will always be too large to compress. + _writer.WriteInt32Unpacked(stateHash); + _writer.WriteSingle(normalizedTime); + } + } + //When it's a crossfade then send crossfade data. + else + { + _writer.WriteUInt8Unpacked(CROSSFADE); + _writer.WriteUInt8Unpacked((byte)layerIndex); + //Current hash will always be too large to compress. + _writer.WriteInt32(sc.Hash); + _writer.WriteBoolean(sc.FixedTime); + //Times usually can be compressed. + _writer.WriteSingle(sc.DurationTime); + _writer.WriteSingle(sc.OffsetTime); + _writer.WriteSingle(sc.NormalizedTransitionTime); + } + } + + if (sentLayers.Count > 0) + { + for (int i = 0; i < sentLayers.Count; i++) + _unsynchronizedLayerStates.Remove(sentLayers[i]); + //Store cache. + CollectionCaches.Store(sentLayers); + } + } + + /* Layer weights. */ + for (int layerIndex = 0; layerIndex < _layerWeights.Length; layerIndex++) + { + float next = _animator.GetLayerWeight(layerIndex); + if (forceAll || _layerWeights[layerIndex] != next) + { + _writer.WriteUInt8Unpacked(LAYER_WEIGHT); + _writer.WriteUInt8Unpacked((byte)layerIndex); + _writer.WriteSingle(next); + _layerWeights[layerIndex] = next; + } + } + + /* Speed is similar to layer weights but we don't need the index, + * only the indicator and value. */ + float speedNext = _animator.speed; + if (forceAll || _speed != speedNext) + { + _writer.WriteUInt8Unpacked(SPEED); + _writer.WriteSingle(speedNext); + _speed = speedNext; + } + + //Nothing to update. + if (_writer.Position == 0) + return false; + + updatedBytes = _writer.GetArraySegment(); + return true; + } + } + + /// + /// Applies changed parameters to the animator. + /// + /// + private void ApplyParametersUpdated(ref ArraySegment updatedParameters) + { + using (_pm_ApplyParametersUpdated.Auto()) + { + if (!_canSynchronizeAnimator) + return; + if (_layerWeights == null) + return; + if (updatedParameters.Count == 0) + return; + + PooledReader reader = ReaderPool.Retrieve(updatedParameters, NetworkManager); + + try + { + while (reader.Remaining > 0) + { + byte parameterIndex = reader.ReadUInt8Unpacked(); + //Layer weight + if (parameterIndex == LAYER_WEIGHT) + { + byte layerIndex = reader.ReadUInt8Unpacked(); + float value = reader.ReadSingle(); + _animator.SetLayerWeight((int)layerIndex, value); + } + //Speed. + else if (parameterIndex == SPEED) + { + float value = reader.ReadSingle(); + _animator.speed = value; + } + //State. + else if (parameterIndex == STATE) + { + byte layerIndex = reader.ReadUInt8Unpacked(); + //Hashes will always be too large to compress. + int hash = reader.ReadInt32Unpacked(); + float normalizedTime = reader.ReadSingle(); + //Play results. + _animator.Play(hash, layerIndex, normalizedTime); + } + //Crossfade. + else if (parameterIndex == CROSSFADE) + { + byte layerIndex = reader.ReadUInt8Unpacked(); + //Hashes will always be too large to compress. + int hash = reader.ReadInt32(); + bool useFixedTime = reader.ReadBoolean(); + //Get time values. + float durationTime = reader.ReadSingle(); + float offsetTime = reader.ReadSingle(); + float normalizedTransitionTime = reader.ReadSingle(); + //If using fixed. + if (useFixedTime) + _animator.CrossFadeInFixedTime(hash, durationTime, layerIndex, offsetTime, normalizedTransitionTime); + else + _animator.CrossFade(hash, durationTime, layerIndex, offsetTime, normalizedTransitionTime); + } + //Not a predetermined index, is an actual parameter. + else + { + AnimatorControllerParameterType acpt = _parameterDetails[parameterIndex].ControllerParameter.type; + if (acpt == AnimatorControllerParameterType.Bool) + { + bool value = reader.ReadBoolean(); + _animator.SetBool(_parameterDetails[parameterIndex].Hash, value); + } + //Float. + else if (acpt == AnimatorControllerParameterType.Float) + { + float value = reader.ReadSingle(); + //If able to smooth floats. + if (_canSmoothFloats) + { + float currentValue = _animator.GetFloat(_parameterDetails[parameterIndex].Hash); + float past = (float)TimeManager.TickDelta; + //float past = _synchronizeInterval + INTERPOLATION; + float rate = Mathf.Abs(currentValue - value) / past; + _smoothedFloats[_parameterDetails[parameterIndex].Hash] = new(rate, value); + } + else + { + _animator.SetFloat(_parameterDetails[parameterIndex].Hash, value); + } + } + //Integer. + else if (acpt == AnimatorControllerParameterType.Int) + { + int value = reader.ReadInt32(); + _animator.SetInteger(_parameterDetails[parameterIndex].Hash, value); + } + //Trigger. + else if (acpt == AnimatorControllerParameterType.Trigger) + { + bool value = reader.ReadBoolean(); + if (value) + _animator.SetTrigger(_parameterDetails[parameterIndex].Hash); + else + _animator.ResetTrigger(_parameterDetails[parameterIndex].Hash); + } + //Unhandled. + else + { + NetworkManager.LogWarning($"Unhandled parameter type of {acpt}."); + } + } + } + } + catch + { + NetworkManager.LogWarning("An error occurred while applying updates. This may occur when malformed data is sent or when you change the animator or controller but not on all connections."); + } + finally + { + reader?.Store(); + } + } + } + + /// + /// Outputs the current state and time for a layer. Returns true if stateHash is not 0. + /// + /// + /// + /// + /// + /// + private bool ReturnCurrentLayerState(out int stateHash, out float normalizedTime, int layerIndex) + { + stateHash = 0; + normalizedTime = 0f; + + if (!_canSynchronizeAnimator) + return false; + + AnimatorStateInfo st = _animator.GetCurrentAnimatorStateInfo(layerIndex); + stateHash = st.fullPathHash; + normalizedTime = st.normalizedTime; + + return stateHash != 0; + } + + /// + /// Immediately sends all variables and states of layers. + /// This is a very bandwidth intensive operation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SendAll() + { + _forceAllOnTimed = true; + } + + #region Play. + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Play(string name) + { + Play(Animator.StringToHash(name)); + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Play(int hash) + { + for (int i = 0; i < _animator.layerCount; i++) + Play(hash, i, 0f); + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Play(string name, int layer) + { + Play(Animator.StringToHash(name), layer); + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Play(int hash, int layer) + { + Play(hash, layer, 0f); + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Play(string name, int layer, float normalizedTime) + { + Play(Animator.StringToHash(name), layer, normalizedTime); + } + + /// + /// Plays a state. + /// + public void Play(int hash, int layer, float normalizedTime) + { + if (!_canSynchronizeAnimator) + return; + if (_animator.HasState(layer, hash) || hash == 0) + { + _animator.Play(hash, layer, normalizedTime); + _unsynchronizedLayerStates[layer] = new(Time.frameCount); + } + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PlayInFixedTime(string name, float fixedTime) + { + PlayInFixedTime(Animator.StringToHash(name), fixedTime); + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PlayInFixedTime(int hash, float fixedTime) + { + for (int i = 0; i < _animator.layerCount; i++) + PlayInFixedTime(hash, i, fixedTime); + } + + /// + /// Plays a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PlayInFixedTime(string name, int layer, float fixedTime) + { + PlayInFixedTime(Animator.StringToHash(name), layer, fixedTime); + } + + /// + /// Plays a state. + /// + public void PlayInFixedTime(int hash, int layer, float fixedTime) + { + if (!_canSynchronizeAnimator) + return; + if (_animator.HasState(layer, hash) || hash == 0) + { + _animator.PlayInFixedTime(hash, layer, fixedTime); + _unsynchronizedLayerStates[layer] = new(Time.frameCount); + } + } + #endregion + + #region Crossfade. + /// + /// Creates a crossfade from the current state to any other state using normalized times. + /// + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CrossFade(string stateName, float normalizedTransitionDuration, int layer, float normalizedTimeOffset = float.NegativeInfinity, float normalizedTransitionTime = 0.0f) + { + CrossFade(Animator.StringToHash(stateName), normalizedTransitionDuration, layer, normalizedTimeOffset, normalizedTransitionTime); + } + + /// + /// Creates a crossfade from the current state to any other state using normalized times. + /// + /// + /// + /// + /// + /// + public void CrossFade(int hash, float normalizedTransitionDuration, int layer, float normalizedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) + { + if (!_canSynchronizeAnimator) + return; + if (_animator.HasState(layer, hash) || hash == 0) + { + _animator.CrossFade(hash, normalizedTransitionDuration, layer, normalizedTimeOffset, normalizedTransitionTime); + _unsynchronizedLayerStates[layer] = new(Time.frameCount, hash, false, normalizedTransitionDuration, normalizedTimeOffset, normalizedTransitionTime); + } + } + + /// + /// Creates a crossfade from the current state to any other state using times in seconds. + /// + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CrossFadeInFixedTime(string stateName, float fixedTransitionDuration, int layer, float fixedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) + { + CrossFadeInFixedTime(Animator.StringToHash(stateName), fixedTransitionDuration, layer, fixedTimeOffset, normalizedTransitionTime); + } + + /// + /// Creates a crossfade from the current state to any other state using times in seconds. + /// + /// + /// + /// + /// + /// + public void CrossFadeInFixedTime(int hash, float fixedTransitionDuration, int layer, float fixedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) + { + if (!_canSynchronizeAnimator) + return; + if (_animator.HasState(layer, hash) || hash == 0) + { + _animator.CrossFadeInFixedTime(hash, fixedTransitionDuration, layer, fixedTimeOffset, normalizedTransitionTime); + _unsynchronizedLayerStates[layer] = new(Time.frameCount, hash, true, fixedTransitionDuration, fixedTimeOffset, normalizedTransitionTime); + } + } + #endregion + + #region Triggers. + /// + /// Sets a trigger on the animator and sends it over the network. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetTrigger(int hash) + { + if (!_canSynchronizeAnimator) + return; + UpdateTrigger(hash, true); + } + + /// + /// Sets a trigger on the animator and sends it over the network. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetTrigger(string name) + { + SetTrigger(Animator.StringToHash(name)); + } + + /// + /// Resets a trigger on the animator and sends it over the network. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ResetTrigger(int hash) + { + UpdateTrigger(hash, false); + } + + /// + /// Resets a trigger on the animator and sends it over the network. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ResetTrigger(string name) + { + ResetTrigger(Animator.StringToHash(name)); + } + + /// + /// Updates a trigger, sets or resets. + /// + /// + private void UpdateTrigger(int hash, bool set) + { + if (!_canSynchronizeAnimator) + return; + + bool clientAuth = ClientAuthoritative; + //If there is an owner perform checks. + if (Owner.IsValid) + { + //If client auth and not owner. + if (clientAuth && !IsOwner) + return; + } + //There is no owner. + else + { + if (!IsServerStarted) + return; + } + + //Update locally. + if (set) + _animator.SetTrigger(hash); + else + _animator.ResetTrigger(hash); + + /* Can send if any of the following are true: + * ClientAuth + Owner. + * ClientAuth + No Owner + IsServer + * !ClientAuth + IsServer. */ + bool canSend = (clientAuth && IsOwner) || (clientAuth && !Owner.IsValid) || (!clientAuth && IsServerStarted); + + //Only queue a send if proper side. + if (canSend) + { + for (byte i = 0; i < _parameterDetails.Count; i++) + { + if (_parameterDetails[i].Hash == hash) + { + _triggerUpdates.Add(new(i, set)); + return; + } + } + //Fall through, hash not found. + NetworkManager.LogWarning($"Hash {hash} not found while trying to update a trigger."); + } + } + #endregion + + #region Remote actions. + /// + /// Called on clients to receive an animator update. + /// + /// + [TargetRpc(ValidateTarget = false)] + private void TargetAnimatorUpdated(NetworkConnection connection, ArraySegment data) + { + if (!_canSynchronizeAnimator) + return; + + //If receiver is client host then do nothing, clientHost need not process. + if (IsServerInitialized && connection.IsLocalClient) + return; + + bool clientAuth = ClientAuthoritative; + bool isOwner = IsOwner; + /* If set for client auth and owner then do not process. + * This could be the case if an update was meant to come before + * ownership gain but came out of late due to out of order when using unreliable. + * Cannot check sendToOwner given clients may not + * always be aware of owner depending on ShareIds setting. */ + if (clientAuth && isOwner) + return; + /* If not client auth and not to send to owner, and is owner + * then also return. */ + if (!clientAuth && !_sendToOwner && isOwner) + return; + + ReceivedServerData rd = new(data); + _fromServerBuffer.Enqueue(rd); + + if (_startTick == 0) + _startTick = TimeManager.LocalTick + _interpolation; + } + + /// + /// Called on server to receive an animator update. + /// + /// + [ServerRpc] + private void ServerAnimatorUpdated(ArraySegment data) + { + if (!_canSynchronizeAnimator) + return; + if (!ClientAuthoritative) + { + Owner.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection Id {Owner.ClientId} has been kicked for trying to update this object without client authority."); + return; + } + + /* Server does not need to apply interpolation. + * Even as clientHost when CSP is being used the + * clientHost will always be on the latest tick. + * Spectators on the other hand will remain behind + * a little depending on their components interpolation. */ + ApplyParametersUpdated(ref data); + _clientAuthoritativeUpdates.AddToBuffer(ref data); + } + #endregion + + #region Editor. + #if UNITY_EDITOR + protected override void Reset() + { + base.Reset(); + if (_animator == null) + SetAnimator(GetComponent()); + } + #endif + #endregion + } +} \ No newline at end of file diff --git a/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs b/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs index d3e6ca6d..6e717c86 100644 --- a/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs +++ b/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs @@ -143,7 +143,7 @@ public void SetFrameRate(ushort value) private SplitReader _splitReader = new(); /// /// - private NetworkTrafficStatistics _networkTrafficStatistics; + [NonSerialized] private NetworkTrafficStatistics _networkTrafficStatistics; #endregion #region Private Profiler Markers diff --git a/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs b/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs index 461c5418..9435fbca 100644 --- a/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs +++ b/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs @@ -99,7 +99,7 @@ internal void OnClientConnectionState(ClientConnectionStateArgs args) /* Clear spawned and scene objects as they will be rebuilt. * Spawned would have already be cleared if DespawnSpawned * was called but it won't hurt anything clearing an empty collection. */ - Spawned.Clear(); + HandleClear(); SceneObjects_Internal.Clear(); } } diff --git a/Assets/FishNet/Runtime/Managing/Client/Object/ObjectCaching.cs b/Assets/FishNet/Runtime/Managing/Client/Object/ObjectCaching.cs index 17dd0c81..87b53a15 100644 --- a/Assets/FishNet/Runtime/Managing/Client/Object/ObjectCaching.cs +++ b/Assets/FishNet/Runtime/Managing/Client/Object/ObjectCaching.cs @@ -501,7 +501,7 @@ internal NetworkObject GetSpawnedObject(int objectId) //If not found in Spawning then check Spawned. if (!IteratedSpawningObjects.TryGetValue(objectId, out result)) { - Dictionary spawned = _networkManager.IsHostStarted ? _networkManager.ServerManager.Objects.Spawned : _networkManager.ClientManager.Objects.Spawned; + IReadOnlyDictionary spawned = _networkManager.IsHostStarted ? _networkManager.ServerManager.Objects.Spawned : _networkManager.ClientManager.Objects.Spawned; spawned.TryGetValue(objectId, out result); } diff --git a/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs b/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs index 41d9c480..5e52c507 100644 --- a/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs +++ b/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs @@ -23,10 +23,20 @@ namespace FishNet.Managing.Object public abstract partial class ManagedObjects { #region Public. + /// /// NetworkObjects which are currently active. /// - public Dictionary Spawned = new(); + + private readonly Dictionary _spawned = new(); + public IReadOnlyDictionary Spawned => _spawned; + + public delegate void OnSpawnedChanged(int objectId, NetworkObject networkObject); + + public event OnSpawnedChanged OnSpawnedAdd; + public event OnSpawnedChanged OnSpawnedRemove; + public event Action OnSpawnedClear; + #endregion #region Protected. @@ -55,7 +65,26 @@ protected internal virtual bool GetNextNetworkObjectId(out int nextNetworkObject public IReadOnlyDictionary SceneObjects => SceneObjects_Internal; /// /// - protected NetworkTrafficStatistics NetworkTrafficStatistics; + [NonSerialized] protected NetworkTrafficStatistics NetworkTrafficStatistics; + + protected void HandleAdd(NetworkObject nob) + { + _spawned[nob.ObjectId] = nob; + OnSpawnedAdd?.Invoke(nob.ObjectId, nob); + } + + protected void HandleRemove(NetworkObject nob) + { + if (_spawned.Remove(nob.ObjectId)) + OnSpawnedAdd?.Invoke(nob.ObjectId, nob); + } + + protected void HandleClear() + { + _spawned.Clear(); + OnSpawnedClear?.Invoke(); + } + #endregion #region Private. @@ -109,7 +138,7 @@ internal virtual void NetworkObjectDestroyed(NetworkObject nob, bool asServer) /// protected virtual void RemoveFromSpawned(NetworkObject nob, bool fromOnDestroy, bool asServer) { - Spawned.Remove(nob.ObjectId); + HandleRemove(nob); // Do the same with SceneObjects. if (fromOnDestroy && nob.IsSceneObject) RemoveFromSceneObjects(nob); @@ -316,7 +345,7 @@ internal virtual void DespawnWithoutSynchronization(bool recursive, bool asServe DespawnWithoutSynchronization(nob, recursive, asServer, nob.GetDefaultDespawnType(), removeFromSpawned: false); } - Spawned.Clear(); + HandleClear(); } /// @@ -371,7 +400,7 @@ protected virtual void DespawnWithoutSynchronization(NetworkObject nob, bool rec /// internal virtual void AddToSpawned(NetworkObject nob, bool asServer) { - Spawned[nob.ObjectId] = nob; + HandleAdd(nob); } /// @@ -408,7 +437,7 @@ protected internal void RemoveFromSceneObjects(ulong sceneId) protected internal NetworkObject GetSpawnedNetworkObject(int objectId) { NetworkObject r; - if (!Spawned.TryGetValueIL2CPP(objectId, out r)) + if (!_spawned.TryGetValueIL2CPP(objectId, out r)) NetworkManager.LogError($"Spawned NetworkObject not found for ObjectId {objectId}."); return r; diff --git a/Assets/FishNet/Runtime/Managing/Server/ServerManager.QOL.cs b/Assets/FishNet/Runtime/Managing/Server/ServerManager.QOL.cs index 64ad0aa0..eeef7a1a 100644 --- a/Assets/FishNet/Runtime/Managing/Server/ServerManager.QOL.cs +++ b/Assets/FishNet/Runtime/Managing/Server/ServerManager.QOL.cs @@ -178,14 +178,15 @@ public void Despawn(NetworkObject networkObject, DespawnType? despawnType = null /// Reason client is being kicked. /// How to print logging as. /// Optional message to be debug logged. - public void Kick(NetworkConnection conn, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "") + /// + public void Kick(NetworkConnection conn, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "", bool immediately = true) { if (!conn.IsValid) return; OnClientKick?.Invoke(conn, conn.ClientId, kickReason); if (conn.IsActive) - conn.Disconnect(true); + conn.Disconnect(immediately); if (!string.IsNullOrEmpty(log)) NetworkManager.Log(loggingType, log); @@ -198,10 +199,11 @@ public void Kick(NetworkConnection conn, KickReason kickReason, LoggingType logg /// Reason client is being kicked. /// How to print logging as. /// Optional message to be debug logged. - public void Kick(int clientId, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "") + /// + public void Kick(int clientId, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "", bool immediately = true) { OnClientKick?.Invoke(null, clientId, kickReason); - NetworkManager.TransportManager.Transport.StopConnection(clientId, true); + NetworkManager.TransportManager.Transport.StopConnection(clientId, immediately); if (!string.IsNullOrEmpty(log)) NetworkManager.Log(loggingType, log); } @@ -214,10 +216,11 @@ public void Kick(int clientId, KickReason kickReason, LoggingType loggingType = /// Reason client is being kicked. /// How to print logging as. /// Optional message to be debug logged. - public void Kick(NetworkConnection conn, Reader reader, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "") + /// + public void Kick(NetworkConnection conn, Reader reader, KickReason kickReason, LoggingType loggingType = LoggingType.Common, string log = "", bool immediately = true) { reader.Clear(); - Kick(conn, kickReason, loggingType, log); + Kick(conn, kickReason, loggingType, log, immediately); } } } \ No newline at end of file diff --git a/Assets/FishNet/Runtime/Managing/Server/ServerManager.cs b/Assets/FishNet/Runtime/Managing/Server/ServerManager.cs index 55d879d0..1365a5b8 100644 --- a/Assets/FishNet/Runtime/Managing/Server/ServerManager.cs +++ b/Assets/FishNet/Runtime/Managing/Server/ServerManager.cs @@ -222,7 +222,7 @@ public void SetFrameRate(ushort value) private SplitReader _splitReader = new(); /// /// - private NetworkTrafficStatistics _networkTrafficStatistics; + [NonSerialized] private NetworkTrafficStatistics _networkTrafficStatistics; #if DEVELOPMENT /// /// Logs data about parser to help debug. diff --git a/Assets/FishNet/Runtime/Managing/Timing/TimeManager.cs b/Assets/FishNet/Runtime/Managing/Timing/TimeManager.cs index c81ec7d1..58c37480 100644 --- a/Assets/FishNet/Runtime/Managing/Timing/TimeManager.cs +++ b/Assets/FishNet/Runtime/Managing/Timing/TimeManager.cs @@ -8,6 +8,7 @@ using System; using System.Runtime.CompilerServices; using FishNet.Managing.Statistic; +using Unity.Mathematics; using Unity.Profiling; using UnityEngine; using SystemStopwatch = System.Diagnostics.Stopwatch; @@ -284,10 +285,11 @@ public void SetPhysicsTimeScale(float value) /// /// - private NetworkTrafficStatistics _networkTrafficStatistics; + [NonSerialized] private NetworkTrafficStatistics _networkTrafficStatistics; #endregion #region Private Profiler Markers + private static readonly ProfilerMarker _pm_IncreaseTick = new("TimeManager.IncreaseTick()"); private static readonly ProfilerMarker _pm_OnFixedUpdate = new("TimeManager.OnFixedUpdate()"); private static readonly ProfilerMarker _pm_OnPostPhysicsSimulation = new("TimeManager.OnPostPhysicsSimulation(float)"); private static readonly ProfilerMarker _pm_OnPrePhysicsSimulation = new("TimeManager.OnPrePhysicsSimulation(float)"); @@ -683,97 +685,100 @@ internal void SendPong(NetworkConnection conn, uint clientTick) /// private void IncreaseTick() { - bool isClient = NetworkManager.IsClientStarted; - bool isServer = NetworkManager.IsServerStarted; - - double timePerSimulation = isServer ? TickDelta : _adjustedTickDelta; - if (timePerSimulation == 0d) + using (_pm_IncreaseTick.Auto()) { - NetworkManager.LogWarning($"Simulation delta cannot be 0. Network timing will not continue."); - return; - } - - double time = Time.unscaledDeltaTime; + bool isClient = NetworkManager.IsClientStarted; + bool isServer = NetworkManager.IsServerStarted; - _elapsedTickTime += time; - FrameTicked = _elapsedTickTime >= timePerSimulation; + double timePerSimulation = isServer ? TickDelta : _adjustedTickDelta; + if (timePerSimulation == 0d) + { + NetworkManager.LogWarning($"Simulation delta cannot be 0. Network timing will not continue."); + return; + } - // Number of ticks to occur this frame. - int ticksCount = Mathf.FloorToInt((float)(_elapsedTickTime / timePerSimulation)); - if (ticksCount > 1) - _lastMultipleTicksTime = Time.unscaledTime; + double time = Time.unscaledDeltaTime; - if (_allowTickDropping) - { - // If ticks require dropping. Set exactly to maximum ticks. - if (ticksCount > _maximumFrameTicks) - _elapsedTickTime = timePerSimulation * (double)_maximumFrameTicks; - } + _elapsedTickTime += time; + FrameTicked = _elapsedTickTime >= timePerSimulation; - bool variableTiming = _timingType == TimingType.Variable; - bool frameTicked = FrameTicked; - float tickDelta = (float)TickDelta * GetPhysicsTimeScale(); + // Number of ticks to occur this frame. + int ticksCount = Mathf.FloorToInt((float)(_elapsedTickTime / timePerSimulation)); + if (ticksCount > 1) + _lastMultipleTicksTime = Time.unscaledTime; - do - { - if (frameTicked) + if (_allowTickDropping) { - using (_pm_OnPreTick.Auto()) - OnPreTick?.Invoke(); + // If ticks require dropping. Set exactly to maximum ticks. + if (ticksCount > _maximumFrameTicks) + _elapsedTickTime = timePerSimulation * (double)_maximumFrameTicks; } - /* This has to be called inside the loop because - * OnPreTick promises data hasn't been read yet. - * Therefor iterate must occur after OnPreTick. - * Iteration will only run once per frame. */ - if (frameTicked || variableTiming) - TryIterateData(true); + bool variableTiming = _timingType == TimingType.Variable; + bool frameTicked = FrameTicked; + float tickDelta = (float)TickDelta * GetPhysicsTimeScale(); - if (frameTicked) + do { - // Tell predicted objecs to reconcile before OnTick. - NetworkManager.PredictionManager.ReconcileToStates(); + if (frameTicked) + { + using (_pm_OnPreTick.Auto()) + OnPreTick?.Invoke(); + } - using (_pm_OnTick.Auto()) - OnTick?.Invoke(); + /* This has to be called inside the loop because + * OnPreTick promises data hasn't been read yet. + * Therefor iterate must occur after OnPreTick. + * Iteration will only run once per frame. */ + if (frameTicked || variableTiming) + TryIterateData(true); - if (PhysicsMode == PhysicsMode.TimeManager && tickDelta > 0f) + if (frameTicked) { - using (_pm_OnPrePhysicsSimulation.Auto()) - OnPrePhysicsSimulation?.Invoke(tickDelta); - using (_pm_PhysicsSimulate.Auto()) - Physics.Simulate(tickDelta); - using (_pm_Physics2DSimulate.Auto()) - Physics2D.Simulate(tickDelta); - using (_pm_OnPostPhysicsSimulation.Auto()) - OnPostPhysicsSimulation?.Invoke(tickDelta); + // Tell predicted objecs to reconcile before OnTick. + NetworkManager.PredictionManager.ReconcileToStates(); + + using (_pm_OnTick.Auto()) + OnTick?.Invoke(); + + if (PhysicsMode == PhysicsMode.TimeManager && tickDelta > 0f) + { + using (_pm_OnPrePhysicsSimulation.Auto()) + OnPrePhysicsSimulation?.Invoke(tickDelta); + using (_pm_PhysicsSimulate.Auto()) + Physics.Simulate(tickDelta); + using (_pm_Physics2DSimulate.Auto()) + Physics2D.Simulate(tickDelta); + using (_pm_OnPostPhysicsSimulation.Auto()) + OnPostPhysicsSimulation?.Invoke(tickDelta); + } + + using (_pm_OnPostTick.Auto()) + OnPostTick?.Invoke(); + // After post tick send states. + NetworkManager.PredictionManager.SendStateUpdate(); + + /* If isClient this is the + * last tick during this loop. */ + bool lastTick = _elapsedTickTime < timePerSimulation * 2d; + if (isClient && lastTick) + TrySendPing(LocalTick + 1); + if (NetworkManager.IsServerStarted) + SendTimingAdjustment(); } - using (_pm_OnPostTick.Auto()) - OnPostTick?.Invoke(); - // After post tick send states. - NetworkManager.PredictionManager.SendStateUpdate(); - - /* If isClient this is the - * last tick during this loop. */ - bool lastTick = _elapsedTickTime < timePerSimulation * 2d; - if (isClient && lastTick) - TrySendPing(LocalTick + 1); - if (NetworkManager.IsServerStarted) - SendTimingAdjustment(); - } - - // Send out data. - if (frameTicked || variableTiming) - TryIterateData(false); + // Send out data. + if (frameTicked || variableTiming) + TryIterateData(false); - if (frameTicked) - { - _elapsedTickTime -= timePerSimulation; - Tick++; - LocalTick++; - } - } while (_elapsedTickTime >= timePerSimulation); + if (frameTicked) + { + _elapsedTickTime -= timePerSimulation; + Tick++; + LocalTick++; + } + } while (_elapsedTickTime >= timePerSimulation); + } } #region Tick conversions. @@ -795,12 +800,14 @@ public double GetTickPercentAsDouble() /// Returns the current elapsed amount for the next tick. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double GetTickElapsedAsDouble() => _elapsedTickTime; /// /// Returns the percentage of how far the TimeManager is into the next tick. /// Value will return between 0 and 100. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte GetTickPercentAsByte() { double result = GetTickPercentAsDouble(); @@ -811,6 +818,7 @@ public byte GetTickPercentAsByte() /// Converts a 0 to 100 byte value to a 0d to 1d percent value. /// This does not check for excessive byte values, such as anything over 100. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double GetTickPercentAsDouble(byte value) { return value / 100d; @@ -892,6 +900,7 @@ public double TicksToTime(TickType tickType = TickType.LocalTick) /// /// PreciseTick to convert. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TicksToTime(PreciseTick pt) { double tickTime = TicksToTime(pt.Tick); @@ -904,6 +913,7 @@ public double TicksToTime(PreciseTick pt) /// /// Ticks to convert. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double TicksToTime(uint ticks) { return TickDelta * (double)ticks; @@ -993,16 +1003,28 @@ public double TimePassed(uint previousTick, bool allowNegative = false) /// /// Time to convert as decimal. /// - public uint TimeToTicks(double time, TickRounding rounding = TickRounding.RoundNearest) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint TimeToTicks(double time, double tickDelta, TickRounding rounding = TickRounding.RoundNearest) { - double result = time / TickDelta; + double result = time / tickDelta; if (rounding == TickRounding.RoundNearest) - return (uint)Math.Round(result); + return (uint)math.round(result); else if (rounding == TickRounding.RoundDown) - return (uint)Math.Floor(result); + return (uint)math.floor(result); else - return (uint)Math.Ceiling(result); + return (uint)math.ceil(result); + } + + /// + /// Converts time to ticks. + /// + /// Time to convert as decimal. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint TimeToTicks(double time, TickRounding rounding = TickRounding.RoundNearest) + { + return TimeToTicks(time, TickDelta, rounding); } /// @@ -1010,6 +1032,7 @@ public uint TimeToTicks(double time, TickRounding rounding = TickRounding.RoundN /// /// Time to convert as whole (milliseconds) /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint TimeToTicks(long time, TickRounding rounding = TickRounding.RoundNearest) { double dTime = (double)time / 1000d; @@ -1021,6 +1044,7 @@ public uint TimeToTicks(long time, TickRounding rounding = TickRounding.RoundNea /// /// Time to convert. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public PreciseTick TimeToPreciseTick(double time) => time.AsPreciseTick(TickDelta); /// diff --git a/Assets/FishNet/Runtime/Managing/Transporting/TransportManager.cs b/Assets/FishNet/Runtime/Managing/Transporting/TransportManager.cs index d1ca6a57..8f947191 100644 --- a/Assets/FishNet/Runtime/Managing/Transporting/TransportManager.cs +++ b/Assets/FishNet/Runtime/Managing/Transporting/TransportManager.cs @@ -122,7 +122,7 @@ public LatencySimulator LatencySimulator private int _customMtuReserve = MINIMUM_MTU_RESERVE; /// /// - private NetworkTrafficStatistics _networkTrafficStatistics; + [NonSerialized] private NetworkTrafficStatistics _networkTrafficStatistics; #endregion #region Consts. From 3b02d1f01262531187848fcd3340be446df0c359 Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 20:26:31 +0300 Subject: [PATCH 3/9] feat: third part of QOLs Now we can observer for NetworkObject server/client start/stop events Now NetworkBehaviour callback safe for error inside user code --- .../NetworkBehaviour.Callbacks.cs | 213 ++++++++++++++---- .../NetworkBehaviour/NetworkBehaviour.cs | 5 +- .../NetworkObject/NetworkObject.Callbacks.cs | 56 ++++- 3 files changed, 222 insertions(+), 52 deletions(-) diff --git a/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.Callbacks.cs b/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.Callbacks.cs index ddcfbbfa..d3e73e31 100644 --- a/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.Callbacks.cs +++ b/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.Callbacks.cs @@ -1,8 +1,11 @@ -using FishNet.Connection; +using System; +using FishNet.Connection; using FishNet.Documenting; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using System.Runtime.CompilerServices; +using FishNet.Managing; +using Unity.Profiling; using UnityEngine; namespace FishNet.Object @@ -23,6 +26,24 @@ public abstract partial class NetworkBehaviour : MonoBehaviour #endregion #region Private. + + #region Private Profiler Markers + private static readonly ProfilerMarker _pm_InvokeSyncTypeOnStartCallbacks = new("NetworkBehaviour.InvokeSyncTypeOnStartCallbacks(bool)"); + private static readonly ProfilerMarker _pm_InvokeSyncTypeOnStopCallbacks = new("NetworkBehaviour.InvokeSyncTypeOnStopCallbacks(bool)"); + + private static readonly ProfilerMarker _pm_InvokeOnNetwork_Internal = new("NetworkBehaviour.InvokeOnNetwork_Internal(bool)"); + private static readonly ProfilerMarker _pm_OnStartNetwork_Internal = new("NetworkBehaviour.OnStartNetwork_Internal(bool)"); + private static readonly ProfilerMarker _pm_OnStopNetwork_Internal = new("NetworkBehaviour.OnStopNetwork_Internal(bool)"); + + private static readonly ProfilerMarker _pm_OnStartServer_Internal = new("NetworkBehaviour.OnStartServer_Internal(bool)"); + private static readonly ProfilerMarker _pm_OnStopServer_Internal = new("NetworkBehaviour.OnStopServer_Internal(bool)"); + private static readonly ProfilerMarker _pm_OnOwnershipServer_Internal = new("NetworkBehaviour.OnOwnershipServer_Internal(NetworkConnection)"); + + private static readonly ProfilerMarker _pm_OnStartClient_Internal = new("NetworkBehaviour.OnStartClient_Internal(bool)"); + private static readonly ProfilerMarker _pm_OnStopClient_Internal = new("NetworkBehaviour.OnStopClient_Internal(bool)"); + private static readonly ProfilerMarker _pm_OnOwnershipClient_Internal = new("NetworkBehaviour.OnOwnershipClient_Internal(NetworkConnection)"); + #endregion + /// /// True if OnStartNetwork has been called. /// @@ -51,8 +72,20 @@ public virtual void ReadPayload(NetworkConnection connection, Reader reader) { } /// internal void InvokeSyncTypeOnStartCallbacks(bool asServer) { - foreach (SyncBase item in _syncTypes.Values) - item.OnStartCallback(asServer); + using (_pm_InvokeSyncTypeOnStartCallbacks.Auto()) + { + foreach (SyncBase item in _syncTypes.Values) + { + try + { + item.OnStartCallback(asServer); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } + } } /// @@ -60,10 +93,22 @@ internal void InvokeSyncTypeOnStartCallbacks(bool asServer) /// internal void InvokeSyncTypeOnStopCallbacks(bool asServer) { - // if (_syncTypes == null) - // return; - foreach (SyncBase item in _syncTypes.Values) - item.OnStopCallback(asServer); + using (_pm_InvokeSyncTypeOnStopCallbacks.Auto()) + { + // if (_syncTypes == null) + // return; + foreach (SyncBase item in _syncTypes.Values) + { + try + { + item.OnStopCallback(asServer); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } + } } /// @@ -71,31 +116,45 @@ internal void InvokeSyncTypeOnStopCallbacks(bool asServer) /// internal void InvokeOnNetwork_Internal(bool start) { - if (start) + using (_pm_InvokeOnNetwork_Internal.Auto()) { - if (_onStartNetworkCalled) - return; + if (start) + { + if (_onStartNetworkCalled) + return; - if (!gameObject.activeInHierarchy) + if (!gameObject.activeInHierarchy) + { + NetworkInitialize___Early(); + NetworkInitialize___Late(); + } + + OnStartNetwork_Internal(); + } + else { - NetworkInitialize___Early(); - NetworkInitialize___Late(); + if (_onStopNetworkCalled) + return; + OnStopNetwork_Internal(); } - OnStartNetwork_Internal(); - } - else - { - if (_onStopNetworkCalled) - return; - OnStopNetwork_Internal(); } } internal virtual void OnStartNetwork_Internal() { - _onStartNetworkCalled = true; - _onStopNetworkCalled = false; - OnStartNetwork(); + using (_pm_OnStartNetwork_Internal.Auto()) + { + _onStartNetworkCalled = true; + _onStopNetworkCalled = false; + try + { + OnStartNetwork(); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -107,10 +166,20 @@ public virtual void OnStartNetwork() { } internal virtual void OnStopNetwork_Internal() { - _onStopNetworkCalled = true; - _onStartNetworkCalled = false; + using (_pm_OnStopNetwork_Internal.Auto()) + { + _onStopNetworkCalled = true; + _onStartNetworkCalled = false; - OnStopNetwork(); + try + { + OnStopNetwork(); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -122,8 +191,18 @@ public virtual void OnStopNetwork() { } internal void OnStartServer_Internal() { - OnStartServerCalled = true; - OnStartServer(); + using (_pm_OnStartServer_Internal.Auto()) + { + OnStartServerCalled = true; + try + { + OnStartServer(); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -134,9 +213,19 @@ public virtual void OnStartServer() { } internal void OnStopServer_Internal() { - OnStartServerCalled = false; - ReturnRpcLinks(); - OnStopServer(); + using (_pm_OnStopServer_Internal.Auto()) + { + OnStartServerCalled = false; + ReturnRpcLinks(); + try + { + OnStopServer(); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -146,8 +235,18 @@ public virtual void OnStopServer() { } internal void OnOwnershipServer_Internal(NetworkConnection prevOwner) { - ResetState_Prediction(true); - OnOwnershipServer(prevOwner); + using (_pm_OnOwnershipServer_Internal.Auto()) + { + ResetState_Prediction(true); + try + { + OnOwnershipServer(prevOwner); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -171,8 +270,18 @@ public virtual void OnDespawnServer(NetworkConnection connection) { } internal void OnStartClient_Internal() { - OnStartClientCalled = true; - OnStartClient(); + using (_pm_OnStartClient_Internal.Auto()) + { + OnStartClientCalled = true; + try + { + OnStartClient(); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -182,8 +291,18 @@ public virtual void OnStartClient() { } internal void OnStopClient_Internal() { - OnStartClientCalled = false; - OnStopClient(); + using (_pm_OnStopClient_Internal.Auto()) + { + OnStartClientCalled = false; + try + { + OnStopClient(); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// @@ -193,13 +312,23 @@ public virtual void OnStopClient() { } internal void OnOwnershipClient_Internal(NetworkConnection prevOwner) { - // If losing or gaining ownership then clear replicate cache. - if (IsOwner || prevOwner == LocalConnection) + using (_pm_OnOwnershipClient_Internal.Auto()) { - ResetState_Prediction(false); - } + // If losing or gaining ownership then clear replicate cache. + if (IsOwner || prevOwner == LocalConnection) + { + ResetState_Prediction(false); + } - OnOwnershipClient(prevOwner); + try + { + OnOwnershipClient(prevOwner); + } + catch (Exception e) + { + NetworkManager.LogError(e.ToString()); + } + } } /// diff --git a/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.cs b/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.cs index 29b3530b..fd455750 100644 --- a/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.cs +++ b/Assets/FishNet/Runtime/Object/NetworkBehaviour/NetworkBehaviour.cs @@ -1,6 +1,7 @@ #if UNITY_EDITOR || DEVELOPMENT_BUILD #define DEVELOPMENT #endif +using System; using FishNet.CodeGenerating; using FishNet.Documenting; using FishNet.Managing.Transporting; @@ -76,7 +77,7 @@ public byte ComponentIndex #if !UNITY_SERVER /// /// - private NetworkTrafficStatistics _networkTrafficStatistics; + [NonSerialized] private NetworkTrafficStatistics _networkTrafficStatistics; /// /// Name of this NetworkBehaviour. /// @@ -101,7 +102,7 @@ public byte ComponentIndex /// public override string ToString() { - return $"Name [{gameObject.name}] ComponentId [{ComponentIndex}] NetworkObject Name [{_networkObjectCache.name}] NetworkObject Id [{_networkObjectCache.ObjectId}]"; + return $"Name [{gameObject.name}] ComponentId [{ComponentIndex}] NetworkObject Name [{_networkObjectCache?.name ?? string.Empty}] NetworkObject Id [{_networkObjectCache?.ObjectId ?? -1}]"; } [MakePublic] diff --git a/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Callbacks.cs b/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Callbacks.cs index 0c6c2e57..6d6d0f4e 100644 --- a/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Callbacks.cs +++ b/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Callbacks.cs @@ -7,6 +7,12 @@ namespace FishNet.Object { public partial class NetworkObject : MonoBehaviour { + #region Types + + public delegate void NetworkObjectCallback(NetworkObject nb); + + #endregion + #region Private. /// /// True if OnStartServer was called. @@ -16,6 +22,40 @@ public partial class NetworkObject : MonoBehaviour /// True if OnStartClient was called. /// private bool _onStartClientCalled; + + private bool OnStartServerCalled + { + get => _onStartServerCalled; + set + { + if (_onStartServerCalled != value) + { + _onStartServerCalled = value; + if (value) OnStartServerEvent?.Invoke(this); + else OnStopServerEvent?.Invoke(this); + } + } + } + + private bool OnStartClientCalled + { + get => _onStartClientCalled; + set + { + if (_onStartClientCalled != value) + { + _onStartClientCalled = value; + if (value) OnStartClientEvent?.Invoke(this); + else OnStopClientEvent?.Invoke(this); + } + } + } + + public event NetworkObjectCallback OnStartServerEvent; + public event NetworkObjectCallback OnStopServerEvent; + public event NetworkObjectCallback OnStartClientEvent; + public event NetworkObjectCallback OnStopClientEvent; + #endregion // ReSharper disable Unity.PerformanceAnalysis @@ -41,7 +81,7 @@ private void InvokeStartCallbacks(bool asServer, bool invokeSyncTypeCallbacks) { for (int i = 0; i < NetworkBehaviours.Count; i++) NetworkBehaviours[i].OnStartServer_Internal(); - _onStartServerCalled = true; + OnStartServerCalled = true; for (int i = 0; i < NetworkBehaviours.Count; i++) NetworkBehaviours[i].OnOwnershipServer_Internal(Managing.NetworkManager.EmptyConnection); } @@ -50,7 +90,7 @@ private void InvokeStartCallbacks(bool asServer, bool invokeSyncTypeCallbacks) { for (int i = 0; i < NetworkBehaviours.Count; i++) NetworkBehaviours[i].OnStartClient_Internal(); - _onStartClientCalled = true; + OnStartClientCalled = true; for (int i = 0; i < NetworkBehaviours.Count; i++) NetworkBehaviours[i].OnOwnershipClient_Internal(Managing.NetworkManager.EmptyConnection); } @@ -114,17 +154,17 @@ internal void InvokeStopCallbacks(bool asServer, bool invokeSyncTypeCallbacks) if (invokeSyncTypeCallbacks) InvokeOnStopSyncTypeCallbacks(asServer); - if (asServer && _onStartServerCalled) + if (asServer && OnStartServerCalled) { for (int i = 0; i < NetworkBehaviours.Count; i++) NetworkBehaviours[i].OnStopServer_Internal(); - if (!_onStartClientCalled) + if (!OnStartClientCalled) InvokeOnNetwork(); - _onStartServerCalled = false; + OnStartServerCalled = false; } - else if (!asServer && _onStartClientCalled) + else if (!asServer && OnStartClientCalled) { for (int i = 0; i < NetworkBehaviours.Count; i++) NetworkBehaviours[i].OnStopClient_Internal(); @@ -133,10 +173,10 @@ internal void InvokeStopCallbacks(bool asServer, bool invokeSyncTypeCallbacks) * that means this is still intialized on the server. This would * happen if the object despawned for the clientHost but not on the * server. */ - if (!_onStartServerCalled) + if (!OnStartServerCalled) InvokeOnNetwork(); - _onStartClientCalled = false; + OnStartClientCalled = false; } void InvokeOnNetwork() From 74c9a65d3b61715d6468cad9e0f716a926aa9114 Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 20:28:47 +0300 Subject: [PATCH 4/9] fix sync collections These things need to be cleaned up, otherwise after ResetState and respawn they fire and OnChange fires when it shouldn't. Also... Make sync type settings public explicit --- Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs | 3 +-- .../FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs | 5 +++++ Assets/FishNet/Runtime/Object/Synchronizing/SyncHashSet.cs | 5 +++++ Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs | 3 +++ Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs | 5 ++++- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs b/Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs index 3be6a8db..a74c956c 100644 --- a/Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs +++ b/Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs @@ -28,8 +28,7 @@ public class SyncBase /// /// The settings for this SyncVar. /// - [MakePublic] - internal SyncTypeSettings Settings; + [MakePublic] public SyncTypeSettings Settings; /// /// How often updates may send. /// diff --git a/Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs b/Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs index 3dbdbc8b..564e6ef5 100644 --- a/Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs +++ b/Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs @@ -446,6 +446,11 @@ protected internal override void ResetState(bool asServer) foreach (KeyValuePair item in _initialValues) Collection[item.Key] = item.Value; } + + if (asServer) + _serverOnChanges.Clear(); + else + _clientOnChanges.Clear(); } /// diff --git a/Assets/FishNet/Runtime/Object/Synchronizing/SyncHashSet.cs b/Assets/FishNet/Runtime/Object/Synchronizing/SyncHashSet.cs index aab05229..d6aee36c 100644 --- a/Assets/FishNet/Runtime/Object/Synchronizing/SyncHashSet.cs +++ b/Assets/FishNet/Runtime/Object/Synchronizing/SyncHashSet.cs @@ -415,6 +415,11 @@ protected internal override void ResetState(bool asServer) foreach (T item in _initialValues) Collection.Add(item); } + + if (asServer) + _serverOnChanges.Clear(); + else + _clientOnChanges.Clear(); } /// diff --git a/Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs b/Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs index 9254ddad..510b785c 100644 --- a/Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs +++ b/Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs @@ -460,6 +460,9 @@ protected internal override void ResetState(bool asServer) foreach (T item in _initialValues) Collection.Add(item); } + + if (asServer) _serverOnChanges.Clear(); + else _clientOnChanges.Clear(); } /// diff --git a/Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs b/Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs index 06a86a33..864fdcdd 100644 --- a/Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs +++ b/Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs @@ -10,7 +10,7 @@ namespace FishNet.Object.Synchronizing { - internal interface ISyncVar { } + public interface ISyncVar { } [APIExclude] [System.Serializable] @@ -468,6 +468,9 @@ protected internal override void ResetState(bool asServer) _value = _initialValue; _valueSetAfterInitialized = false; } + + if (asServer) _serverOnChange = null; + else _clientOnChange = null; } } } \ No newline at end of file From 269fcfce6a6e71ea1c10b321fc77c1b420af78f8 Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 20:37:48 +0300 Subject: [PATCH 5/9] feat: fix asmdefs --- Assets/FishNet/CodeGenerating/Unity.FishNet.CodeGen.asmdef | 5 ++++- Assets/FishNet/Runtime/FishNet.Runtime.asmdef | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Assets/FishNet/CodeGenerating/Unity.FishNet.CodeGen.asmdef b/Assets/FishNet/CodeGenerating/Unity.FishNet.CodeGen.asmdef index 9aa9b97a..22a5b611 100644 --- a/Assets/FishNet/CodeGenerating/Unity.FishNet.CodeGen.asmdef +++ b/Assets/FishNet/CodeGenerating/Unity.FishNet.CodeGen.asmdef @@ -4,7 +4,10 @@ "references": [ "FishNet.Runtime", "FishNet.Codegen.Cecil", - "GameKit.Dependencies" + "GameKit.Dependencies", + "Unity.Burst", + "Unity.Mathematics", + "Unity.Collections" ], "includePlatforms": [ "Editor" diff --git a/Assets/FishNet/Runtime/FishNet.Runtime.asmdef b/Assets/FishNet/Runtime/FishNet.Runtime.asmdef index dc41020c..b2c5f6a9 100644 --- a/Assets/FishNet/Runtime/FishNet.Runtime.asmdef +++ b/Assets/FishNet/Runtime/FishNet.Runtime.asmdef @@ -4,7 +4,9 @@ "references": [ "GUID:894a6cc6ed5cd2645bb542978cbed6a9", "GUID:1d82bdf40e2465b44b34adf79595e74c", - "GUID:d8b63aba1907145bea998dd612889d6b" + "GUID:d8b63aba1907145bea998dd612889d6b", + "GUID:2665a8d13d1b3f18800f46e256720795", + "GUID:e0cd26848372d4e5c891c569017e11f1" ], "includePlatforms": [], "excludePlatforms": [], From 643a95ac1d0dba7246a0b47009526df3b9931897 Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 20:38:44 +0300 Subject: [PATCH 6/9] fix asmdefs --- .../Plugins/GameKit/Dependencies/GameKit.Dependencies.asmdef | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/GameKit.Dependencies.asmdef b/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/GameKit.Dependencies.asmdef index 438b416f..751eaca6 100644 --- a/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/GameKit.Dependencies.asmdef +++ b/Assets/FishNet/Runtime/Plugins/GameKit/Dependencies/GameKit.Dependencies.asmdef @@ -2,7 +2,10 @@ "name": "GameKit.Dependencies", "rootNamespace": "", "references": [ - "GUID:6055be8ebefd69e48b49212b09b47b2f" + "GUID:6055be8ebefd69e48b49212b09b47b2f", + "GUID:d8b63aba1907145bea998dd612889d6b", + "GUID:2665a8d13d1b3f18800f46e256720795", + "GUID:e0cd26848372d4e5c891c569017e11f1" ], "includePlatforms": [], "excludePlatforms": [], From 6916a28231dd6acfd5277e7c62b3e8a65d767441 Mon Sep 17 00:00:00 2001 From: belplaton Date: Wed, 17 Dec 2025 22:39:48 +0300 Subject: [PATCH 7/9] Update NetworkTransform.cs --- .../NetworkTransform/NetworkTransform.cs | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs b/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs index 1f2de518..46338147 100644 --- a/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs +++ b/Assets/FishNet/Runtime/Generated/Component/NetworkTransform/NetworkTransform.cs @@ -1756,27 +1756,6 @@ private void MoveToTarget(float delta) //No more in buffer, see if can extrapolate. else { - //PROSTART - //Can extrapolate. - if (td.ExtrapolationState == TransformData.ExtrapolateState.Available) - { - rd.TimeRemaining = (float)(_extrapolation * _timeManager.TickDelta); - td.ExtrapolationState = TransformData.ExtrapolateState.Active; - if (leftOver > 0f) - MoveToTarget(leftOver); - } - //Ran out of extrapolate. - else if (td.ExtrapolationState == TransformData.ExtrapolateState.Active) - { - rd.TimeRemaining = (float)(_extrapolation * _timeManager.TickDelta); - td.ExtrapolationState = TransformData.ExtrapolateState.Disabled; - if (leftOver > 0f) - MoveToTarget(leftOver); - } - //Extrapolation has ended or was never enabled. - else - { - //PROEND /* If everything matches up then end queue. * Otherwise let it play out until stuff * aligns. Generally the time remaining is enough @@ -1785,9 +1764,7 @@ private void MoveToTarget(float delta) if (!HasChanged(td)) _currentGoalData = null; OnInterpolationComplete?.Invoke(); - //PROSTART - } - //PROEND + } } } @@ -2321,16 +2298,6 @@ private void SetExtrapolatedData(TransformData prev, TransformData next, Channel { //Default value. next.ExtrapolationState = TransformData.ExtrapolateState.Disabled; - - //PROSTART - //Teleports cannot extrapolate. - if (_extrapolation == 0 || !_synchronizePosition || channel == Channel.Reliable || next.Position == prev.Position) - return; - - Vector3 offet = (next.Position - prev.Position) * _extrapolation; - next.ExtrapolatedPosition = next.Position + offet; - next.ExtrapolationState = TransformData.ExtrapolateState.Available; - //PROEND } /// From 4ce08dceb83131bcc6fc08add0e2267151fb27bb Mon Sep 17 00:00:00 2001 From: waterb <143908720+belplaton@users.noreply.github.com> Date: Wed, 17 Dec 2025 22:41:05 +0300 Subject: [PATCH 8/9] Delete Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak --- .../NetworkAnimator/NetworkAnimator.cs.bak | 1621 ----------------- 1 file changed, 1621 deletions(-) delete mode 100644 Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak deleted file mode 100644 index 822d0ccd..00000000 --- a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs.bak +++ /dev/null @@ -1,1621 +0,0 @@ -#if UNITY_EDITOR || DEVELOPMENT_BUILD -#define DEVELOPMENT -#endif -using FishNet.Component.Transforming; -using FishNet.Connection; -using FishNet.Documenting; -using FishNet.Managing.Logging; -using FishNet.Managing.Server; -using FishNet.Object; -using FishNet.Serializing; -using FishNet.Utility; -using FishNet.Utility.Performance; -using GameKit.Dependencies.Utilities; -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using FishNet.Managing; -using UnityEngine; -using UnityEngine.Profiling; -using Unity.Profiling; -using TimeManagerCls = FishNet.Managing.Timing.TimeManager; - -namespace FishNet.Component.Animating -{ - [AddComponentMenu("FishNet/Component/NetworkAnimator")] - public sealed class NetworkAnimator : NetworkBehaviour - { - #region Types. - /// - /// Data received from the server. - /// - private struct ReceivedServerData - { - /// - /// Gets an Arraysegment of received data. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArraySegment GetArraySegment() => new(_data, 0, _length); - - /// - /// How much data written. - /// - private int _length; - /// - /// Buffer which contains data. - /// - private byte[] _data; - - public ReceivedServerData(ArraySegment segment) - { - _length = segment.Count; - _data = ByteArrayPool.Retrieve(_length); - Buffer.BlockCopy(segment.Array, segment.Offset, _data, 0, _length); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() - { - if (_data != null) - ByteArrayPool.Store(_data); - } - } - - private struct StateChange - { - /// - /// Frame which the state was changed. - /// - public int FrameCount; - /// - /// True if a crossfade. - /// - public bool IsCrossfade; - /// - /// Hash to crossfade into. - /// - public int Hash; - /// - /// True if using FixedTime. - /// - public bool FixedTime; - /// - /// Duration of crossfade. - /// - public float DurationTime; - /// - /// Offset time of crossfade. - /// - public float OffsetTime; - /// - /// Normalized transition time of crossfade. - /// - public float NormalizedTransitionTime; - - public StateChange(int frame) - { - FrameCount = frame; - IsCrossfade = default; - Hash = default; - FixedTime = default; - DurationTime = default; - OffsetTime = default; - NormalizedTransitionTime = default; - } - - public StateChange(int frame, int hash, bool fixedTime, float duration, float offset, float normalizedTransition) - { - FrameCount = frame; - IsCrossfade = true; - Hash = hash; - FixedTime = fixedTime; - DurationTime = duration; - OffsetTime = offset; - NormalizedTransitionTime = normalizedTransition; - } - } - - /// - /// Animator updates received from clients when using Client Authoritative. - /// - private class ClientAuthoritativeUpdate - { - /// - /// - public ClientAuthoritativeUpdate() - { - // Start buffers off at 8 bytes nad grow them as needed. - for (int i = 0; i < MAXIMUM_BUFFER_COUNT; i++) - _buffers.Add(new byte[MAXIMUM_DATA_SIZE]); - - _bufferLengths = new int[MAXIMUM_BUFFER_COUNT]; - } - - #region Public. - /// - /// True to force all animator data and ignore buffers. - /// - public bool ForceAll { get; private set; } - /// - /// Number of entries in Buffers. - /// - public int BufferCount = 0; - #endregion - - #region Private. - /// - /// Length of buffers. - /// - private int[] _bufferLengths; - /// - /// Buffers. - /// - private List _buffers = new(); - #endregion - - #region Const. - /// - /// Maximum size data may be. - /// - private const int MAXIMUM_DATA_SIZE = 1000; - /// - /// Maximum number of allowed buffers. - /// - public const int MAXIMUM_BUFFER_COUNT = 2; - #endregion - - public void AddToBuffer(ref ArraySegment data) - { - int dataCount = data.Count; - /* Data will never get this large, it's quite impossible. - * Just ignore the data if it does, client is likely performing - * an attack. */ - if (dataCount > MAXIMUM_DATA_SIZE) - return; - - // If index exceeds buffer count. - if (BufferCount >= MAXIMUM_BUFFER_COUNT) - { - ForceAll = true; - return; - } - - /* If here, can write to buffer. */ - byte[] buffer = _buffers[BufferCount]; - Buffer.BlockCopy(data.Array, data.Offset, buffer, 0, dataCount); - _bufferLengths[BufferCount] = dataCount; - BufferCount++; - } - - /// - /// Sets referenced data to buffer and it's length for index. - /// - /// - /// - /// - public void GetBuffer(int index, ref byte[] buffer, ref int length) - { - if (index > _buffers.Count) - { - NetworkManagerExtensions.LogWarning("Index exceeds Buffers count."); - return; - } - if (index > _bufferLengths.Length) - { - NetworkManagerExtensions.LogWarning("Index exceeds BufferLengths count."); - return; - } - - buffer = _buffers[index]; - length = _bufferLengths[index]; - } - - /// - /// Resets buffers. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Reset() - { - BufferCount = 0; - ForceAll = false; - } - } - - /// - /// Information on how to smooth to a float value. - /// - private struct SmoothedFloat - { - public SmoothedFloat(float rate, float target) - { - Rate = rate; - Target = target; - } - - public readonly float Rate; - public readonly float Target; - } - - /// - /// Details about a trigger update. - /// - private struct TriggerUpdate - { - public byte ParameterIndex; - public bool Setting; - - public TriggerUpdate(byte parameterIndex, bool setting) - { - ParameterIndex = parameterIndex; - Setting = setting; - } - } - - /// - /// Details about an animator parameter. - /// - private class ParameterDetail - { - /// - /// Parameter information. - /// - public readonly AnimatorControllerParameter ControllerParameter = null; - /// - /// Index within the types collection for this parameters value. The exception is with triggers; if the parameter type is a trigger then a value of 1 is set, 0 is unset. - /// - public readonly byte TypeIndex = 0; - /// - /// Hash for the animator string. - /// - public readonly int Hash; - - public ParameterDetail(AnimatorControllerParameter controllerParameter, byte typeIndex) - { - ControllerParameter = controllerParameter; - TypeIndex = typeIndex; - Hash = controllerParameter.nameHash; - } - } - #endregion - - #region Private - - #region Private Profiler Markers - - private static readonly ProfilerMarker _pm_OnPreTick = new ProfilerMarker("NetworkAnimator.TimeManager_OnPreTick()"); - private static readonly ProfilerMarker _pm_OnPostTick = new ProfilerMarker("NetworkAnimator.TimeManager_OnPostTick()"); - private static readonly ProfilerMarker _pm_OnUpdate = new ProfilerMarker("NetworkAnimator.TimeManager_OnUpdate()"); - private static readonly ProfilerMarker _pm_CheckSendToServer = new ProfilerMarker("NetworkAnimator.CheckSendToServer()"); - private static readonly ProfilerMarker _pm_CheckSendToClients = new ProfilerMarker("NetworkAnimator.CheckSendToClients()"); - private static readonly ProfilerMarker _pm_SmoothFloats = new ProfilerMarker("NetworkAnimator.SmoothFloats()"); - private static readonly ProfilerMarker _pm_AnimatorUpdated = new ProfilerMarker("NetworkAnimator.AnimatorUpdated(ref ArraySegment, bool)"); - private static readonly ProfilerMarker _pm_ApplyParametersUpdated = new ProfilerMarker("NetworkAnimator.ApplyParametersUpdated(ref ArraySegment)"); - - #endregion - - #endregion - - #region Public. - /// - /// Parameters which will not be synchronized. - /// - [SerializeField] - [HideInInspector] - internal List IgnoredParameters = new(); - #endregion - - #region Serialized. - /// - /// The animator component to synchronize. - /// - [Tooltip("The animator component to synchronize.")] - [SerializeField] - private Animator _animator; - /// - /// The animator component to synchronize. - /// - public Animator Animator - { - get { return _animator; } - } - /// - /// True to synchronize changes even when the animator component is disabled. - /// - [Tooltip("True to synchronize changes even when the animator component is disabled.")] - [SerializeField] - private bool _synchronizeWhenDisabled; - /// - /// True to synchronize changes even when the animator component is disabled. - /// - public bool SynchronizeWhenDisabled - { - get { return _synchronizeWhenDisabled; } - set { _synchronizeWhenDisabled = value; } - } - /// - /// True to smooth float value changes for spectators. - /// - [Tooltip("True to smooth float value changes for spectators.")] - [SerializeField] - private bool _smoothFloats = true; - /// - /// How many ticks to interpolate. - /// - [Tooltip("How many ticks to interpolate.")] - [Range(1, NetworkTransform.MAX_INTERPOLATION)] - [SerializeField] - private ushort _interpolation = 2; - /// - /// - [Tooltip("True if using client authoritative animations.")] - [SerializeField] - private bool _clientAuthoritative = true; - /// - /// True if using client authoritative animations. - /// - public bool ClientAuthoritative - { - get { return _clientAuthoritative; } - } - /// - /// True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations. - /// - [Tooltip("True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations.")] - [SerializeField] - private bool _sendToOwner; - /// - /// True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations. - /// - public bool SendToOwner => _sendToOwner; - - #endregion - - #region Private. - /// - /// All parameter values, excluding triggers. - /// - private readonly List _parameterDetails = new(); - /// - /// Last int values. - /// - private readonly List _ints = new(); - /// - /// Last float values. - /// - private readonly List _floats = new(); - /// - /// Last bool values. - /// - private readonly List _bools = new(); - /// - /// Last layer weights. - /// - private float[] _layerWeights; - /// - /// Last speed. - /// - private float _speed; - /// - /// Trigger values set by using SetTrigger and ResetTrigger. - /// - private readonly List _triggerUpdates = new(); - // /// - // /// Updates going to clients. - // /// - // private List _toClientsBuffer = new(); - /// - /// Synchronization enabled state. True by default - /// - private bool _isSynchronizationEnabled = true; - /// - /// Returns if the animator is exist and can be synchronized. - /// - private bool _canSynchronizeAnimator - { - get - { - if (!_isSynchronizationEnabled) - return false; - - if (!_isAnimatorSet) - return false; - - if (_animator.enabled || _synchronizeWhenDisabled) - return true; - - return false; - } - } - /// - /// True if the animator is valid but not enabled. - /// - private bool _isAnimatorSet - { - get - { - bool failedChecks = _animator == null || _animator.runtimeAnimatorController == null; - return !failedChecks; - } - } - /// - /// Float valeus to smooth towards. - /// - private Dictionary _smoothedFloats = new(); - /// - /// Returns if floats can be smoothed for this client. - /// - private bool _canSmoothFloats - { - get - { - // Don't smooth on server only. - if (!IsClientStarted) - return false; - // Smoothing is disabled. - if (!_smoothFloats) - return false; - // No reason to smooth for self. - if (IsOwner && ClientAuthoritative) - return false; - - //Fall through. - return true; - } - } - /// - /// Layers which need to have their state synchronized. Key is the layer, Value is the state change information. - /// - private Dictionary _unsynchronizedLayerStates = new(); - /// - /// Last animator set. - /// - private Animator _lastAnimator; - /// - /// Last Controller set. - /// - private RuntimeAnimatorController _lastController; - /// - /// PooledWriter for this animator. - /// - private PooledWriter _writer = new(); - /// - /// Holds client authoritative updates received to send to other clients. - /// - private ClientAuthoritativeUpdate _clientAuthoritativeUpdates; - /// - /// True to forceAll next timed send. - /// - private bool _forceAllOnTimed; - /// - /// Animations received which should be applied. - /// - private Queue _fromServerBuffer = new(); - /// - /// Tick when the buffer may begin to run. - /// - private uint _startTick = TimeManagerCls.UNSET_TICK; - /// - /// True if subscribed to TimeManager for ticks. - /// - private bool _subscribedToTicks; - #endregion - - #region Const. - ///// - ///// How much time to fall behind when using smoothing. Only increase value if the smoothing is sometimes jittery. Recommended values are between 0 and 0.04. - ///// - //private const float INTERPOLATION = 0.02f; - /// - /// ParameterDetails index which indicates a layer weight change. - /// - private const byte LAYER_WEIGHT = 240; - /// - /// ParameterDetails index which indicates an animator speed change. - /// - private const byte SPEED = 241; - /// - /// ParameterDetails index which indicates a layer state change. - /// - private const byte STATE = 242; - /// - /// ParameterDetails index which indicates a crossfade change. - /// - private const byte CROSSFADE = 243; - #endregion - - private void Awake() - { - InitializeOnce(); - } - - private void OnDestroy() - { - ChangeTickSubscription(false); - } - - [APIExclude] - public override void OnSpawnServer(NetworkConnection connection) - { - if (!_canSynchronizeAnimator) - return; - if (AnimatorUpdated(out ArraySegment updatedBytes, true)) - TargetAnimatorUpdated(connection, updatedBytes); - } - - public override void OnStartNetwork() - { - ChangeTickSubscription(true); - _isSynchronizationEnabled = true; - } - - [APIExclude] - public override void OnStartServer() - { - //If using client authoritative then initialize clientAuthoritativeUpdates. - if (_clientAuthoritative) - { - _clientAuthoritativeUpdates = new(); - // //Expand to clients buffer count to however many buffers can be held. - // for (int i = 0; i < ClientAuthoritativeUpdate.MAXIMUM_BUFFER_COUNT; i++) - // _toClientsBuffer.Add(new byte[0]); - } - // else - // { - // _toClientsBuffer.Add(new byte[0]); - // } - } - - public override void OnStartClient() - { - TimeManager.OnUpdate += TimeManager_OnUpdate; - } - - public override void OnStopClient() - { - if (TimeManager != null) - TimeManager.OnUpdate -= TimeManager_OnUpdate; - } - - public override void OnStopNetwork() - { - _unsynchronizedLayerStates.Clear(); - ChangeTickSubscription(false); - } - - /// - /// Tries to subscribe to TimeManager ticks. - /// - private void ChangeTickSubscription(bool subscribe) - { - if (subscribe == _subscribedToTicks || NetworkManager == null) - return; - - _subscribedToTicks = subscribe; - if (subscribe) - { - NetworkManager.TimeManager.OnPreTick += TimeManager_OnPreTick; - NetworkManager.TimeManager.OnPostTick += TimeManager_OnPostTick; - } - else - { - NetworkManager.TimeManager.OnPreTick -= TimeManager_OnPreTick; - NetworkManager.TimeManager.OnPostTick -= TimeManager_OnPostTick; - } - } - - /// - /// Called right before a tick occurs, as well before data is read. - /// - private void TimeManager_OnPreTick() - { - using (_pm_OnPreTick.Auto()) - { - if (!_canSynchronizeAnimator) - { - _fromServerBuffer.Clear(); - return; - } - - //Disabled/cannot start. - if (_startTick == 0) - return; - //Nothing in queue. - if (_fromServerBuffer.Count == 0) - { - _startTick = 0; - return; - } - - //Not enough time has passed to start queue. - if (TimeManager.LocalTick < _startTick) - return; - - ReceivedServerData rd = _fromServerBuffer.Dequeue(); - ArraySegment segment = rd.GetArraySegment(); - ApplyParametersUpdated(ref segment); - rd.Dispose(); - } - } - - /* Use post tick values are checked after - * client has an opportunity to use OnTick. */ - /// - /// Called after a tick occurs; physics would have simulated if using PhysicsMode.TimeManager. - /// - private void TimeManager_OnPostTick() - { - using (_pm_OnPostTick.Auto()) - { - //One check rather than per each method. - if (!_canSynchronizeAnimator) - return; - - CheckSendToServer(); - CheckSendToClients(); - } - } - - private void TimeManager_OnUpdate() - { - using (_pm_OnUpdate.Auto()) - { - if (!_canSynchronizeAnimator) - return; - - if (IsClientStarted) - SmoothFloats(); - } - } - - /// - /// Initializes this script for use. - /// - private void InitializeOnce() - { - if (_animator == null) - _animator = GetComponent(); - - //Don't run the rest if not in play mode. - if (!ApplicationState.IsPlaying()) - return; - - if (!_canSynchronizeAnimator) - { - //Debug.LogWarning("Animator is null or not enabled; unable to initialize for animator. Use SetAnimator if animator was changed or enable the animator."); - return; - } - - //Speed. - _speed = _animator.speed; - - //Build layer weights. - _layerWeights = new float[_animator.layerCount]; - for (int i = 0; i < _layerWeights.Length; i++) - _layerWeights[i] = _animator.GetLayerWeight(i); - - _parameterDetails.Clear(); - _bools.Clear(); - _floats.Clear(); - _ints.Clear(); - //Create a parameter detail for each parameter that can be synchronized. - foreach (AnimatorControllerParameter item in _animator.parameters) - { - bool process = !_animator.IsParameterControlledByCurve(item.name); - //PROSTART - - //PROEND - if (process) - { - //Over 250 parameters; who would do this!? - if (_parameterDetails.Count == 240) - { - NetworkManager.LogError($"Parameter {item.name} exceeds the allowed 240 parameter count and is being ignored."); - continue; - } - - int typeIndex = 0; - //Bools. - if (item.type == AnimatorControllerParameterType.Bool) - { - typeIndex = _bools.Count; - _bools.Add(_animator.GetBool(item.nameHash)); - } - //Floats. - else if (item.type == AnimatorControllerParameterType.Float) - { - typeIndex = _floats.Count; - _floats.Add(_animator.GetFloat(item.name)); - } - //Ints. - else if (item.type == AnimatorControllerParameterType.Int) - { - typeIndex = _ints.Count; - _ints.Add(_animator.GetInteger(item.nameHash)); - } - //Triggers. - else if (item.type == AnimatorControllerParameterType.Trigger) - { - /* Triggers aren't persistent so they don't use stored values - * but I do need to make a parameter detail to track the hash. */ - typeIndex = -1; - } - - _parameterDetails.Add(new(item, (byte)typeIndex)); - } - } - } - - /// - /// Sets synchronization state to NetworkAnimator. Enabled by default. - /// - /// - public void SetSynchronizationState(bool state) - { - _isSynchronizationEnabled = state; - } - - /// - /// Sets which animator to use. You must call this with the appropriate animator on all clients and server. This change is not automatically synchronized. - /// - /// - public void SetAnimator(Animator animator) - { - //No update required. - if (animator == _lastAnimator) - return; - - _animator = animator; - InitializeOnce(); - _lastAnimator = animator; - } - - /// - /// Sets which controller to use. You must call this with the appropriate controller on all clients and server. This change is not automatically synchronized. - /// - /// - public void SetController(RuntimeAnimatorController controller) - { - //No update required. - if (controller == _lastController) - return; - - _animator.runtimeAnimatorController = controller; - InitializeOnce(); - _lastController = controller; - } - - /// - /// Checks to send animator data from server to clients. - /// - private void CheckSendToServer() - { - using (_pm_CheckSendToServer.Auto()) - { - //Cannot send to server if is server or not client. - if (IsServerStarted || !IsClientInitialized) - return; - //Cannot send to server if not client authoritative or don't have authority. - if (!ClientAuthoritative || !IsOwner) - return; - - /* If there are updated parameters to send. - * Don't really need to worry about mtu here - * because there's no way the sent bytes are - * ever going to come close to the mtu - * when sending a single update. */ - if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) - ServerAnimatorUpdated(updatedBytes); - - _forceAllOnTimed = false; - } - } - - /// - /// Checks to send animator data from server to clients. - /// - private void CheckSendToClients() - { - using (_pm_CheckSendToClients.Auto()) - { - //Cannot send to clients if not server initialized. - if (!IsServerInitialized) - return; - - bool sendFromServer; - //If client authoritative. - if (ClientAuthoritative) - { - //If has no owner then use latest values on server. - if (!Owner.IsValid) - { - sendFromServer = true; - } - //If has a owner. - else - { - //If is owner then send latest values on server. - if (IsOwner) - { - sendFromServer = true; - } - //Not owner. - else - { - //Haven't received any data from clients, cannot send yet. - if (_clientAuthoritativeUpdates.BufferCount == 0) - { - return; - } - //Data was received from client; check eligibility to send it. - else - { - /* If forceAll is true then the latest values on - * server must be used, rather than what was received - * from client. This can occur if the client is possibly - * trying to use an attack or if the client is - * excessively sending updates. To prevent relaying that - * same data to others the server will send it's current - * animator settings in this scenario. */ - if (_clientAuthoritativeUpdates.ForceAll) - { - sendFromServer = true; - _clientAuthoritativeUpdates.Reset(); - } - else - { - sendFromServer = false; - } - } - } - } - } - //Not client authoritative, always send from server. - else - { - sendFromServer = true; - } - - /* If client authoritative then use what was received from clients - * if data exist. */ - if (!sendFromServer) - { - byte[] buffer = null; - int bufferLength = 0; - for (int i = 0; i < _clientAuthoritativeUpdates.BufferCount; i++) - { - _clientAuthoritativeUpdates.GetBuffer(i, ref buffer, ref bufferLength); - - //If null was returned then something went wrong. - if (buffer == null || bufferLength == 0) - continue; - - SendSegment(new(buffer, 0, bufferLength)); - } - - //Reset client auth buffer. - _clientAuthoritativeUpdates.Reset(); - } - //Sending from server, send what's changed. - else - { - if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) - SendSegment(updatedBytes); - - _forceAllOnTimed = false; - } - - //Sends segment to clients - void SendSegment(ArraySegment data) - { - foreach (NetworkConnection nc in Observers) - { - //If to not send to owner. - if (!_sendToOwner && nc == Owner) - continue; - TargetAnimatorUpdated(nc, data); - } - } - } - } - - /// - /// Smooths floats on clients. - /// - private void SmoothFloats() - { - using (_pm_SmoothFloats.Auto()) - { - //Don't need to smooth on authoritative client. - if (!_canSmoothFloats) - return; - //Nothing to smooth. - if (_smoothedFloats.Count == 0) - return; - - float deltaTime = Time.deltaTime; - - List finishedEntries = new(); - - /* Cycle through each target float and move towards it. - * Once at a target float mark it to be removed from floatTargets. */ - foreach (KeyValuePair item in _smoothedFloats) - { - float current = _animator.GetFloat(item.Key); - float next = Mathf.MoveTowards(current, item.Value.Target, item.Value.Rate * deltaTime); - _animator.SetFloat(item.Key, next); - - if (next == item.Value.Target) - finishedEntries.Add(item.Key); - } - - //Remove finished entries from dictionary. - for (int i = 0; i < finishedEntries.Count; i++) - _smoothedFloats.Remove(finishedEntries[i]); - } - } - - /// - /// Returns if animator is updated and bytes of updated values. - /// - /// - private bool AnimatorUpdated(out ArraySegment updatedBytes, bool forceAll = false) - { - using (_pm_AnimatorUpdated.Auto()) - { - updatedBytes = default; - //Something isn't setup right. - if (_layerWeights == null) - return false; - //Reset the writer. - _writer.Clear(); - - /* Every time a parameter is updated a byte is added - * for it's index, this is why requiredBytes increases - * by 1 when a value updates. ChangedParameter contains - * the index updated and the new value. The requiresBytes - * is increased also by however many bytes are required - * for the type which has changed. Some types use special parameter - * detail indexes, such as layer weights; these can be found under const. */ - for (byte parameterIndex = 0; parameterIndex < _parameterDetails.Count; parameterIndex++) - { - ParameterDetail pd = _parameterDetails[parameterIndex]; - /* Bool. */ - if (pd.ControllerParameter.type == AnimatorControllerParameterType.Bool) - { - bool next = _animator.GetBool(pd.Hash); - //If changed. - if (forceAll || _bools[pd.TypeIndex] != next) - { - _writer.WriteUInt8Unpacked(parameterIndex); - _writer.WriteBoolean(next); - _bools[pd.TypeIndex] = next; - } - } - /* Float. */ - else if (pd.ControllerParameter.type == AnimatorControllerParameterType.Float) - { - float next = _animator.GetFloat(pd.Hash); - //If changed. - if (forceAll || _floats[pd.TypeIndex] != next) - { - _writer.WriteUInt8Unpacked(parameterIndex); - _writer.WriteSingle(next); - _floats[pd.TypeIndex] = next; - } - } - /* Int. */ - else if (pd.ControllerParameter.type == AnimatorControllerParameterType.Int) - { - int next = _animator.GetInteger(pd.Hash); - //If changed. - if (forceAll || _ints[pd.TypeIndex] != next) - { - _writer.WriteUInt8Unpacked(parameterIndex); - _writer.WriteInt32(next); - _ints[pd.TypeIndex] = next; - } - } - } - - /* Don't need to force trigger sends since - * they're one-shots. */ - for (int i = 0; i < _triggerUpdates.Count; i++) - { - _writer.WriteUInt8Unpacked(_triggerUpdates[i].ParameterIndex); - _writer.WriteBoolean(_triggerUpdates[i].Setting); - } - - _triggerUpdates.Clear(); - - /* States. */ - if (forceAll) - { - //Add all layers to layer states. - for (int i = 0; i < _animator.layerCount; i++) - _unsynchronizedLayerStates[i] = new(Time.frameCount); - } - - /* Only iterate if the collection has values. This is to avoid some - * unnecessary caching when collection is empty. */ - if (_unsynchronizedLayerStates.Count > 0) - { - int frameCount = Time.frameCount; - List sentLayers = CollectionCaches.RetrieveList(); - //Go through each layer which needs to be synchronized. - foreach (KeyValuePair item in _unsynchronizedLayerStates) - { - /* If a frame has not passed since the state was created - * then do not send it until next tick. State changes take 1 frame - * to be processed by Unity, this check ensures that. */ - if (frameCount == item.Value.FrameCount) - continue; - - //Add to layers being sent. This is so they can be removed from the collection later. - sentLayers.Add(item.Key); - int layerIndex = item.Key; - StateChange sc = item.Value; - //If a regular state change. - if (!sc.IsCrossfade) - { - if (ReturnCurrentLayerState(out int stateHash, out float normalizedTime, layerIndex)) - { - _writer.WriteUInt8Unpacked(STATE); - _writer.WriteUInt8Unpacked((byte)layerIndex); - //Current hash will always be too large to compress. - _writer.WriteInt32Unpacked(stateHash); - _writer.WriteSingle(normalizedTime); - } - } - //When it's a crossfade then send crossfade data. - else - { - _writer.WriteUInt8Unpacked(CROSSFADE); - _writer.WriteUInt8Unpacked((byte)layerIndex); - //Current hash will always be too large to compress. - _writer.WriteInt32(sc.Hash); - _writer.WriteBoolean(sc.FixedTime); - //Times usually can be compressed. - _writer.WriteSingle(sc.DurationTime); - _writer.WriteSingle(sc.OffsetTime); - _writer.WriteSingle(sc.NormalizedTransitionTime); - } - } - - if (sentLayers.Count > 0) - { - for (int i = 0; i < sentLayers.Count; i++) - _unsynchronizedLayerStates.Remove(sentLayers[i]); - //Store cache. - CollectionCaches.Store(sentLayers); - } - } - - /* Layer weights. */ - for (int layerIndex = 0; layerIndex < _layerWeights.Length; layerIndex++) - { - float next = _animator.GetLayerWeight(layerIndex); - if (forceAll || _layerWeights[layerIndex] != next) - { - _writer.WriteUInt8Unpacked(LAYER_WEIGHT); - _writer.WriteUInt8Unpacked((byte)layerIndex); - _writer.WriteSingle(next); - _layerWeights[layerIndex] = next; - } - } - - /* Speed is similar to layer weights but we don't need the index, - * only the indicator and value. */ - float speedNext = _animator.speed; - if (forceAll || _speed != speedNext) - { - _writer.WriteUInt8Unpacked(SPEED); - _writer.WriteSingle(speedNext); - _speed = speedNext; - } - - //Nothing to update. - if (_writer.Position == 0) - return false; - - updatedBytes = _writer.GetArraySegment(); - return true; - } - } - - /// - /// Applies changed parameters to the animator. - /// - /// - private void ApplyParametersUpdated(ref ArraySegment updatedParameters) - { - using (_pm_ApplyParametersUpdated.Auto()) - { - if (!_canSynchronizeAnimator) - return; - if (_layerWeights == null) - return; - if (updatedParameters.Count == 0) - return; - - PooledReader reader = ReaderPool.Retrieve(updatedParameters, NetworkManager); - - try - { - while (reader.Remaining > 0) - { - byte parameterIndex = reader.ReadUInt8Unpacked(); - //Layer weight - if (parameterIndex == LAYER_WEIGHT) - { - byte layerIndex = reader.ReadUInt8Unpacked(); - float value = reader.ReadSingle(); - _animator.SetLayerWeight((int)layerIndex, value); - } - //Speed. - else if (parameterIndex == SPEED) - { - float value = reader.ReadSingle(); - _animator.speed = value; - } - //State. - else if (parameterIndex == STATE) - { - byte layerIndex = reader.ReadUInt8Unpacked(); - //Hashes will always be too large to compress. - int hash = reader.ReadInt32Unpacked(); - float normalizedTime = reader.ReadSingle(); - //Play results. - _animator.Play(hash, layerIndex, normalizedTime); - } - //Crossfade. - else if (parameterIndex == CROSSFADE) - { - byte layerIndex = reader.ReadUInt8Unpacked(); - //Hashes will always be too large to compress. - int hash = reader.ReadInt32(); - bool useFixedTime = reader.ReadBoolean(); - //Get time values. - float durationTime = reader.ReadSingle(); - float offsetTime = reader.ReadSingle(); - float normalizedTransitionTime = reader.ReadSingle(); - //If using fixed. - if (useFixedTime) - _animator.CrossFadeInFixedTime(hash, durationTime, layerIndex, offsetTime, normalizedTransitionTime); - else - _animator.CrossFade(hash, durationTime, layerIndex, offsetTime, normalizedTransitionTime); - } - //Not a predetermined index, is an actual parameter. - else - { - AnimatorControllerParameterType acpt = _parameterDetails[parameterIndex].ControllerParameter.type; - if (acpt == AnimatorControllerParameterType.Bool) - { - bool value = reader.ReadBoolean(); - _animator.SetBool(_parameterDetails[parameterIndex].Hash, value); - } - //Float. - else if (acpt == AnimatorControllerParameterType.Float) - { - float value = reader.ReadSingle(); - //If able to smooth floats. - if (_canSmoothFloats) - { - float currentValue = _animator.GetFloat(_parameterDetails[parameterIndex].Hash); - float past = (float)TimeManager.TickDelta; - //float past = _synchronizeInterval + INTERPOLATION; - float rate = Mathf.Abs(currentValue - value) / past; - _smoothedFloats[_parameterDetails[parameterIndex].Hash] = new(rate, value); - } - else - { - _animator.SetFloat(_parameterDetails[parameterIndex].Hash, value); - } - } - //Integer. - else if (acpt == AnimatorControllerParameterType.Int) - { - int value = reader.ReadInt32(); - _animator.SetInteger(_parameterDetails[parameterIndex].Hash, value); - } - //Trigger. - else if (acpt == AnimatorControllerParameterType.Trigger) - { - bool value = reader.ReadBoolean(); - if (value) - _animator.SetTrigger(_parameterDetails[parameterIndex].Hash); - else - _animator.ResetTrigger(_parameterDetails[parameterIndex].Hash); - } - //Unhandled. - else - { - NetworkManager.LogWarning($"Unhandled parameter type of {acpt}."); - } - } - } - } - catch - { - NetworkManager.LogWarning("An error occurred while applying updates. This may occur when malformed data is sent or when you change the animator or controller but not on all connections."); - } - finally - { - reader?.Store(); - } - } - } - - /// - /// Outputs the current state and time for a layer. Returns true if stateHash is not 0. - /// - /// - /// - /// - /// - /// - private bool ReturnCurrentLayerState(out int stateHash, out float normalizedTime, int layerIndex) - { - stateHash = 0; - normalizedTime = 0f; - - if (!_canSynchronizeAnimator) - return false; - - AnimatorStateInfo st = _animator.GetCurrentAnimatorStateInfo(layerIndex); - stateHash = st.fullPathHash; - normalizedTime = st.normalizedTime; - - return stateHash != 0; - } - - /// - /// Immediately sends all variables and states of layers. - /// This is a very bandwidth intensive operation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SendAll() - { - _forceAllOnTimed = true; - } - - #region Play. - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Play(string name) - { - Play(Animator.StringToHash(name)); - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Play(int hash) - { - for (int i = 0; i < _animator.layerCount; i++) - Play(hash, i, 0f); - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Play(string name, int layer) - { - Play(Animator.StringToHash(name), layer); - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Play(int hash, int layer) - { - Play(hash, layer, 0f); - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Play(string name, int layer, float normalizedTime) - { - Play(Animator.StringToHash(name), layer, normalizedTime); - } - - /// - /// Plays a state. - /// - public void Play(int hash, int layer, float normalizedTime) - { - if (!_canSynchronizeAnimator) - return; - if (_animator.HasState(layer, hash) || hash == 0) - { - _animator.Play(hash, layer, normalizedTime); - _unsynchronizedLayerStates[layer] = new(Time.frameCount); - } - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void PlayInFixedTime(string name, float fixedTime) - { - PlayInFixedTime(Animator.StringToHash(name), fixedTime); - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void PlayInFixedTime(int hash, float fixedTime) - { - for (int i = 0; i < _animator.layerCount; i++) - PlayInFixedTime(hash, i, fixedTime); - } - - /// - /// Plays a state. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void PlayInFixedTime(string name, int layer, float fixedTime) - { - PlayInFixedTime(Animator.StringToHash(name), layer, fixedTime); - } - - /// - /// Plays a state. - /// - public void PlayInFixedTime(int hash, int layer, float fixedTime) - { - if (!_canSynchronizeAnimator) - return; - if (_animator.HasState(layer, hash) || hash == 0) - { - _animator.PlayInFixedTime(hash, layer, fixedTime); - _unsynchronizedLayerStates[layer] = new(Time.frameCount); - } - } - #endregion - - #region Crossfade. - /// - /// Creates a crossfade from the current state to any other state using normalized times. - /// - /// - /// - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CrossFade(string stateName, float normalizedTransitionDuration, int layer, float normalizedTimeOffset = float.NegativeInfinity, float normalizedTransitionTime = 0.0f) - { - CrossFade(Animator.StringToHash(stateName), normalizedTransitionDuration, layer, normalizedTimeOffset, normalizedTransitionTime); - } - - /// - /// Creates a crossfade from the current state to any other state using normalized times. - /// - /// - /// - /// - /// - /// - public void CrossFade(int hash, float normalizedTransitionDuration, int layer, float normalizedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) - { - if (!_canSynchronizeAnimator) - return; - if (_animator.HasState(layer, hash) || hash == 0) - { - _animator.CrossFade(hash, normalizedTransitionDuration, layer, normalizedTimeOffset, normalizedTransitionTime); - _unsynchronizedLayerStates[layer] = new(Time.frameCount, hash, false, normalizedTransitionDuration, normalizedTimeOffset, normalizedTransitionTime); - } - } - - /// - /// Creates a crossfade from the current state to any other state using times in seconds. - /// - /// - /// - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void CrossFadeInFixedTime(string stateName, float fixedTransitionDuration, int layer, float fixedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) - { - CrossFadeInFixedTime(Animator.StringToHash(stateName), fixedTransitionDuration, layer, fixedTimeOffset, normalizedTransitionTime); - } - - /// - /// Creates a crossfade from the current state to any other state using times in seconds. - /// - /// - /// - /// - /// - /// - public void CrossFadeInFixedTime(int hash, float fixedTransitionDuration, int layer, float fixedTimeOffset = 0.0f, float normalizedTransitionTime = 0.0f) - { - if (!_canSynchronizeAnimator) - return; - if (_animator.HasState(layer, hash) || hash == 0) - { - _animator.CrossFadeInFixedTime(hash, fixedTransitionDuration, layer, fixedTimeOffset, normalizedTransitionTime); - _unsynchronizedLayerStates[layer] = new(Time.frameCount, hash, true, fixedTransitionDuration, fixedTimeOffset, normalizedTransitionTime); - } - } - #endregion - - #region Triggers. - /// - /// Sets a trigger on the animator and sends it over the network. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetTrigger(int hash) - { - if (!_canSynchronizeAnimator) - return; - UpdateTrigger(hash, true); - } - - /// - /// Sets a trigger on the animator and sends it over the network. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetTrigger(string name) - { - SetTrigger(Animator.StringToHash(name)); - } - - /// - /// Resets a trigger on the animator and sends it over the network. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResetTrigger(int hash) - { - UpdateTrigger(hash, false); - } - - /// - /// Resets a trigger on the animator and sends it over the network. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResetTrigger(string name) - { - ResetTrigger(Animator.StringToHash(name)); - } - - /// - /// Updates a trigger, sets or resets. - /// - /// - private void UpdateTrigger(int hash, bool set) - { - if (!_canSynchronizeAnimator) - return; - - bool clientAuth = ClientAuthoritative; - //If there is an owner perform checks. - if (Owner.IsValid) - { - //If client auth and not owner. - if (clientAuth && !IsOwner) - return; - } - //There is no owner. - else - { - if (!IsServerStarted) - return; - } - - //Update locally. - if (set) - _animator.SetTrigger(hash); - else - _animator.ResetTrigger(hash); - - /* Can send if any of the following are true: - * ClientAuth + Owner. - * ClientAuth + No Owner + IsServer - * !ClientAuth + IsServer. */ - bool canSend = (clientAuth && IsOwner) || (clientAuth && !Owner.IsValid) || (!clientAuth && IsServerStarted); - - //Only queue a send if proper side. - if (canSend) - { - for (byte i = 0; i < _parameterDetails.Count; i++) - { - if (_parameterDetails[i].Hash == hash) - { - _triggerUpdates.Add(new(i, set)); - return; - } - } - //Fall through, hash not found. - NetworkManager.LogWarning($"Hash {hash} not found while trying to update a trigger."); - } - } - #endregion - - #region Remote actions. - /// - /// Called on clients to receive an animator update. - /// - /// - [TargetRpc(ValidateTarget = false)] - private void TargetAnimatorUpdated(NetworkConnection connection, ArraySegment data) - { - if (!_canSynchronizeAnimator) - return; - - //If receiver is client host then do nothing, clientHost need not process. - if (IsServerInitialized && connection.IsLocalClient) - return; - - bool clientAuth = ClientAuthoritative; - bool isOwner = IsOwner; - /* If set for client auth and owner then do not process. - * This could be the case if an update was meant to come before - * ownership gain but came out of late due to out of order when using unreliable. - * Cannot check sendToOwner given clients may not - * always be aware of owner depending on ShareIds setting. */ - if (clientAuth && isOwner) - return; - /* If not client auth and not to send to owner, and is owner - * then also return. */ - if (!clientAuth && !_sendToOwner && isOwner) - return; - - ReceivedServerData rd = new(data); - _fromServerBuffer.Enqueue(rd); - - if (_startTick == 0) - _startTick = TimeManager.LocalTick + _interpolation; - } - - /// - /// Called on server to receive an animator update. - /// - /// - [ServerRpc] - private void ServerAnimatorUpdated(ArraySegment data) - { - if (!_canSynchronizeAnimator) - return; - if (!ClientAuthoritative) - { - Owner.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection Id {Owner.ClientId} has been kicked for trying to update this object without client authority."); - return; - } - - /* Server does not need to apply interpolation. - * Even as clientHost when CSP is being used the - * clientHost will always be on the latest tick. - * Spectators on the other hand will remain behind - * a little depending on their components interpolation. */ - ApplyParametersUpdated(ref data); - _clientAuthoritativeUpdates.AddToBuffer(ref data); - } - #endregion - - #region Editor. - #if UNITY_EDITOR - protected override void Reset() - { - base.Reset(); - if (_animator == null) - SetAnimator(GetComponent()); - } - #endif - #endregion - } -} \ No newline at end of file From dc0606bd222ecd9345f07427b75657c3b9d2f82e Mon Sep 17 00:00:00 2001 From: waterb <143908720+belplaton@users.noreply.github.com> Date: Thu, 18 Dec 2025 03:42:12 +0300 Subject: [PATCH 9/9] Refactor ManagedObjects class for clarity and structure --- Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs b/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs index 5e52c507..2051ee58 100644 --- a/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs +++ b/Assets/FishNet/Runtime/Managing/Object/ManagedObjects.cs @@ -76,7 +76,7 @@ protected void HandleAdd(NetworkObject nob) protected void HandleRemove(NetworkObject nob) { if (_spawned.Remove(nob.ObjectId)) - OnSpawnedAdd?.Invoke(nob.ObjectId, nob); + OnSpawnedRemove?.Invoke(nob.ObjectId, nob); } protected void HandleClear() @@ -541,4 +541,5 @@ protected void CheckReadSceneObjectDetails(Reader r, ref string sceneName, ref s } #endif } -} \ No newline at end of file + +}