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