step-timeline/Scripts/Steps/Step.cs

667 lines
24 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System;
using System.Linq;
using UnityEngine;
using TriInspector;
using R0bbie.Timeline.Events;
namespace R0bbie.Timeline
{
/// <summary>
/// An individual Step in the timeline, with associated commands attached to it. May also be inherited and command overridden by StepGroup or StepSwitch
/// </summary>
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<StepCmd> commands = new List<StepCmd>();
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;
/// <summary>
/// Standard Init function, called by the parent timeline
/// </summary>
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<StepCmd>().ToList();
InitializeContinueConditions();
init = true;
}
/// <summary>
/// Step Init function called by a parent StepGroup
/// </summary>
public virtual void Init(StepTimeline _stepTimeline, StepGroup _stepGroup)
{
// Setup necessary refs parent StepGroup
parentStepGroup = _stepGroup;
// Init base shared step functionality
Init(_stepTimeline);
}
/// <summary>
/// Step Init function called by a parent StepSwitch
/// </summary>
public virtual void Init(StepTimeline _stepTimeline, StepSwitch _stepSwitch)
{
// Setup necessary refs parent StepSwitch
parentStepSwitch = _stepSwitch;
// Init base shared step functionality
Init(_stepTimeline);
}
/// <summary>
/// Override Init function, called by a parent StepController if this step isn't part of a level timeline
/// </summary>
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<StepCmd>().ToList();
InitializeContinueConditions();
init = true;
}
/// <summary>
/// Step Init function called by a parent StepGroup, if a StepController is in control
/// </summary>
public virtual void Init(StepController _controller, StepGroup _stepGroup)
{
// Setup necessary refs parent StepGroup
parentStepGroup = _stepGroup;
// Init base shared step functionality
Init(_controller);
}
/// <summary>
/// Step Init function called by a parent StepSwitch, if a StepController is in control
/// </summary>
public virtual void Init(StepController _controller, StepSwitch _stepSwitch)
{
// Setup necessary refs parent StepSwitch
parentStepSwitch = _stepSwitch;
// Init base shared step functionality
Init(_controller);
}
/// <summary>
/// Initialize any reference needed for the continue condition
/// </summary>
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);
}
}
/// <summary>
/// Update loop used for tracking timers
/// </summary>
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();
}
}
}
/// <summary>
/// Play this step, triggering all commands attached to it
/// </summary>
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;
}
}
/// <summary>
/// Called to Skip over this step, mark it done without triggering any attached commands, and move onto the next step
/// </summary>
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();
}
}
}
/// <summary>
/// Called externally to force this step to end, without automatically continuing
/// </summary>
public virtual void ForceEnd()
{
Debug.Log(name + " - Forced End");
isActive = false;
foreach (var command in commands)
{
command.DeactivateEarly();
}
}
/// <summary>
/// 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.
/// </summary>
void TimedContinueGoalMet()
{
timerActive = false;
Debug.Log(name + " - Continue after delay");
Continue();
}
/// <summary>
/// If continue condition is OnCommandComplete, this function will be called once the reference command has completed its function
/// </summary>
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();
}
/// <summary>
/// If continue condition is OnExternalTrigger, this function will be called by the external trigger
/// </summary>
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();
}
/// <summary>
/// Called if a GameEvent is raised that this step is registered to
/// </summary>
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();
}
/// <summary>
/// Used to complete Step until minimum time limit has been met
/// </summary>
/// <returns></returns>
[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();
}
/// <summary>
/// End this step, and inform timeline to continue playing
/// </summary>
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();
}
}
/// <summary>
/// Used on game pause, pauses active timers etc
/// </summary>
public void Pause()
{
if (!isActive || wasCompleted)
return;
// Set paused flag
isPaused = true;
}
/// <summary>
/// Called when game unpaused
/// </summary>
/// <param name="_elapsedTime">How long step was paused for</param>
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<StepGroup>())
{
// 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<StepSwitch>())
{
// 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<StepLooper>())
{
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
}
}