// 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 Doozy.Runtime.Common; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Signals; using UnityEngine; using UnityEngine.Events; namespace Doozy.Runtime.Mody { /// /// Base class for actions in the Mody System. /// Any action that interacts with the system needs to derive from this. /// It is used to start, stop and finish Module's (MonoBehaviour's) tasks. /// It can be triggered manually (via code) or by any Trigger registered to the system. /// [Serializable] public abstract class ModyAction : MultiSignalsReceiver { #region Action Behaviour Reference /// /// The MonoBehaviour (Module) that this ModyAction belongs to. /// Needs to implement the IHaveActions interface. /// [SerializeField] private MonoBehaviour ActionBehaviourReference; /// /// The MonoBehaviour (Module) that this ModyAction belongs to. /// Needs to implement the IHaveActions interface. /// public MonoBehaviour actionBehaviourReference { get => ActionBehaviourReference; internal set { ActionBehaviourReference = value; m_BehaviourIsModule = false; if (!(ActionBehaviourReference is ModyModule module)) return; m_Module = module; m_BehaviourIsModule = true; } } #endregion #region Action Name /// Name of the ModyAction [SerializeField] private string ActionName; /// Name of the ModyAction public string actionName => ActionName; #endregion #region Action State /// ModyAction current state (disabled, idle, running or cooldown) [SerializeField] private ActionState ActionCurrentState; /// ModyAction current state (disabled, idle, running or cooldown) public ActionState currentState { get => ActionCurrentState; private set { // Debug.Log($"{ActionName} - CurrentState - From: {ActionCurrentState} To: {value}"); ActionCurrentState = value; if (!m_BehaviourIsModule) return; m_Module.UpdateState(); } } /// ModyAction is ready and can be triggered public bool isIdle => currentState == ActionState.Idle; /// ModyAction has started and is preparing to run public bool inStartDelay => currentState == ActionState.InStartDelay; /// ModyAction has started and is running public bool isRunning => currentState == ActionState.IsRunning; /// ModyAction is in the 'InCooldown' state and cannot be triggered again during this time public bool inCooldown => currentState == ActionState.InCooldown; /// ModyAction has started running and is either in 'IsStartDelay', 'IsRunning' or 'InCooldown' state public bool isActive => inStartDelay || isRunning || inCooldown; #endregion #region Action Enabled /// If TRUE the ModyAction can run, FALSE otherwise [SerializeField] private bool ActionEnabled; /// If TRUE the ModyAction can run, FALSE otherwise public bool enabled { get => ActionEnabled; set { if (value) { OnActivate(); ActionEnabled = true; currentState = ActionState.Idle; onStateChanged?.Invoke(TriggeredActionState.Idle); return; } OnDeactivate(); ActionEnabled = false; currentState = ActionState.Disabled; onStateChanged?.Invoke(TriggeredActionState.Disabled); } } #endregion #region Action Start Delay /// Time interval before the ModyAction executes its task, after it started running [SerializeField] private float ActionStartDelay; /// Time interval before the ModyAction executes its task, after it started running public float startDelay { get => ActionStartDelay > 0 ? ActionStartDelay : 0; internal set => ActionStartDelay = value > 0 ? value : 0; } #endregion #region Action Duration /// /// Running time from start to finish Does not include StartDelay. /// At 0 (zero) the ModyAction's task happens instantly. /// [SerializeField] private float ActionDuration; /// /// Running time from start to finish Does not include StartDelay. /// At 0 (zero) the ModyAction's task happens instantly. /// public float duration { get => ActionDuration > 0 ? ActionDuration : 0; internal set => ActionDuration = value > 0 ? value : 0; } #endregion #region Action Total Duration /// /// Total running time for the ModyAction Cooldown is not taken into account. /// StartDelay + Duration /// public float totalDuration => startDelay + duration; #endregion #region Action Cooldown /// Cooldown time after the ModyAction ran. During this time, the ModyAction cannot Start running again [SerializeField] private float ActionCooldown; /// Cooldown time after the ModyAction ran. During this time, the ModyAction cannot Start running again public float cooldown { get => ActionCooldown > 0 ? ActionCooldown : 0; internal set => ActionCooldown = value > 0 ? value : 0; } #endregion #region Action Timescale Independent /// /// Determine if the ModyAction's timers will be affected by the application's timescale /// Timescale is the scale at which time passes /// Timescale.Independent - (Realtime) Not affected by the application's timescale value /// Timescale.Dependent - (Application Time) Affected by the application's timescale value /// [SerializeField] private Timescale ActionTimescale; /// /// Determine if the ModyAction's timers will be affected by the application's timescale /// Timescale is the scale at which time passes /// TRUE - Timescale.Independent - (Realtime) Not affected by the application's timescale value /// FALSE - Timescale.Dependent - (Application Time) Affected by the application's timescale value /// public bool isTimescaleIndependent { get => ActionTimescale == Timescale.Independent; internal set => ActionTimescale = value ? Timescale.Independent : Timescale.Dependent; } #endregion #region Action OnStart Stop Other Actions /// Stop for all other ModyActions, on the Module (MonoBehaviour), when this ModyAction starts running [SerializeField] private bool ActionOnStartStopOtherActions; /// Stop for all other ModyActions, on this Module (MonoBehaviour), when this ModyAction starts running public bool onStartStopOtherActions { get => ActionOnStartStopOtherActions; internal set => ActionOnStartStopOtherActions = value; } #endregion #region Action OnStart Callback /// Events triggered when this ModyAction starts running [SerializeField] private ModyEvent OnStartEvents; /// Events triggered when this ModyAction starts running public ModyEvent onStartEvents => OnStartEvents; #endregion #region Action OnFinish Callback /// Events triggered when this ModyAction finished running [SerializeField] private ModyEvent OnFinishEvents; /// Events triggered when this ModyAction finished running public ModyEvent onFinishEvents => OnFinishEvents; #endregion #region Coroutines private Coroutine m_RunCoroutine; private Coroutine m_CooldownCoroutine; #endregion #region TriggeredActionState /// ModyAction state used by the state indicators public enum TriggeredActionState { /// Disabled state Disabled, /// Idle state Idle, /// StartDelay state StartDelay, /// OnStart state OnStart, /// Run state Run, /// OnFinish state OnFinish, /// Cooldown state Cooldown } /// UnityAction triggered every time the the ModyAction changes its state public UnityAction onStateChanged { get; set; } #endregion private bool m_BehaviourIsModule; private ModyModule m_Module; /// Flag used to mark the this ModyAction has a value public bool HasValue; /// The type of value this ModyAction has public Type ValueType; /// Flag used to ignore signal values when triggering this action via a Signal public bool IgnoreSignalValue; /// Flag used to set this ModyAction to react to any signal public bool ReactToAnySignal; //ToDo - add IgnoreSignalValue to editor options //ToDo - add ReactToAnySignal to editor options protected ModyAction(MonoBehaviour behaviour, string actionName) { actionBehaviourReference = behaviour; ActionName = actionName.RemoveWhitespaces().RemoveAllSpecialCharacters(); currentState = ActionState.Disabled; ActionEnabled = false; ActionStartDelay = 0; ActionDuration = 0; ActionCooldown = 0; ActionTimescale = Timescale.Independent; ActionOnStartStopOtherActions = true; OnStartEvents = new ModyEvent("OnStart"); OnFinishEvents = new ModyEvent("OnFinish"); HasValue = false; ValueType = null; IgnoreSignalValue = true; ReactToAnySignal = true; } protected override void OnSignal(Signal signal) => StartRunning(signal); /// /// OnActivate should be called before this ModyAction is used to perform its initial setup. /// It is called automatically when the ModyAction's Enabled state changes to TRUE. /// This method should be called in the OnEnable method of the controlling MonoBehaviour (Module). /// public virtual void OnActivate() { if (!enabled) return; Validate(); ConnectReceivers(); StopRunning(); StopCooldown(); currentState = ActionState.Idle; onStateChanged?.Invoke(TriggeredActionState.Idle); } /// /// OnDeactivate should be called to clean up the ModyAction after it has been used / activated. /// It is called automatically when the ModyAction's Enabled state changes to FALSE. /// This method should be called in the OnDisable method of the controlling MonoBehaviour (Module). /// public virtual void OnDeactivate() { DisconnectReceivers(); StopRunning(); StopCooldown(); } /// Validate the this ModyAction's settings public virtual void Validate() => UpdateSignalReceivers(); /// /// Start running this ModyAction /// The ModyAction needs to have Enabled set to TRUE for this method to work. /// If the ModyAction is in the 'InCooldown' state, this method will NOT work. /// public void StartRunning() => StartRunning(null, false); /// /// Start running this ModyAction. /// The ModyAction needs to have Enabled set to TRUE for this method to work. /// /// Ignore cooldown if the ModyAction is in the 'InCooldown' state public void StartRunning(bool ignoreCooldown) => StartRunning(null, ignoreCooldown); /// /// Start running this ModyAction. /// The ModyAction needs to have Enabled set to TRUE for this method to work. /// If the Action is in the 'InCooldown' state, this method will NOT work. /// /// Signal used to pass data public void StartRunning(Signal signal) => StartRunning(signal, false); /// /// Start running this ModyAction. /// The ModyAction needs to have Enabled set to TRUE for this method to work. /// /// Signal used to pass data /// Ignore cooldown if the ModyAction is in the 'InCooldown' state /// Execute method even if the ModyAction is not enabled public void StartRunning(Signal signal, bool ignoreCooldown, bool forced = false) { if (!forced && !enabled) return; if (onStartStopOtherActions) { StopAllOtherActions(); } if (currentState == ActionState.InCooldown) { if (!ignoreCooldown) { return; } StopCooldown(); } if (currentState == ActionState.IsRunning) { StopRunning(); } if (totalDuration == 0) { onStartEvents?.Execute(); onStateChanged?.Invoke(TriggeredActionState.OnStart); currentState = ActionState.IsRunning; onStateChanged?.Invoke(TriggeredActionState.Run); Run(signal); FinishRunning(); return; } m_RunCoroutine = actionBehaviourReference.StartCoroutine(ExecuteRun(signal)); } /// /// Executes the run cycle as follows: /// >> (time interval) StartDelay /// >> (custom method) Run /// >> (time interval) Duration /// >> (method) FinishRunning /// /// Signal used to pass data protected IEnumerator ExecuteRun(Signal signal) { if (ActionStartDelay > 0) { currentState = ActionState.InStartDelay; onStateChanged?.Invoke(TriggeredActionState.StartDelay); if (isTimescaleIndependent) { yield return new WaitForSecondsRealtime(startDelay); } else { yield return new WaitForSeconds(startDelay); } } onStartEvents?.Execute(); onStateChanged?.Invoke(TriggeredActionState.OnStart); currentState = ActionState.IsRunning; onStateChanged?.Invoke(TriggeredActionState.Run); Run(signal); if (duration > 0) { if (isTimescaleIndependent) { yield return new WaitForSecondsRealtime(duration); } else { yield return new WaitForSeconds(duration); } } FinishRunning(); } /// /// Stops running this ModyAction. /// The ModyAction needs to be in the 'IsRunning' state for this method to work. /// If the ModyAction is in the 'InCooldown' state, this method does NOT reset or stop the cooldown timer. /// This method does NOT execute the Finisher. /// public void StopRunning() { if (m_RunCoroutine != null) { actionBehaviourReference.StopCoroutine(m_RunCoroutine); m_RunCoroutine = null; } switch (currentState) { case ActionState.Disabled: case ActionState.InCooldown: return; default: currentState = ActionState.Idle; // onStateChanged?.Invoke(TriggeredActionState.Idle); break; } } /// /// Start the ModyAction's cooldown timer, that makes it unable to start running again until the timer finishes. /// The ModyAction needs to have Enabled set to TRUE for this method to work. /// If the ModyAction is in the 'IsRunning' state, this method will also Stop the Action from running. /// If the ModyAction is in the 'Disabled' state, this method will NOT do anything. /// If the ModyAction is in the 'InCooldown' state, this method will restart the cooldown timer. /// public void StartCooldown() { if (!enabled) return; if (currentState == ActionState.IsRunning) { StopRunning(); } if (currentState == ActionState.Disabled) { return; } if (cooldown == 0) { currentState = ActionState.Idle; onStateChanged?.Invoke(TriggeredActionState.Idle); return; } if (currentState == ActionState.InCooldown) { StopCooldown(); } m_CooldownCoroutine = actionBehaviourReference.StartCoroutine(ExecuteCooldown()); } /// /// Executes the cooldown cycle as follows: /// >> (time interval) Cooldown /// >> (method) StopCooldown /// protected IEnumerator ExecuteCooldown() { currentState = ActionState.InCooldown; onStateChanged?.Invoke(TriggeredActionState.Cooldown); if (isTimescaleIndependent) { yield return new WaitForSecondsRealtime(cooldown); } else { yield return new WaitForSeconds(cooldown); } StopCooldown(); } /// /// Stop the ModyAction's cooldown timer and set it ready to start running again. /// public void StopCooldown() { if (m_CooldownCoroutine != null) { actionBehaviourReference.StopCoroutine(m_CooldownCoroutine); m_CooldownCoroutine = null; } currentState = ActionState.Idle; onStateChanged?.Invoke(TriggeredActionState.Idle); } /// /// Finish running the ModyAction by doing the following: /// >> (method) Execute Finisher (if enabled) /// >> (method) StartCooldown /// The ModyAction needs to have Enabled set to TRUE for this method to work. /// public void FinishRunning() { if (!isActive && !enabled) return; onFinishEvents?.Execute(); onStateChanged?.Invoke(TriggeredActionState.OnFinish); if (cooldown > 0) { StartCooldown(); return; } currentState = ActionState.Idle; onStateChanged?.Invoke(TriggeredActionState.Idle); } /// /// Execute the given method The available options are as follows: /// 1 StartRunning /// 2 StopRunning /// 3 FinishRunning /// /// Method to call /// Ignore cooldown if the ModyAction is in the 'InCooldown' state (only for the StartRunning option) /// Execute method even if the ModyAction is not enabled public void ExecuteMethod(RunAction method, bool ignoreCooldown = false, bool forced = false) { switch (method) { case RunAction.Start: StartRunning(null, ignoreCooldown, forced); break; case RunAction.Stop: StopRunning(); break; case RunAction.Finish: FinishRunning(); break; default: throw new ArgumentOutOfRangeException(nameof(method), method, null); } } /// /// Stop all the other ModyAction that are running on the controlling MonoBehaviour (Module) /// public void StopAllOtherActions() { if (!enabled) return; ((IHaveActions)actionBehaviourReference)?.StopAllActions(); } /// /// Run the code that executes the task this ModyAction was designed to do. /// This method is called after the ModyAction has started running and the StartDelay time interval has passed. /// This is where the code that 'does things' is added in derived classes. /// /// Signal used to pass data protected abstract void Run(Signal signal); /// Try to set a value to the MetaSignal. Returns TRUE if the operation was successful /// Value public abstract bool SetValue(object objectValue); /// Try to set a value to the MetaSignal. Returns TRUE if the operation was successful /// Value /// Check if the passed object type is the same as the action's ValueType internal abstract bool SetValue(object objectValue, bool restrictValueType); /// Update all the signal receivers references private void UpdateSignalReceivers() { foreach (SignalReceiver receiver in SignalsReceivers) { switch (receiver.streamConnection) { case StreamConnection.None: receiver.SetSignalSource(actionBehaviourReference.gameObject); break; case StreamConnection.ProviderId: receiver.SetSignalSource ( receiver.providerId.Type == ProviderType.Local ? actionBehaviourReference.gameObject : Signals.Signals.instance.gameObject ); break; case StreamConnection.ProviderReference: if (receiver.providerReference != null) receiver.SetSignalSource(receiver.providerReference.gameObject); break; case StreamConnection.StreamId: break; default: throw new ArgumentOutOfRangeException(); } } } } /// /// Extension methods for ModyAction /// public static class ModyActionExtensions { /// Set the ModyAction's Enabled state /// Target ModyAction /// Is enabled public static T SetEnabled(this T target, bool value) where T : ModyAction { target.enabled = value; return target; } /// Set the MonoBehaviour that controls the ModyAction /// Target ModyAction /// The MonoBehaviour (Module) that this ModyAction belongs to. Needs to implement the IHaveActions interface public static T SetBehaviour(this T target, MonoBehaviour behaviour) where T : ModyAction { target.actionBehaviourReference = behaviour; return target; } /// Set if when the ModyAction starts running, all the other Actions on the Module (MonoBehaviour controller) are stopped /// Target ModyAction /// Stop other ModyAction when this Action starts running public static T SetStopAllActionsOnStart(this T target, bool value) where T : ModyAction { target.onStartStopOtherActions = value; return target; } /// Set the ModyAction's StartDelay /// Target ModyAction /// Time interval before the ModyAction executes its task, after it started running public static T SetStartDelay(this T target, float value) where T : ModyAction { target.startDelay = value; return target; } /// Set the ModyAction's Duration /// Target ModyAction /// Running time from start to finish Does not include StartDelay At 0 (zero) the ModyAction's task happens instantly public static T SetDuration(this T target, float value) where T : ModyAction { target.duration = value; return target; } /// Set the ModyAction's Cooldown /// Target ModyAction /// Cooldown time after the ModyAction ran During this time, the ModyAction cannot Start running again public static T SetCooldown(this T target, float value) where T : ModyAction { target.cooldown = value; return target; } /// Set how TimeScale influences the ModyAction's timers /// Target ModyAction /// /// Determine if the ModyAction's timers will be Timescale independent and thus not affected by the scale at which time passes /// TRUE - Timescale independent /// FALSE - Timescale dependent (affected by Time settings) /// public static T SetTimescaleIndependent(this T target, bool value) where T : ModyAction { target.isTimescaleIndependent = value; return target; } } }