using System; using UnityEngine; namespace Unity.Netcode { /// /// A variable that can be synchronized over the network. /// /// the unmanaged type for [Serializable] [GenerateSerializationForGenericParameter(0)] public class NetworkVariable : NetworkVariableBase { /// /// Delegate type for value changed event /// /// The value before the change /// The new value public delegate void OnValueChangedDelegate(T previousValue, T newValue); /// /// The callback to be invoked when the value gets changed /// public OnValueChangedDelegate OnValueChanged; public delegate bool CheckExceedsDirtinessThresholdDelegate(in T previousValue, in T newValue); public CheckExceedsDirtinessThresholdDelegate CheckExceedsDirtinessThreshold; public override bool ExceedsDirtinessThreshold() { if (CheckExceedsDirtinessThreshold != null && m_HasPreviousValue) { return CheckExceedsDirtinessThreshold(m_PreviousValue, m_InternalValue); } return true; } public override void OnInitialize() { base.OnInitialize(); m_HasPreviousValue = true; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); } internal override NetworkVariableType Type => NetworkVariableType.Value; /// /// Constructor for /// /// initial value set that is of type T /// the for this /// the for this public NetworkVariable(T value = default, NetworkVariableReadPermission readPerm = DefaultReadPerm, NetworkVariableWritePermission writePerm = DefaultWritePerm) : base(readPerm, writePerm) { m_InternalValue = value; m_InternalOriginalValue = default; // Since we start with IsDirty = true, this doesn't need to be duplicated // right away. It won't get read until after ResetDirty() is called, and // the duplicate will be made there. Avoiding calling // NetworkVariableSerialization.Duplicate() is important because calling // it in the constructor might not give users enough time to set the // DuplicateValue callback if they're using UserNetworkVariableSerialization m_PreviousValue = default; } /// /// Resets the NetworkVariable when the associated NetworkObject is not spawned /// /// the value to reset the NetworkVariable to (if none specified it resets to the default) public void Reset(T value = default) { if (m_NetworkBehaviour == null || m_NetworkBehaviour != null && !m_NetworkBehaviour.NetworkObject.IsSpawned) { m_InternalValue = value; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); m_PreviousValue = default; } } /// /// The internal value of the NetworkVariable /// [SerializeField] private protected T m_InternalValue; // The introduction of standard .NET collections caused an issue with permissions since there is no way to detect changes in the // collection without doing a full comparison. While this approach does consume more memory per collection instance, it is the // lowest risk approach to resolving the issue where a client with no write permissions could make changes to a collection locally // which can cause a myriad of issues. private protected T m_InternalOriginalValue; private protected T m_PreviousValue; private bool m_HasPreviousValue; private bool m_IsDisposed; /// /// The value of the NetworkVariable container /// /// /// When assigning collections to , unless it is a completely new collection this will not /// detect any deltas with most managed collection classes since assignment of one collection value to another /// is actually just a reference to the collection itself.
/// To detect deltas in a collection, you should invoke after making modifications to the collection. ///
public virtual T Value { get => m_InternalValue; set { if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId)) { LogWritePermissionError(); return; } // Compare the Value being applied to the current value if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref value)) { T previousValue = m_InternalValue; m_InternalValue = value; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); SetDirty(true); m_IsDisposed = false; OnValueChanged?.Invoke(previousValue, m_InternalValue); } } } /// /// Invoke this method to check if a collection's items are dirty. /// The default behavior is to exit early if the is already dirty. /// /// when true, this check will force a full item collection check even if the NetworkVariable is already dirty /// /// This is to be used as a way to check if a containing a managed collection has any changees to the collection items.
/// If you invoked this when a collection is dirty, it will not trigger the unless you set to true.
///
public bool CheckDirtyState(bool forceCheck = false) { var isDirty = base.IsDirty(); // A client without permissions invoking this method should only check to assure the current value is equal to the last known current value if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId)) { // If modifications are detected, then revert back to the last known current value if (!NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue)) { NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); } return false; } // Compare the previous with the current if not dirty or forcing a check. if ((!isDirty || forceCheck) && !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue)) { SetDirty(true); OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue); m_IsDisposed = false; isDirty = true; } return isDirty; } internal ref T RefValue() { return ref m_InternalValue; } public override void Dispose() { if (m_IsDisposed) { return; } m_IsDisposed = true; if (m_InternalValue is IDisposable internalValueDisposable) { internalValueDisposable.Dispose(); } m_InternalValue = default; m_InternalOriginalValue = default; if (m_HasPreviousValue && m_PreviousValue is IDisposable previousValueDisposable) { m_HasPreviousValue = false; previousValueDisposable.Dispose(); } m_PreviousValue = default; base.Dispose(); } ~NetworkVariable() { Dispose(); } /// /// Gets Whether or not the container is dirty /// /// Whether or not the container is dirty public override bool IsDirty() { // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert // to the original collection value prior to applying updates (primarily for collections). if (!NetworkUpdaterCheck && m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_InternalOriginalValue)) { NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); return true; } // For most cases we can use the dirty flag. // This doesn't work for cases where we're wrapping more complex types // like INetworkSerializable, NativeList, NativeArray, etc. // Changes to the values in those types don't call the Value.set method, // so we can't catch those changes and need to compare the current value // against the previous one. if (base.IsDirty()) { return true; } var dirty = !NetworkVariableSerialization.AreEqual(ref m_PreviousValue, ref m_InternalValue); // Cache the dirty value so we don't perform this again if we already know we're dirty // Unfortunately we can't cache the NOT dirty state, because that might change // in between to checks... but the DIRTY state won't change until ResetDirty() // is called. SetDirty(dirty); return dirty; } /// /// Resets the dirty state and marks the variable as synced / clean /// public override void ResetDirty() { // Resetting the dirty value declares that the current value is not dirty // Therefore, we set the m_PreviousValue field to a duplicate of the current // field, so that our next dirty check is made against the current "not dirty" // value. if (IsDirty()) { m_HasPreviousValue = true; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); // Once updated, assure the original current value is updated for future comparison purposes NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); } base.ResetDirty(); } /// /// Writes the variable to the writer /// /// The stream to write the value to public override void WriteDelta(FastBufferWriter writer) { NetworkVariableSerialization.WriteDelta(writer, ref m_InternalValue, ref m_PreviousValue); } /// /// Reads value from the reader and applies it /// /// The stream to read the value from /// Whether or not the container should keep the dirty delta, or mark the delta as consumed public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert // to the original collection value prior to applying updates (primarily for collections). if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue)) { NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); } NetworkVariableSerialization.ReadDelta(reader, ref m_InternalValue); // keepDirtyDelta marks a variable received as dirty and causes the server to send the value to clients // In a prefect world, whether a variable was A) modified locally or B) received and needs retransmit // would be stored in different fields // LEGACY NOTE: This is only to handle NetworkVariableDeltaMessage Version 0 connections. The updated // NetworkVariableDeltaMessage no longer uses this approach. if (keepDirtyDelta) { SetDirty(true); } OnValueChanged?.Invoke(m_PreviousValue, m_InternalValue); } /// /// This should be always invoked (client & server) to assure the previous values are set /// !! IMPORTANT !! /// When a server forwards delta updates to connected clients, it needs to preserve the previous dirty value(s) /// until it is done serializing all valid NetworkVariable field deltas (relative to each client). This is invoked /// after it is done forwarding the deltas at the end of the method. /// internal override void PostDeltaRead() { // In order to get managed collections to properly have a previous and current value, we have to // duplicate the collection at this point before making any modifications to the current. m_HasPreviousValue = true; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); // Once updated, assure the original current value is updated for future comparison purposes NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); } /// public override void ReadField(FastBufferReader reader) { // If the client does not have write permissions but the internal value is determined to be locally modified and we are applying updates, then we should revert // to the original collection value prior to applying updates (primarily for collections). if (m_NetworkManager && !CanClientWrite(m_NetworkManager.LocalClientId) && !NetworkVariableSerialization.AreEqual(ref m_InternalOriginalValue, ref m_InternalValue)) { NetworkVariableSerialization.Duplicate(m_InternalOriginalValue, ref m_InternalValue); } NetworkVariableSerialization.Read(reader, ref m_InternalValue); // In order to get managed collections to properly have a previous and current value, we have to // duplicate the collection at this point before making any modifications to the current. // We duplicate the final value after the read (for ReadField ONLY) so the previous value is at par // with the current value (since this is only invoked when initially synchronizing). m_HasPreviousValue = true; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); // Once updated, assure the original current value is updated for future comparison purposes NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_InternalOriginalValue); } /// public override void WriteField(FastBufferWriter writer) { NetworkVariableSerialization.Write(writer, ref m_InternalValue); } internal override void WriteFieldSynchronization(FastBufferWriter writer) { // If we have a pending update, then synchronize the client with the previously known // value since the updated version will be sent on the next tick or next time it is // set to be updated if (base.IsDirty() && m_HasPreviousValue) { NetworkVariableSerialization.Write(writer, ref m_PreviousValue); } else { base.WriteFieldSynchronization(writer); } } } }