// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using System.Collections.Generic; using System.Linq; using Doozy.Runtime.Common.Attributes; using Doozy.Runtime.Common.Events; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Reactor.Internal; using Doozy.Runtime.Reactor.Reactions; using Doozy.Runtime.Reactor.Ticker; using UnityEngine; using UnityEngine.Events; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBeProtected.Global namespace Doozy.Runtime.Reactor { /// /// Specialized Reactor class that allows you animate a float value over time to be used as a progress bar or indicator. /// [AddComponentMenu("Doozy/Reactor/Progressor")] public partial class Progressor : MonoBehaviour { #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/Reactor/Progressor", false, 6)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene(false, true); } #endif public static HashSet database { get; private set; } = new HashSet(); [ExecuteOnReload] private static void OnReload() { database = new HashSet(); } [SerializeField] private List ProgressTargets; /// Progress targets that get updated by this Progressor when activated public List progressTargets { get { Initialize(); return ProgressTargets; } } [SerializeField] private List ProgressorTargets; /// Other Progressors that get updated by this Progressor when activated public List progressorTargets { get { Initialize(); return ProgressorTargets; } } [SerializeField] protected float FromValue = 0f; /// Start Value public float fromValue { get => FromValue; set { FromValue = value; if (reaction.isActive) reaction.SetFrom(fromValue); } } [SerializeField] protected float ToValue = 1f; /// End Value public float toValue { get => ToValue; set { ToValue = value; if (reaction.isActive) reaction.SetTo(toValue); } } [SerializeField] protected float CurrentValue; /// Current Value public float currentValue => CurrentValue; [SerializeField] protected float Progress; /// Current Progress public float progress => Progress; [SerializeField] protected float CustomResetValue; /// Custom Reset Value public float customResetValue { get => CustomResetValue; set => CustomResetValue = Mathf.Clamp(value, FromValue, ToValue); } [SerializeField] protected FloatReaction Reaction; /// Reaction that runs this progressor public FloatReaction reaction { get { Initialize(); return Reaction; } } /// OnEnable behaviour for this Progressor public ResetValue ResetValueOnEnable = ResetValue.Disabled; /// /// Fired when the value changed. /// Returns the current value. /// public FloatEvent OnValueChanged; /// /// Fired when the progress changed. /// Returns the current progress. /// public FloatEvent OnProgressChanged; /// /// Fired when the value increases. /// Returns the difference between the new value and the previous value. /// Example: if the previous value was 0.5 and the new value is 0.7, the returned value will be 0.2 /// public FloatEvent OnValueIncremented = new FloatEvent(); /// /// Fired when the value decreases. /// Returns the difference between the new value and the previous value. /// Example: if the previous value was 10 and the new value is 5, the returned value will be -5 /// public FloatEvent OnValueDecremented = new FloatEvent(); /// Fired when the value was reset public UnityEvent OnValueReset = new UnityEvent(); /// Fired when the value has reached the fromValue public UnityEvent OnValueReachedFromValue = new UnityEvent(); /// Fired when the value has reached the toValue public UnityEvent OnValueReachedToValue = new UnityEvent(); /// Initialization flag public bool initialized { get; set; } public ProgressorId Id; protected Progressor() { Id = new ProgressorId(); } public virtual void Initialize() { if (initialized) return; ProgressTargets ??= new List(); ProgressorTargets ??= new List(); Reaction = Reaction ?? ReactionPool.Get(); Reaction.SetFrom(fromValue); Reaction.SetTo(toValue); Reaction.SetValue(fromValue); Reaction.OnUpdateCallback = UpdateProgressor; initialized = true; } protected virtual void Awake() { if (!Application.isPlaying) return; database.Add(this); Initialize(); // OnValueChanged.AddListener(newValue => Debug.Log($"[{name}] OnValueChanged: {newValue}")); // OnProgressChanged.AddListener(newProgress => Debug.Log($"[{name}] OnProgressChanged: {newProgress}")); // OnValueIncremented.AddListener(increment => Debug.Log($"[{name}] OnValueIncremented: {increment}")); // OnValueDecremented.AddListener(decrement => Debug.Log($"[{name}] OnValueDecremented: {decrement}")); // OnValueReset.Event.AddListener(() => Debug.Log($"[{name}] OnValueReset")); // OnValueReachedFromValue.Event.AddListener(() => Debug.Log($"[{name}] OnValueReachedFromValue")); // OnValueReachedToValue.Event.AddListener(() => Debug.Log($"[{name}] OnValueReachedToValue")); } protected virtual void OnEnable() { if (!Application.isPlaying) return; CleanDatabase(); ValidateTargets(); ResetCurrentValue(ResetValueOnEnable); } protected virtual void OnDisable() { if (!Application.isPlaying) return; CleanDatabase(); ValidateTargets(); reaction.Stop(); } protected void OnDestroy() { if (!Application.isPlaying) return; database.Remove(this); CleanDatabase(); Reaction?.Recycle(); } /// Remove null and duplicate targets private void ValidateTargets() => ProgressTargets = progressTargets.Where(t => t != null).Distinct().ToList(); /// Reset the Progressor to the given reset value /// Value to reset to protected void ResetCurrentValue(ResetValue resetValue) { if (resetValue == ResetValue.Disabled) return; reaction.SetFrom(FromValue); reaction.SetTo(ToValue); switch (resetValue) { case ResetValue.FromValue: SetProgressAtZero(); OnValueReset?.Invoke(); break; case ResetValue.EndValue: SetProgressAtOne(); OnValueReset?.Invoke(); break; case ResetValue.CustomValue: SetProgressAt(reaction.GetProgressAtValue(CustomResetValue)); OnValueReset?.Invoke(); break; case ResetValue.Disabled: default: throw new ArgumentOutOfRangeException(nameof(resetValue), resetValue, null); } } /// Reset all the reactions to their initial values (if the animation is enabled) public void ResetToStartValues() { if (reaction.isActive) Stop(); ResetCurrentValue(ResetValueOnEnable); #if UNITY_EDITOR if (this == null) return; foreach (ProgressTarget progressTarget in progressTargets) { if (progressTarget == null) continue; UnityEditor.EditorUtility.SetDirty(progressTarget); } UnityEditor.EditorUtility.SetDirty(this); UnityEditor.SceneView.RepaintAll(); #endif } /// Update current value and trigger callbacks public virtual void UpdateProgressor() { float previousValue = CurrentValue; CurrentValue = reaction.currentValue; Progress = Mathf.InverseLerp(fromValue, toValue, currentValue); if (previousValue < CurrentValue) { OnValueIncremented?.Invoke(CurrentValue - previousValue); } else if (previousValue > CurrentValue) { OnValueDecremented?.Invoke(previousValue - CurrentValue); } OnValueChanged?.Invoke(CurrentValue); OnProgressChanged?.Invoke(Progress); if (currentValue.Approximately(fromValue)) { //value reached fromValue -> invoke the OnValueReachedFromValue event OnValueReachedFromValue?.Invoke(); } if (currentValue.Approximately(toValue)) { //value reached toValue -> invoke the OnValueReachedToValue event OnValueReachedToValue?.Invoke(); } ProgressTargets.RemoveNulls(); ProgressTargets.ForEach(t => t.UpdateTarget(this)); ProgressorTargets.RemoveNulls(); for (int i = ProgressorTargets.Count - 1; i >= 0; i--) { if (progressorTargets[i] == this) ProgressorTargets.RemoveAt(i); } ProgressorTargets.ForEach(p => p.SetProgressAt(Progress)); } /// Set the Progressor's current value to the given target value /// Target value public void SetValueAt(float value) { // SetProgressAt(reaction.GetProgressAtValue(Mathf.Clamp(value, fromValue, toValue))); value = Mathf.Clamp(value, fromValue, toValue); reaction.SetFrom(FromValue); reaction.SetTo(ToValue); reaction.SetValue(value); UpdateProgressor(); } /// Set the Progressor's current progress to the given target progress /// Target progress public void SetProgressAt(float targetProgress) { reaction.SetFrom(FromValue); reaction.SetTo(ToValue); reaction.SetProgressAt(targetProgress); UpdateProgressor(); } /// Set the Progressor's current progress to 1 (100%) public void SetProgressAtOne() => SetProgressAt(1f); /// Set the Progressor's current progress to 0 (0%) public void SetProgressAtZero() => SetProgressAt(0f); /// Play from start (from value) to end (to value), depending on the given PlayDirection /// Play direction public void Play(PlayDirection direction) => Play(direction == PlayDirection.Reverse); /// Play from the given start (from value) to end (to value), or in reverse /// Play in reverse? public void Play(bool inReverse = false) { reaction.SetValue(inReverse ? ToValue : FromValue); reaction.Play(FromValue, ToValue, inReverse); } /// Play from the current value to the given target value /// Target value public void PlayToValue(float value) { value = Mathf.Clamp(value, fromValue, toValue); //clamp the value if (Math.Abs(value - fromValue) < 0.001f) { PlayToProgress(0f); return; } if (Math.Abs(value - toValue) < 0.001f) { PlayToProgress(1f); return; } PlayToProgress(Mathf.InverseLerp(fromValue, toValue, value)); } /// Play from the current progress to the given target progress /// Target progress public void PlayToProgress(float toProgress) { float p = Mathf.Clamp01(toProgress); //clamp the progress switch (p) { case 0: reaction.Play(currentValue, fromValue); break; case 1: reaction.Play(currentValue, toValue); break; default: reaction.Play(currentValue, Mathf.Lerp(fromValue, toValue, p)); break; } } /// Stop the Progressor from playing public void Stop() => reaction.Stop(); /// Reverse the Progressor's playing direction (works only if the Progressor is playing) public void Reverse() => reaction.Reverse(); /// Rewind the Progressor to 0% if playing forward or to 100% if playing in reverse public void Rewind() => reaction.Rewind(); /// /// Progressor start delay. /// /// If the Progressor is active, it will return the current start delay, otherwise it returns the value from settings. /// /// For random, it returns the maximum value from settings. /// public float GetStartDelay() => reaction.isActive ? reaction.startDelay : reaction.settings.GetStartDelay(); /// /// Progressor duration. /// /// If the Progressor is active, it will return the current duration, otherwise it returns the value from settings. /// /// For random, it returns the maximum value from settings. /// public float GetDuration() => reaction.isActive ? reaction.duration : reaction.settings.GetDuration(); /// /// Progressor start delay + duration. /// /// If the Progressor is active, it will return the current start delay + current duration, otherwise it returns the summed values from settings. /// /// For random, it returns the maximum value from settings. /// public float GetTotalDuration() => GetStartDelay() + GetDuration(); /// Set animation heartbeat public List SetHeartbeat() where T : Heartbeat, new() { var list = new List() { new T() }; reaction.SetHeartbeat(list[0]); return list; } #region Static Methods /// Remove all null references from the database protected static void CleanDatabase() => database.Remove(null); /// Get all the registered progressors with the given category and name /// Progressor category /// Progressor name (from the given category) public static IEnumerable GetProgressors(string category, string name) => database.Where(p => p.Id.Category.Equals(category)).Where(button => button.Id.Name.Equals(name)); /// Get all the registered progressors with the given category /// Progressor category public static IEnumerable GetAllProgressorsInCategory(string name) => database.Where(p => p.Id.Category.Equals(name)); /// Get all the registered progressors with the given name (regardless of their category) /// Progressor name (from the given category) public static IEnumerable GetAllProgressorsByName(string name) => database.Where(p => p.Id.Name.Equals(name)); #endregion } }