// 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; using System.Collections.Generic; using Doozy.Runtime.Common; using Doozy.Runtime.Global; using Doozy.Runtime.Mody; using UnityEngine; using UnityEngine.Events; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBeProtected.Global namespace Doozy.Runtime.UIManager.Content.Internal { public abstract class DateTimeComponent : MonoBehaviour { /// Minimum value for the update interval // ReSharper disable once MemberCanBePrivate.Global protected const float k_MinimumUpdateInterval = 0.001f; /// The timescale mode to use when updating the time public Timescale TimescaleMode = Timescale.Independent; [Space(5)] [SerializeField] protected float UpdateInterval; /// The interval in seconds between each update public float updateInterval { get => UpdateInterval; set { UpdateInterval = Mathf.Max(k_MinimumUpdateInterval, value); waitRealtime = new WaitForSecondsRealtime(UpdateInterval); wait = new WaitForSeconds(UpdateInterval); } } /// /// Behaviour on Start /// What should happen when the component starts (Start method is called) /// public TimerBehaviour OnStartBehaviour = TimerBehaviour.Disabled; /// /// Behaviour OnEnable /// What should happen when the component is enabled (OnEnable method is called) /// public TimerBehaviour OnEnableBehaviour = TimerBehaviour.ResetAndStart; /// /// Behaviour OnDisable /// What should happen when the component is disabled (OnDisable method is called) /// public TimerBehaviour OnDisableBehaviour = TimerBehaviour.Finish; /// /// Behaviour OnDestroy /// What should happen when the component is destroyed (OnDestroy method is called) /// public TimerBehaviour OnDestroyBehaviour = TimerBehaviour.Cancel; [SerializeField] private List Labels; /// /// List of labels that will be updated with relevant time information /// public List labels => Labels ?? (Labels = new List()); /// Callback triggered when the timer starts public ModyEvent OnStart = new ModyEvent(); /// /// Callback triggered when the timer starts. /// This is a quick access to the OnStart ModyEvent. public UnityEvent onStartEvent => OnStart.Event; /// /// Callback triggered when the timer stops /// public ModyEvent OnStop = new ModyEvent(); /// /// Callback triggered when the timer stops. /// This is a quick access to the OnStop ModyEvent. public UnityEvent onStopEvent => OnStop.Event; /// /// Callback triggered when the timer reaches the target time /// public ModyEvent OnFinish = new ModyEvent(); /// /// Callback triggered when the timer reaches the target time. /// This is a quick access to the OnFinish ModyEvent. /// public UnityEvent onFinishEvent => OnFinish.Event; /// /// Callback triggered when the timer is canceled /// public ModyEvent OnCancel = new ModyEvent(); /// /// Callback triggered when the timer is canceled. /// This is a quick access to the OnCancel ModyEvent. /// public UnityEvent onCancelEvent => OnFinish.Event; /// /// Callback triggered when the timer is paused /// public ModyEvent OnPause = new ModyEvent(); /// /// Callback triggered when the timer is paused. /// This is a quick access to the OnPause ModyEvent. /// public UnityEvent onPauseEvent => OnPause.Event; /// /// Callback triggered when the timer is resumed /// public ModyEvent OnResume = new ModyEvent(); /// /// Callback triggered when the timer is resumed. /// This is a quick access to the OnResume ModyEvent. /// public UnityEvent onResumeEvent => OnResume.Event; /// /// Callback triggered when the timer is reset /// public ModyEvent OnReset = new ModyEvent(); /// /// Callback triggered when the timer is reset. /// This is a quick access to the OnReset ModyEvent. /// public UnityEvent onResetEvent => OnReset.Event; /// /// Callback triggered when the timer is updated /// public ModyEvent OnUpdate = new ModyEvent(); /// /// Callback triggered when the timer is updated. /// This is a quick access to the OnUpdate ModyEvent. /// public UnityEvent onUpdateEvent => OnUpdate.Event; /// Returns TRUE if this timer has at least one callback assigned public bool hasCallbacks => OnStart.hasCallbacks || OnStop.hasCallbacks || OnFinish.hasCallbacks || OnCancel.hasCallbacks || OnPause.hasCallbacks || OnResume.hasCallbacks || OnReset.hasCallbacks || OnUpdate.hasCallbacks; [SerializeField] protected int Years; /// Years public int years => Years; [SerializeField] protected int Months; /// Months public int months => Months; [SerializeField] protected int Days; /// Days public int days => Days; [SerializeField] protected int Hours; /// Hours public int hours => Hours; [SerializeField] protected int Minutes; /// Minutes public int minutes => Minutes; [SerializeField] protected int Seconds; /// Seconds public int seconds => Seconds; [SerializeField] protected int Milliseconds; /// Milliseconds public int milliseconds => Milliseconds; /// Start time of the timer (when is running) public DateTime startTime { get; protected set; } /// Current time of the timer (when is running) public DateTime currentTime { get; protected set; } /// End time of the timer (when is running) public DateTime endTime { get; protected set; } /// Elapsed time since the timer started (when is running) public TimeSpan elapsedTime { get; protected set; } /// Remaining time until currentTime reaches endTime (when is running) public TimeSpan remainingTime { get; protected set; } /// Returns TRUE if the time update is running public bool isRunning { get; protected set; } /// Returns TRUE if the time update is running, but it's paused public bool isPaused { get; private set; } /// Returns TRUE if the time update has finished protected bool isFinished => remainingTime.TotalMilliseconds <= 0; /// /// Keeps track of the last time the time was updated, /// when the TimeScaleMode is set to 'Dependent'. /// This value is used to calculate the lastDeltaTime value. /// protected double lastTime { get; set; } /// /// Keeps track of the last time the time was updated, /// when the TimeScaleMode is set to 'Independent'. /// This value is used to calculate the lastUnscaledDeltaTime value. /// protected double lastUnscaledTime { get; set; } /// /// The time that has passed since the last time the time was updated, /// when the TimeScaleMode is set to 'Dependent' /// protected double lastDeltaTime => Time.timeAsDouble - lastTime; /// /// The time that has passed since the last time the time was updated, /// when the TimeScaleMode is set to 'Independent' /// protected double lastUnscaledDeltaTime => Time.realtimeSinceStartupAsDouble - lastUnscaledTime; /// Flag used to track if the update interval has changed when this component is running protected float previousUpdateInterval { get; set; } /// /// Custom YieldInstruction that waits for the specified amount of seconds (unscaled time) /// This is used to lower the GC allocation of the time update coroutine /// protected WaitForSecondsRealtime waitRealtime { get; set; } /// /// Custom YieldInstruction that waits for the specified amount of seconds (scaled by the time scale). /// This is used to lower the GC allocation of the time update coroutine /// protected WaitForSeconds wait { get; set; } /// Coroutine that updates the timer protected Coroutine updateCoroutine { get; set; } #if UNITY_EDITOR protected virtual void Reset() { TimescaleMode = Timescale.Independent; isRunning = false; isPaused = false; updateInterval = 0.1f; } #endif // UNITY_EDITOR protected virtual void Awake() { startTime = DateTime.Now; currentTime = DateTime.Now; endTime = DateTime.Now; isRunning = false; isPaused = false; updateInterval = UpdateInterval; } protected void Start() { RunBehaviour(OnStartBehaviour); } protected virtual void OnEnable() { //make sure the update interval is not 0 or less updateInterval = UpdateInterval; RunBehaviour(OnEnableBehaviour); } protected virtual void OnDisable() { switch (OnDisableBehaviour) { case TimerBehaviour.Disabled: case TimerBehaviour.Stop: case TimerBehaviour.StopAndReset: case TimerBehaviour.Pause: case TimerBehaviour.Reset: case TimerBehaviour.Finish: case TimerBehaviour.Cancel: RunBehaviour(OnDisableBehaviour); break; case TimerBehaviour.Start: case TimerBehaviour.Resume: case TimerBehaviour.ResetAndStart: Debug.LogWarning ( $"[{name}][{GetType().Name}] OnDisable Behaviour is set to '{OnDisableBehaviour}'. " + $"This doesn't make sense. " + $"Doing nothing." ); break; default: throw new ArgumentOutOfRangeException(); } } protected virtual void OnDestroy() { switch (OnDisableBehaviour) { case TimerBehaviour.Disabled: case TimerBehaviour.Stop: case TimerBehaviour.Finish: case TimerBehaviour.Cancel: RunBehaviour(OnDisableBehaviour); break; case TimerBehaviour.StopAndReset: case TimerBehaviour.Pause: case TimerBehaviour.Reset: case TimerBehaviour.Start: case TimerBehaviour.Resume: case TimerBehaviour.ResetAndStart: Debug.LogWarning ( $"[{name}][{GetType().Name}] OnDisable Behaviour is set to '{OnDisableBehaviour}'. " + $"This doesn't make sense. " + $"Doing nothing." ); break; default: throw new ArgumentOutOfRangeException(); } RunBehaviour(OnDestroyBehaviour); StopUpdateCoroutine(); } protected virtual void OnApplicationPause(bool pauseStatus) { RunBehaviour ( pauseStatus ? TimerBehaviour.Pause : TimerBehaviour.Resume ); } /// /// Coroutine responsible for updating the current time /// protected virtual IEnumerator TimeUpdateCoroutine() { waitRealtime ??= new WaitForSecondsRealtime(UpdateInterval); wait ??= new WaitForSeconds(UpdateInterval); previousUpdateInterval = UpdateInterval; while (isRunning) { if (isPaused) { yield return null; lastTime = Time.timeAsDouble; lastUnscaledTime = (float)Time.realtimeSinceStartupAsDouble; continue; } //check if the update interval has changed if (Math.Abs(previousUpdateInterval - UpdateInterval) > 0.001f) { waitRealtime = new WaitForSecondsRealtime(UpdateInterval); wait = new WaitForSeconds(UpdateInterval); previousUpdateInterval = UpdateInterval; } switch (TimescaleMode) { case Timescale.Independent: yield return waitRealtime; break; case Timescale.Dependent: yield return wait; break; default: throw new ArgumentOutOfRangeException(); } UpdateCurrentTime(); OnUpdate.Execute(); if (currentTime < endTime) continue; isRunning = false; OnFinish?.Execute(); } } /// /// Set the start time value for this timer /// protected virtual void SetStartTime() { startTime = DateTime.Now; UpdateLastTime(); } /// /// Set the end time value for this timer /// protected virtual void SetEndTime() { endTime = startTime .AddYears(Years) .AddMonths(Months) .AddDays(Days) .AddHours(Hours) .AddMinutes(Minutes) .AddSeconds(Seconds) .AddMilliseconds(Milliseconds); } /// /// Update the current time, the elapsed time and the remaining time /// protected virtual void UpdateCurrentTime() { switch (TimescaleMode) { case Timescale.Independent: currentTime = currentTime.AddMilliseconds(lastUnscaledDeltaTime * 1000); break; case Timescale.Dependent: currentTime = currentTime.AddMilliseconds(lastDeltaTime * 1000); break; default: throw new ArgumentOutOfRangeException(); } elapsedTime = currentTime.Subtract(startTime); remainingTime = endTime.Subtract(currentTime); UpdateLastTime(); } public virtual void UpdateLabels() { for (int i = 0; i < labels.Count; i++) { if (labels[i].Label == null) continue; labels[i].SetText(currentTime); } } /// /// Reset the timer and call the OnReset event. /// If the timer is running, it will be stopped and the OnStop event will be called before the OnReset event. /// public virtual void ResetTimer() { updateInterval = UpdateInterval; StopUpdateCoroutine(); isRunning = false; isPaused = false; OnReset?.Execute(); SetStartTime(); SetEndTime(); currentTime = startTime; elapsedTime = TimeSpan.Zero; remainingTime = endTime - startTime; } /// /// Start the timer and call the OnStart event. /// If the timer is already running and is paused, it will resume the timer. /// It does nothing if the timer is already running and is not paused. /// public virtual void StartTimer() { if (isPaused) { ResumeTimer(); return; } if (isRunning) return; SetStartTime(); SetEndTime(); currentTime = startTime; elapsedTime = TimeSpan.Zero; remainingTime = endTime - startTime; OnStart?.Execute(); isRunning = true; UpdateCurrentTime(); if (!isActiveAndEnabled) return; StartUpdateCoroutine(); } /// /// Stop the timer and call the OnStop event. /// It does nothing if the timer is not running. /// public virtual void StopTimer() { StopUpdateCoroutine(); if (!isRunning) return; OnStop?.Execute(); isRunning = false; isPaused = false; } /// /// Pause the timer and call the OnPause event. /// It does nothing if the timer is not running or if it's already paused. /// public virtual void PauseTimer() { if (!isRunning || isPaused) return; OnPause?.Execute(); isPaused = true; } /// /// Resume the timer, from a paused state, and call the OnResume event. /// It does nothing if the timer is not running and not paused. /// public virtual void ResumeTimer() { if (!isRunning) return; if (!isPaused) return; OnResume?.Execute(); isPaused = false; } /// /// Stop the timer, call the OnStop event, and then call the OnFinish event. /// It does nothing if the timer is not running. /// public virtual void FinishTimer() { StopUpdateCoroutine(); currentTime = endTime; UpdateCurrentTime(); StopTimer(); OnFinish?.Execute(); } /// /// Cancel the timer and trigger the OnCancel event. /// It does nothing if the timer is not running. /// public virtual void CancelTimer() { StopUpdateCoroutine(); if (isRunning) OnCancel?.Execute(); isRunning = false; isPaused = false; } /// Runs the given timer behavior. /// The timer behavior to run. protected virtual void RunBehaviour(TimerBehaviour behaviour) { // if (!isActiveAndEnabled) return; switch (behaviour) { case TimerBehaviour.Disabled: //do nothing break; case TimerBehaviour.Start: StartTimer(); break; case TimerBehaviour.Stop: StopTimer(); break; case TimerBehaviour.ResetAndStart: ResetTimer(); StartTimer(); break; case TimerBehaviour.StopAndReset: StopTimer(); ResetTimer(); break; case TimerBehaviour.Pause: PauseTimer(); break; case TimerBehaviour.Resume: ResumeTimer(); break; case TimerBehaviour.Reset: ResetTimer(); break; case TimerBehaviour.Finish: FinishTimer(); break; case TimerBehaviour.Cancel: CancelTimer(); break; default: throw new ArgumentOutOfRangeException(nameof(behaviour), behaviour, null); } } /// Update the last time values protected void UpdateLastTime() { lastTime = Time.timeAsDouble; lastUnscaledTime = Time.realtimeSinceStartupAsDouble; } /// Start the update coroutine protected void StartUpdateCoroutine() { StopUpdateCoroutine(); updateCoroutine = Coroutiner.Start(TimeUpdateCoroutine()); } /// Stop the update coroutine protected void StopUpdateCoroutine() { if (updateCoroutine == null) return; Coroutiner.Stop(updateCoroutine); } } }