using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityAtoms { /// /// Generic base class for Variables. Inherits from `AtomBaseVariable<T>`. /// /// The Variable value type. /// IPair of type `T`. /// Event of type `AtomEvent<T>`. /// Event of type `AtomEvent<T, T>`. /// Function of type `FunctionEvent<T, T>`. [EditorIcon("atom-icon-lush")] public abstract class AtomVariable : AtomBaseVariable, IGetEvent, ISetEvent, IGetOrCreateEvent where P : struct, IPair where E1 : AtomEvent where E2 : AtomEvent

where F : AtomFunction { ///

/// The Variable value as a property. /// /// Get or set the Variable's value. public override T Value { get { #if UNITY_EDITOR try { if (!Application.isPlaying) return InitialValue; } catch (UnityException) { // "get_isPlaying is not allowed to be called during serialization" // -> while compiling we can savely return InitialValue as OnEnable is to about to set Value = InitialValue return InitialValue; } #endif return _value; } set => SetValue(value); } /// /// The initial value as a property. /// /// Get the Variable's initial value. public virtual T InitialValue { get => _initialValue; set => _initialValue = value; } /// /// The value the Variable had before its value got changed last time. /// /// Get the Variable's old value. public T OldValue { get => _oldValue; } /// /// Changed Event triggered when the Variable value gets changed. /// [SerializeField] [FormerlySerializedAs("Changed")] private E1 _changed; public E1 Changed { get { GetOrCreateEvent(); return _changed; } set { _changed = value; } } /// /// Changed with history Event triggered when the Variable value gets changed. /// [SerializeField] [FormerlySerializedAs("ChangedWithHistory")] private E2 _changedWithHistory; public E2 ChangedWithHistory { get { GetOrCreateEvent(); return _changedWithHistory; } set { _changedWithHistory = value; } } /// /// Whether Changed Event should be triggered on OnEnable or not /// [SerializeField] private bool _triggerChangedOnOnEnable = default; /// /// Whether ChangedWithHistory Event should be triggered on OnEnable or not /// [SerializeField] private bool _triggerChangedWithHistoryOnOnEnable = default; [SerializeField] private T _oldValue; /// /// The inital value of the Variable. /// [SerializeField] private T _initialValue = default(T); #if UNITY_EDITOR /// /// Set of all AtomVariable instances in editor. /// private static HashSet> _instances = new HashSet>(); #endif /// /// When setting the value of a Variable the new value will be piped through all the pre change transformers, which allows you to create custom logic and restriction on for example what values can be set for this Variable. /// /// Get the list of pre change transformers. public List PreChangeTransformers { get => _preChangeTransformers; set { if (value == null) { _preChangeTransformers.Clear(); } else { _preChangeTransformers = value; } } } [SerializeField] private List _preChangeTransformers = new List(); protected abstract bool ValueEquals(T other); private void OnValidate() { InitialValue = RunPreChangeTransformers(InitialValue); _value = RunPreChangeTransformers(_value); } protected virtual void OnEnable() { SetInitialValues(); TriggerInitialEvents(); #if UNITY_EDITOR if (EditorSettings.enterPlayModeOptionsEnabled) { _instances.Add(this); EditorApplication.playModeStateChanged -= HandlePlayModeStateChange; EditorApplication.playModeStateChanged += HandlePlayModeStateChange; } #endif } private void OnDisable() { // NOTE: This will not be called when deleting the Atom from the editor. // Therefore, there might still be null instances, but even though not ideal, // it should not cause any problems. // More info: https://issuetracker.unity3d.com/issues/ondisable-and-ondestroy-methods-are-not-called-when-a-scriptableobject-is-deleted-manually-in-project-window #if UNITY_EDITOR _instances.Remove(this); #endif } /// /// Set initial values /// private void SetInitialValues() { _oldValue = InitialValue; _value = InitialValue; } /// /// Trigger initial events if related options enabled /// public void TriggerInitialEvents() { if (_triggerChangedOnOnEnable) { if (Changed == null) GetOrCreateEvent(); Changed.Raise(Value); } if (_triggerChangedWithHistoryOnOnEnable) { if (ChangedWithHistory == null) GetOrCreateEvent(); var pair = default(P); pair.Item1 = _value; pair.Item2 = _oldValue; ChangedWithHistory.Raise(pair); } } #if UNITY_EDITOR private static void HandlePlayModeStateChange(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingEditMode) // BEFORE any GO is initialized: { foreach (var instance in _instances) { instance.SetInitialValues(); } } else if (state == PlayModeStateChange.EnteredPlayMode) // within/end of the first frame { foreach (var instance in _instances) { instance.TriggerInitialEvents(); }; } } #endif /// /// Reset the Variable to its `_initialValue`. /// /// Set to `true` if Events should be triggered on reset, otherwise `false`. public override void Reset(bool shouldTriggerEvents = false) { if (!shouldTriggerEvents) { _oldValue = _value; _value = InitialValue; } else { SetValue(InitialValue); } } /// /// Set the Variable value. /// /// The new value to set. /// `true` if the value got changed, otherwise `false`. public bool SetValue(T newValue, bool forceEvent = false) { var preProcessedNewValue = RunPreChangeTransformers(newValue); var changeValue = !ValueEquals(preProcessedNewValue); var triggerEvents = changeValue || forceEvent; if (changeValue) { _oldValue = _value; _value = preProcessedNewValue; } if (triggerEvents) { if (_changed != null) { _changed.Raise(_value); } if (_changedWithHistory != null) { // NOTE: Doing new P() here, even though it is cleaner, generates garbage. var pair = default(P); pair.Item1 = _value; pair.Item2 = _oldValue; _changedWithHistory.Raise(pair); } } return changeValue; } /// /// Set the Variable value. /// /// The value to set provided from another Variable. /// `true` if the value got changed, otherwise `false`. public bool SetValue(AtomVariable variable) { return SetValue(variable.Value); } #region Observable /// /// Turn the Variable's change Event into an `IObservable<T>`. Makes the Variable's change Event compatible with for example UniRx. /// /// The Variable's change Event as an `IObservable<T>`. public IObservable ObserveChange() { if (Changed == null) { throw new Exception("You must assign a Changed event in order to observe variable changes."); } return new ObservableEvent(Changed.Register, Changed.Unregister); } /// /// Turn the Variable's change with history Event into an `IObservable<T, T>`. Makes the Variable's change with history Event compatible with for example UniRx. /// /// The Variable's change Event as an `IObservable<T, T>`. public IObservable

ObserveChangeWithHistory() { if (ChangedWithHistory == null) { throw new Exception("You must assign a ChangedWithHistory event in order to observe variable changes."); } return new ObservableEvent

(ChangedWithHistory.Register, ChangedWithHistory.Unregister); } #endregion // Observable private T RunPreChangeTransformers(T value) { if (_preChangeTransformers.Count <= 0) { return value; } var preProcessedValue = value; for (var i = 0; i < _preChangeTransformers.Count; ++i) { var Transformer = _preChangeTransformers[i]; if (Transformer != null) { preProcessedValue = Transformer.Call(preProcessedValue); } } return preProcessedValue; } ///

/// Get event by type (allowing inheritance). /// /// /// Changed - If Changed (or ChangedWithHistory) are of type E /// ChangedWithHistory - If not Changed but ChangedWithHistory is of type E /// if none of the events are of type E /// public E GetEvent() where E : AtomEventBase { if(TryGetEvent(out E result)) return result; throw new NotSupportedException($"Event type {typeof(E)} not supported! Use {typeof(E1)} or {typeof(E2)}."); } /// /// Set event by type. /// /// The new event value. /// public void SetEvent(E e) where E : AtomEventBase { if (typeof(E) == typeof(E1)) { Changed = (e as E1); return; } if (typeof(E) == typeof(E2)) { ChangedWithHistory = (e as E2); return; } throw new Exception($"Event type {typeof(E)} not supported! Use {typeof(E1)} or {typeof(E2)}."); } /// /// Get event by type (allowing inheritance). Creates an event if the type is supported for this Variable, but the Event itself is `null`. /// /// /// Changed - If Changed (or ChangedWithHistory) are of type E /// ChangedWithHistory - If not Changed but ChangedWithHistory is of type E /// if none of the events are of type E /// public E GetOrCreateEvent() where E : AtomEventBase { if (TryGetOrCreateEvent(out E result)) return result; throw new NotSupportedException($"Event type {typeof(E)} not supported! Use {typeof(E1)} or {typeof(E2)}."); } public bool TryGetEvent(out E atomEvent) where E : AtomEventBase { if (typeof(E) == typeof(E1)) { atomEvent = Changed as E; return true; } if (typeof(E) == typeof(E2)) { atomEvent = ChangedWithHistory as E; return true; } atomEvent = null; return false; } public bool TryGetOrCreateEvent(out E atomEvent) where E : AtomEventBase { if (typeof(E) == typeof(E1)) { if (_changed == null) { _changed = ScriptableObject.CreateInstance(); _changed.name = $"{(String.IsNullOrWhiteSpace(name) ? "" : $"{name}_")}ChangedEvent_Runtime_{typeof(E1)}"; } atomEvent = _changed as E; return true; } if (typeof(E) == typeof(E2)) { if (_changedWithHistory == null) { _changedWithHistory = ScriptableObject.CreateInstance(); _changedWithHistory.name = $"{(String.IsNullOrWhiteSpace(name) ? "" : $"{name}_")}ChangedWithHistoryEvent_Runtime_{typeof(E2)}"; } atomEvent = _changedWithHistory as E; return true; } atomEvent = null; return false; } } }