123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- // SyncVar<T> to make [SyncVar] weaving easier.
- //
- // we can possibly move a lot of complex logic out of weaver:
- // * set dirty bit
- // * calling the hook
- // * hook guard in host mode
- // * GameObject/NetworkIdentity internal netId storage
- //
- // here is the plan:
- // 1. develop SyncVar<T> along side [SyncVar]
- // 2. internally replace [SyncVar]s with SyncVar<T>
- // 3. eventually obsolete [SyncVar]
- //
- // downsides:
- // - generic <T> types don't show in Unity Inspector
- //
- using System;
- using System.Collections.Generic;
- using System.Runtime.CompilerServices;
- using UnityEngine;
- namespace Mirror
- {
- // 'class' so that we can track it in SyncObjects list, and iterate it for
- // de/serialization.
- [Serializable]
- public class SyncVar<T> : SyncObject, IEquatable<T>
- {
- // Unity 2020+ can show [SerializeField]<T> in inspector.
- // (only if SyncVar<T> isn't readonly though)
- [SerializeField] T _Value;
- // Value property with hooks
- // virtual for SyncFieldNetworkIdentity netId trick etc.
- public virtual T Value
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _Value;
- set
- {
- // only if value changed. otherwise don't dirty/hook.
- // we have .Equals(T), simply reuse it here.
- if (!Equals(value))
- {
- // set value, set dirty bit
- T old = _Value;
- _Value = value;
- OnDirty();
- // Value.set calls the hook if changed.
- // calling Value.set from within the hook would call the
- // hook again and deadlock. prevent it with hookGuard.
- // (see test: Hook_Set_DoesntDeadlock)
- if (!hookGuard &&
- // original [SyncVar] only calls hook on clients.
- // let's keep it for consistency for now
- // TODO remove check & dependency in the future.
- // use isClient/isServer in the hook instead.
- NetworkClient.active)
- {
- hookGuard = true;
- InvokeCallback(old, value);
- hookGuard = false;
- }
- }
- }
- }
- // OnChanged Callback.
- // named 'Callback' for consistency with SyncList etc.
- // needs to be public so we can assign it in OnStartClient.
- // (ctor passing doesn't work, it can only take static functions)
- // assign via: field.Callback += ...!
- public event Action<T, T> Callback;
- // OnCallback is responsible for calling the callback.
- // this is necessary for inheriting classes like SyncVarGameObject,
- // where the netIds should be converted to GOs and call the GO hook.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected virtual void InvokeCallback(T oldValue, T newValue) =>
- Callback?.Invoke(oldValue, newValue);
- // Value.set calls the hook if changed.
- // calling Value.set from within the hook would call the hook again and
- // deadlock. prevent it with a simple 'are we inside the hook' bool.
- bool hookGuard;
- public override void ClearChanges() {}
- public override void Reset() {}
- // ctor from value <T> and OnChanged hook.
- // it was always called 'hook'. let's keep naming for convenience.
- public SyncVar(T value)
- {
- // recommend explicit GameObject, NetworkIdentity, NetworkBehaviour
- // with persistent netId method
- if (this is SyncVar<GameObject>)
- Debug.LogWarning($"Use explicit {nameof(SyncVarGameObject)} class instead of {nameof(SyncVar<T>)}<GameObject>. It stores netId internally for persistence.");
- if (this is SyncVar<NetworkIdentity>)
- Debug.LogWarning($"Use explicit {nameof(SyncVarNetworkIdentity)} class instead of {nameof(SyncVar<T>)}<NetworkIdentity>. It stores netId internally for persistence.");
- if (this is SyncVar<NetworkBehaviour>)
- Debug.LogWarning($"Use explicit SyncVarNetworkBehaviour class instead of {nameof(SyncVar<T>)}<NetworkBehaviour>. It stores netId internally for persistence.");
- _Value = value;
- }
- // NOTE: copy ctor is unnecessary.
- // SyncVar<T>s are readonly and only initialized by 'Value' once.
- // implicit conversion: int value = SyncVar<T>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static implicit operator T(SyncVar<T> field) => field.Value;
- // implicit conversion: SyncVar<T> = value
- // even if SyncVar<T> is readonly, it's still useful: SyncVar<int> = 1;
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static implicit operator SyncVar<T>(T value) => new SyncVar<T>(value);
- // serialization (use .Value instead of _Value so hook is called!)
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override void OnSerializeAll(NetworkWriter writer) => writer.Write(Value);
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override void OnSerializeDelta(NetworkWriter writer) => writer.Write(Value);
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override void OnDeserializeAll(NetworkReader reader) => Value = reader.Read<T>();
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public override void OnDeserializeDelta(NetworkReader reader) => Value = reader.Read<T>();
- // IEquatable should compare Value.
- // SyncVar<T> should act invisibly like [SyncVar] before.
- // this way we can do SyncVar<int> health == 0 etc.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public bool Equals(T other) =>
- // from NetworkBehaviour.SyncVarEquals:
- // EqualityComparer method avoids allocations.
- // otherwise <T> would have to be :IEquatable (not all structs are)
- EqualityComparer<T>.Default.Equals(Value, other);
- // ToString should show Value.
- // SyncVar<T> should act invisibly like [SyncVar] before.
- public override string ToString() => Value.ToString();
- }
- }
|