2023-12-14 22:59:25 +00:00
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 ( ) ;
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 ( ) ;
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>
/// 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 ( ) ;
2024-02-28 18:16:58 +00:00
2023-12-14 22:59:25 +00:00
// 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 ;
2024-02-28 18:16:58 +00:00
// Watch for the trigger
externalContinueTrigger . onTrigger . AddListener ( TriggerContinue ) ;
2023-12-14 22:59:25 +00:00
}
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" ) ;
2024-02-28 18:16:58 +00:00
// Remove listener
externalContinueTrigger . onTrigger . RemoveListener ( TriggerContinue ) ;
2023-12-14 22:59:25 +00:00
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
}
}