// 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
}
}