651 lines
23 KiB
C#
651 lines
23 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();
|
|
|
|
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();
|
|
|
|
// 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;
|
|
|
|
// Watch for the trigger
|
|
externalContinueTrigger.onTrigger.AddListener(TriggerContinue);
|
|
}
|
|
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");
|
|
|
|
// Remove listener
|
|
externalContinueTrigger.onTrigger.RemoveListener(TriggerContinue);
|
|
|
|
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
|
|
|
|
}
|
|
}
|