using System.Collections; using System.Collections.Generic; using System; using System.Linq; using UnityEngine; using TriInspector; using R0bbie.Timeline.Events; namespace R0bbie.Timeline { /// /// An individual Step in the timeline, with associated commands attached to it. May also be inherited and command overridden by StepGroup or StepSwitch /// public class Step : MonoBehaviour, IGameEventListener, IPausable { public enum ContinueCondition { Immediate, Timed, OnCommandComplete, OnExternalTrigger, OnGameEventRaise } [Title("Step")] [SerializeField, Tooltip("Short descriptive name for this ")] public string shortName; [Title("Continue Condition")] //[HideIf("@this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep")] [ShowIf("Inspector_ShowContinueCondition")] [SerializeField] ContinueCondition continueCondition; [ShowIf(nameof(continueCondition), ContinueCondition.Timed)] [SerializeField] float stepContinueDelay; [ShowIf(nameof(continueCondition), ContinueCondition.OnCommandComplete)] [SerializeField] StepCmd continueOnCommandComplete; [ShowIf(nameof(continueCondition), ContinueCondition.OnExternalTrigger)] [SerializeField] TimelineTrigger externalContinueTrigger; [ShowIf("Inspector_HasContinueTrigger")] [SerializeField] bool continueImmediatelyIfPrevTriggered; [ShowIf(nameof(continueCondition), ContinueCondition.OnGameEventRaise)] [SerializeField] GameEvent continueOnEventRaised; [Title("Time Limits")] //[HideIf("@this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep || continueCondition == ContinueCondition.Immediate || continueCondition == ContinueCondition.Timed")] [ShowIf("Inspector_AllowTimeLimits")] [Tooltip("Force Step to not continue for a minimum time duration, even once continue condition met")] [SerializeField] float minimumTimeLimit; //[HideIf("@this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep || continueCondition == ContinueCondition.Immediate || continueCondition == ContinueCondition.Timed")] [ShowIf("Inspector_AllowTimeLimits")] [Tooltip("Force Step to continue once this time limit is up")] [SerializeField] bool setMaxTimeLimit; //[HideIf("@this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep || continueCondition == ContinueCondition.Immediate || continueCondition == ContinueCondition.Timed || !setMaxTimeLimit")] [ShowIf("Inspector_ShowMaxTimeLimit")] [Tooltip("Force Step to continue once this time limit is up")] [SerializeField] float maximumTimeLimit; [Title("Interrupt")] [HideIf(nameof(continueCondition), ContinueCondition.Immediate)] [Tooltip("If this step can be interrupted by a step outside the timeline")] [SerializeField] bool canBeInterrupted; [HideIf(nameof(continueCondition), ContinueCondition.Immediate)] [Tooltip("The step that should skip to if interrupted")] [SerializeField, ShowIf("canBeInterrupted")] Step skipToStep; // Refs to associated objects public StepTimeline attachedStepTimeline { get; protected set; } public StepController attachedController { get; protected set; } public StepGroup parentStepGroup { get; protected set; } public StepSwitch parentStepSwitch { get; protected set; } // Refs to all attached commands private List commands = new List(); bool timerActive; float timeSinceStepStarted; bool minimumTimeMet; bool conditionCompleted; // Step status vars public bool init { get; protected set; } public bool isActive { get; protected set; } public bool isPaused { get; protected set; } public bool wasSkipped { get; protected set; } public bool wasCompleted { get; protected set; } public bool fastForwarding { get; protected set; } public bool waitingForExternalTrigger { get; protected set; } public bool registeredToGameEvent { get; protected set; } // Properties public bool CanBeInterrupted { get => canBeInterrupted; } public Step SkipToStep { get => skipToStep; } // Events public event Action onStepPlayed; public event Action onStepContinued; /// /// Standard Init function, called by the parent timeline /// public virtual void Init(StepTimeline _stepTimeline) { if (init) return; // Setup necessary refs such as parent timeline attachedStepTimeline = _stepTimeline; // Get refs to all commands attached to this step commands = GetComponents().ToList(); InitializeContinueConditions(); init = true; } /// /// Step Init function called by a parent StepGroup /// public virtual void Init(StepTimeline _stepTimeline, StepGroup _stepGroup) { // Setup necessary refs parent StepGroup parentStepGroup = _stepGroup; // Init base shared step functionality Init(_stepTimeline); } /// /// Step Init function called by a parent StepSwitch /// public virtual void Init(StepTimeline _stepTimeline, StepSwitch _stepSwitch) { // Setup necessary refs parent StepSwitch parentStepSwitch = _stepSwitch; // Init base shared step functionality Init(_stepTimeline); } /// /// Override Init function, called by a parent StepController if this step isn't part of a level timeline /// public virtual void Init(StepController _controller) { if (init) return; // Setup necessary refs such as parent timeline attachedController = _controller; // Get refs to all commands attached to this step commands = GetComponents().ToList(); InitializeContinueConditions(); init = true; } /// /// Step Init function called by a parent StepGroup, if a StepController is in control /// public virtual void Init(StepController _controller, StepGroup _stepGroup) { // Setup necessary refs parent StepGroup parentStepGroup = _stepGroup; // Init base shared step functionality Init(_controller); } /// /// Step Init function called by a parent StepSwitch, if a StepController is in control /// public virtual void Init(StepController _controller, StepSwitch _stepSwitch) { // Setup necessary refs parent StepSwitch parentStepSwitch = _stepSwitch; // Init base shared step functionality Init(_controller); } /// /// Initialize any reference needed for the continue condition /// void InitializeContinueConditions() { // Only need to initialize the external trigger if (continueCondition == ContinueCondition.OnExternalTrigger) { // Assign this step to the trigger if (externalContinueTrigger != null) externalContinueTrigger.AddStep(this); else Debug.LogWarning("No external trigger assigned to step: " + name); } } /// /// Update loop used for tracking timers /// void Update() { // If step not active return if (!init || !isActive || wasCompleted) return; // If no timer active, return if (!timerActive) return; // If game / step paused, return if (isPaused) return; // Update timer timeSinceStepStarted += Time.deltaTime; // Check timer against time limit (if Timed continue condition) or minimum time limit (if set) if (continueCondition == ContinueCondition.Timed) { if (timeSinceStepStarted > stepContinueDelay) TimedContinueGoalMet(); } else if (minimumTimeLimit > 0) { if (timeSinceStepStarted > minimumTimeLimit) { minimumTimeMet = true; // Keep it separated in case the condition completes later than the min time if(conditionCompleted) Continue(); } } else if (setMaxTimeLimit && maximumTimeLimit > minimumTimeLimit) { if (timeSinceStepStarted > maximumTimeLimit) { // Reached maximum time on step, auto-continue TimedContinueGoalMet(); } } } /// /// Play this step, triggering all commands attached to it /// public virtual void Play() { Debug.Log(name + " - Started"); isActive = true; StepTimelineManager.lastPlayedStep = this; // Invoke event onStepPlayed?.Invoke(); // ACTIVATE ATTACHED COMMANDS (some command types can have unlimited instances on a step, others can only have one instance on a step) foreach (StepCmd command in commands) { command.Activate(this); } // SETUP CONTINUE CONDITION TO LOOK OUT FOR switch (continueCondition) { case ContinueCondition.Immediate: // Continue immediately, and end this step Debug.Log(name + " - Continue Immediate"); Continue(); break; case ContinueCondition.Timed: // Continue after the defined time delay timerActive = true; break; // Player input is also based on command complete, but we set the command from the accessibility settings of the game case ContinueCondition.OnCommandComplete: // If the step can be completed if the command is already completed then skip register the listener if (continueImmediatelyIfPrevTriggered && continueOnCommandComplete.completed) { Debug.Log(name + " - Command already completed"); Continue(); return; } // Subscribe to event when continueOnCommandComplete command completes, and call ContinueConditionCommandComplete continueOnCommandComplete.onCompleteCommand += ContinueConditionCommandComplete; break; case ContinueCondition.OnExternalTrigger: if (externalContinueTrigger != null) { // Option to continue immediately if trigger was previously called (for example, if player performed a trigger action early) if (continueImmediatelyIfPrevTriggered && externalContinueTrigger.triggered) { Debug.Log(name + " - External trigger already triggered"); Continue(); return; } // Otherwise, we'll wait for the external trigger to tell the step to continue.. waitingForExternalTrigger = true; } else { Debug.LogWarning("Step continue condition set to external trigger, but no trigger ref was set."); } break; case ContinueCondition.OnGameEventRaise: // Register listener registeredToGameEvent = true; continueOnEventRaised.RegisterListener(this); break; } // Check if we need to start a timer for minimumTimeLimit if (continueCondition is ContinueCondition.OnCommandComplete or ContinueCondition.OnExternalTrigger or ContinueCondition.OnGameEventRaise) { if (minimumTimeLimit > 0 || (setMaxTimeLimit && maximumTimeLimit > minimumTimeLimit)) timerActive = true; } } /// /// Called to Skip over this step, mark it done without triggering any attached commands, and move onto the next step /// public virtual void Skip() { wasSkipped = true; isActive = false; Debug.Log(name + " - Skipped"); if (parentStepGroup) { if (canBeInterrupted) parentStepGroup.GoToStepAndPlay(skipToStep); else parentStepGroup.Continue(); } else if (parentStepSwitch) { parentStepSwitch.Continue(); } else { if (attachedStepTimeline) { if (canBeInterrupted) attachedStepTimeline.GoToStepAndPlay(skipToStep); else attachedStepTimeline.PlayNextStep(); } else { attachedController.StepCompleted(); } } } /// /// Called externally to force this step to end, without automatically continuing /// public virtual void ForceEnd() { Debug.Log(name + " - Forced End"); isActive = false; foreach (var command in commands) { command.DeactivateEarly(); } } /// /// Called if continue condition is Timed and timer reaches goal. Will trigger the step to end and timeline to continue after a set time delay. /// void TimedContinueGoalMet() { timerActive = false; Debug.Log(name + " - Continue after delay"); Continue(); } /// /// If continue condition is OnCommandComplete, this function will be called once the reference command has completed its function /// public void ContinueConditionCommandComplete() { if (!isActive) return; Debug.Log(name + " - Command Completed"); continueOnCommandComplete.onCompleteCommand -= ContinueConditionCommandComplete; // Continue immediately, unless a minimum time limit was set that we've not met yet if (minimumTimeLimit > 0 && !minimumTimeMet) conditionCompleted = true; else Continue(); } /// /// If continue condition is OnExternalTrigger, this function will be called by the external trigger /// public virtual void TriggerContinue(TimelineTrigger _trigger) { if (!isActive || !waitingForExternalTrigger) return; // Only accept continue command if it was received from the TimelineTrigger specified if (externalContinueTrigger != _trigger) return; Debug.Log(name + " - Continue from trigger"); waitingForExternalTrigger = false; // Continue immediately, unless a minimum time limit was set that we've not met yet if (minimumTimeLimit > 0 && !minimumTimeMet) conditionCompleted = true; else Continue(); } /// /// Called if a GameEvent is raised that this step is registered to /// public void OnEventRaised() { // Unregister from event continueOnEventRaised.UnregisterListener(this); registeredToGameEvent = false; // If step is now inactive, then don't do anything if (!isActive) return; Debug.Log(name + " - Continue on event Raised"); // Continue condition met.. // Continue immediately, unless a minimum time limit was set that we've not met yet if (minimumTimeLimit > 0 && !minimumTimeMet) conditionCompleted = true; else Continue(); } /// /// Used to complete Step until minimum time limit has been met /// /// [Obsolete("Coroutines are stopped when the object is disabled, blocking progress if going to the fold while running")] IEnumerator WaitForMinimumTimeLimit() { // Wait until minimum time limit met while (!minimumTimeMet) yield return null; // Now continue Continue(); } /// /// End this step, and inform timeline to continue playing /// public virtual void Continue() { isActive = false; wasCompleted = true; // Invoke step continued event onStepContinued?.Invoke(); if (attachedStepTimeline) attachedStepTimeline.StepContinuedEvent(); if (parentStepGroup) { parentStepGroup.Continue(); } else if (parentStepSwitch) { parentStepSwitch.Continue(); } else { if (attachedStepTimeline != null) attachedStepTimeline.PlayNextStep(); else // Playing through a StepController attachedController.StepCompleted(); } } /// /// Used on game pause, pauses active timers etc /// public void Pause() { if (!isActive || wasCompleted) return; // Set paused flag isPaused = true; } /// /// Called when game unpaused /// /// How long step was paused for public void Resume(float _elapsedTime) { if (!isActive || wasCompleted || !isPaused) return; // Unset paused flag isPaused = false; } #if UNITY_EDITOR #region Editor Inspector functions public void UpdateStepName() { // Get corresponding string for the type of step this is string stepType = this switch { StepGroup => "GROUP", StepSwitch => "SWITCH", StepLooper => "LOOP", GoToStep => "GOTO", _ => "Step" }; string stepNameToDisplay = "*ADD STEP NAME*"; if (!String.IsNullOrEmpty(shortName)) stepNameToDisplay = shortName; // Check if this step is a child of a step group or switch if (transform.parent.GetComponent()) { // Get step no. of parent step int parentStepNo = transform.parent.GetSiblingIndex() + 1; // Format = #XX.xx - Step name if (stepType == "Step") name = "#" + parentStepNo.ToString("00") + "." + (transform.GetSiblingIndex() + 1).ToString("00") + " - " + stepNameToDisplay; else name = "#" + parentStepNo.ToString("00") + "." + (transform.GetSiblingIndex() + 1).ToString("00") + " - " + stepType + " - " + stepNameToDisplay; return; } else if (transform.parent.GetComponent()) { // Get step no. of parent step int parentStepNo = transform.parent.GetSiblingIndex() + 1; // Format = #XX.xx - Switch Option - Step name if (stepType == "Step") name = "#" + parentStepNo.ToString("00") + "." + (transform.GetSiblingIndex() + 1).ToString("00") + " - Switch Option - " + stepNameToDisplay; else name = "#" + parentStepNo.ToString("00") + "." + (transform.GetSiblingIndex() + 1).ToString("00") + " - Switch Option - " + stepType + " - " + stepNameToDisplay; return; } else if (transform.parent.GetComponent()) { Debug.LogWarning("A StepLooper should not have any child steps - these won't be played!"); return; } // Rename (including string type in name) // Format = #XX - TYPE - Step name if (stepType == "Step") name = "#" + (transform.GetSiblingIndex() + 1).ToString("00") + " - " + stepNameToDisplay; else name = "#" + (transform.GetSiblingIndex() + 1).ToString("00") + " - " + stepType + " - " + stepNameToDisplay; } bool Inspector_ShowContinueCondition() { return !(this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep); } bool Inspector_HasContinueTrigger() { return (continueCondition is ContinueCondition.OnExternalTrigger or ContinueCondition.OnCommandComplete); } bool Inspector_AllowTimeLimits() { return !(this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep || continueCondition == ContinueCondition.Immediate || continueCondition == ContinueCondition.Timed); } bool Inspector_ShowMaxTimeLimit() { return !(this is StepGroup || this is StepSwitch || this is StepLooper || this is GoToStep || continueCondition == ContinueCondition.Immediate || continueCondition == ContinueCondition.Timed || !setMaxTimeLimit); } #endregion #endif } }