479 lines
17 KiB
C#
479 lines
17 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using TriInspector;
|
|
|
|
namespace R0bbie.Timeline
|
|
{
|
|
/// <summary>
|
|
/// Plays through timeline steps in order, in turn triggering commands attached to each step
|
|
/// </summary>
|
|
public class StepTimeline : MonoBehaviour
|
|
{
|
|
#region Inspector exposed vars
|
|
[Title("Timeline")]
|
|
[SerializeField] bool playOnStart;
|
|
|
|
[Title("Events")]
|
|
[SerializeField] UnityEvent onTimelinePlayEvent;
|
|
[SerializeField] UnityEvent onTimelineCompleteEvent;
|
|
#endregion
|
|
|
|
#region Private Variables
|
|
List<Step> steps = new List<Step>();
|
|
|
|
List<StepCmd> activePausableCommands = new List<StepCmd>();
|
|
|
|
int activeStepIndex;
|
|
|
|
bool init;
|
|
#endregion
|
|
|
|
#region Constants
|
|
public const float RepeatOnResumeDelay = 30.0f;
|
|
#endregion
|
|
|
|
#region Properties
|
|
public Step activeStep { get; private set; }
|
|
public bool activeStepIsGroup { get; private set; }
|
|
public bool activeStepHasVariants { get; private set; }
|
|
|
|
public bool forwarding { get; private set; }
|
|
public bool isPaused { get; private set; }
|
|
public bool isPlaying { get; private set; }
|
|
public bool isComplete { get; private set; }
|
|
#endregion
|
|
|
|
#region Public subscribable events
|
|
public event Action onStepContinued;
|
|
#endregion
|
|
|
|
|
|
/// <summary>
|
|
/// Unity default Start function, called the first time the script is activated. Used here to check if timeline should auto-play or just be initialised ready for play
|
|
/// </summary>
|
|
void Start()
|
|
{
|
|
// If playOnStart then start playing first step in timeline, otherwise just initialise everything ready for play, and wait for Play to be called externally
|
|
if (playOnStart)
|
|
Play();
|
|
else
|
|
Init();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Initialise timeline ready for play
|
|
/// </summary>
|
|
void Init()
|
|
{
|
|
// Avoid duplicated initialisation
|
|
if (init)
|
|
return;
|
|
|
|
// Get refs to all child steps (only 1 level deep to avoid getting children of StepGroups and StepSwitches etc, as these will initialise their own children)
|
|
foreach (Transform t in transform)
|
|
{
|
|
if (t.GetComponent<Step>())
|
|
{
|
|
steps.Add(t.GetComponent<Step>());
|
|
}
|
|
}
|
|
|
|
// Initialise Steps (and StepGroups and StepSwitches)
|
|
foreach (var step in steps)
|
|
{
|
|
step.Init(this);
|
|
}
|
|
|
|
init = true;
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Play the timeline
|
|
/// </summary>
|
|
public void Play()
|
|
{
|
|
if (!init)
|
|
Init();
|
|
|
|
// Don't try and start playing the timeline if it's already playing
|
|
if (isPlaying)
|
|
return;
|
|
|
|
isPlaying = true;
|
|
isComplete = false;
|
|
|
|
// Invoke timeline play event
|
|
onTimelinePlayEvent?.Invoke();
|
|
|
|
StepTimelineManager.activeStepTimeline = this;
|
|
|
|
// Set initial step index to zero by default
|
|
activeStepIndex = 0;
|
|
|
|
// Play the first step
|
|
SetActiveStep(activeStepIndex);
|
|
activeStep.Play();
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Play the timeline from a set step
|
|
/// </summary>
|
|
public void Play(Step _playFromStep, bool _skipToIfPlaying = true)
|
|
{
|
|
if (!init)
|
|
Init();
|
|
|
|
// Check if timeline is already playing, and if we need to take any action first
|
|
if (isPlaying)
|
|
{
|
|
if (!_skipToIfPlaying)
|
|
{
|
|
// Don't try and start playing the timeline if it's already playing
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, before we play from this step, end the current step, if one is active
|
|
if (activeStep)
|
|
activeStep.ForceEnd();
|
|
|
|
// TODO: Possibly don't allow if step requested is earlier than the active step?
|
|
}
|
|
}
|
|
|
|
isPlaying = true;
|
|
isComplete = false;
|
|
|
|
StepTimelineManager.activeStepTimeline = this;
|
|
|
|
// Invoke timeline play event
|
|
onTimelinePlayEvent?.Invoke();
|
|
|
|
// Play the requested step
|
|
GoToStepAndPlay(_playFromStep);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Play the next step in timeline. Called by Continue function on Step, which should ensure the previously active step is tidied up and set inactive before playing asking the timeline to play the next one
|
|
/// </summary>
|
|
public void PlayNextStep()
|
|
{
|
|
// If the controller is playing force the step to end as the timeline has been resumed and has priority on the level
|
|
if (StepTimelineManager.activeController)
|
|
{
|
|
StepTimelineManager.activeController.ForceEnd();
|
|
}
|
|
|
|
activeStepIndex++;
|
|
|
|
// Check we've not reached the end of the timeline, before playing the next step (if there is one)
|
|
if (activeStepIndex >= steps.Count)
|
|
{
|
|
TimelineCompleted();
|
|
}
|
|
else
|
|
{
|
|
SetActiveStep(activeStepIndex);
|
|
activeStep.Play();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Internal only function to set activeStep and associated vars
|
|
/// </summary>
|
|
/// <param name="_newActiveStepIndex">New active step index on the main timeline</param>
|
|
void SetActiveStep(int _newActiveStepIndex)
|
|
{
|
|
activeStep = steps[_newActiveStepIndex];
|
|
activeStepIsGroup = (activeStep is StepGroup);
|
|
activeStepHasVariants = (activeStep is StepSwitch);
|
|
}
|
|
|
|
|
|
public void StepContinuedEvent()
|
|
{
|
|
// Invoke step continued event
|
|
onStepContinued?.Invoke();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Play a specific step which may not be the "next" one (may mean replaying an already played step or jumping to a future one), and update activeStepIndex to the desired one
|
|
/// </summary>
|
|
/// <param name="_step">Reference to the Step the timeline should now play</param>
|
|
public void GoToStepAndPlay(Step _step)
|
|
{
|
|
// Make absolutely sure previously active step has been set inactive before setting the new active one and playing
|
|
if (activeStep && activeStep.isActive)
|
|
activeStep.ForceEnd();
|
|
|
|
// Make sure requested step isn't a child of a step switch - only step switches should play a sub-option
|
|
if (_step.parentStepSwitch)
|
|
{
|
|
Debug.LogError("Can't 'GoTo' a step which is a child of a step switch. Use the step switch to switch between options, or move the step outwith the switch!");
|
|
return;
|
|
}
|
|
|
|
// If requested step has a parent step group, we want to activate the step group first, then jump to it
|
|
if (_step.parentStepGroup)
|
|
{
|
|
// Tell the direct parent step group to jump to the step and play it (will handle any other intermediary step groups between the timeline root and the step there)
|
|
_step.parentStepGroup.GoToStepAndPlay(_step);
|
|
|
|
// GoToStepAndPlay will in turn recurse upwards and find the highest level group before the root timeline, and call back into Timeline via BLAAA to set it active
|
|
}
|
|
else // Otherwise this is just a normal step with no parent step group, play it directly
|
|
{
|
|
// Set the index to the newly requested step
|
|
activeStepIndex = steps.IndexOf(_step);
|
|
// Set new active step, then play it
|
|
SetActiveStep(activeStepIndex);
|
|
activeStep.Play();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Called via a child step group or switch when a deeper child step group has been activated via GoToStep
|
|
/// </summary>
|
|
public void SetSpecifiedHighestStepActive(Step _step)
|
|
{
|
|
// Set the index in the timeline to that of new step's highest parent group
|
|
activeStepIndex = steps.IndexOf(_step);
|
|
SetActiveStep(activeStepIndex);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Called when the timeline is completed
|
|
/// </summary>
|
|
public void TimelineCompleted()
|
|
{
|
|
isComplete = true;
|
|
isPlaying = false;
|
|
onTimelineCompleteEvent?.Invoke();
|
|
|
|
// If GameTimeline thinks this is the currently active timeline, set null now timeline is completed
|
|
if (StepTimelineManager.activeStepTimeline == this)
|
|
StepTimelineManager.activeStepTimeline = null;
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Pause currently active commands
|
|
/// </summary>
|
|
public void Pause()
|
|
{
|
|
isPaused = true;
|
|
|
|
// Tell active step to pause (pauses timers)
|
|
if (activeStep)
|
|
activeStep.Pause();
|
|
|
|
// Pause all currently active commands which are pausable
|
|
foreach (StepCmd commandToPause in activePausableCommands)
|
|
{
|
|
// On the (theoretically impossible) chance a non-pausable command made it onto the list, don't try and pause it, and remove it from list
|
|
if (commandToPause is not IPausable)
|
|
{
|
|
RemoveActivePausableCommand(commandToPause);
|
|
continue;
|
|
}
|
|
|
|
// Pause command
|
|
(commandToPause as IPausable).Pause();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Resume paused commands
|
|
/// </summary>
|
|
/// <param name="_elapsedTime">Amount of time in seconds the game was paused i.e. player was in The Fold</param>
|
|
public void Resume(float _elapsedTime)
|
|
{
|
|
isPaused = false;
|
|
|
|
// Tell active step to resume (resumes any active timers)
|
|
if (activeStep)
|
|
activeStep.Resume(_elapsedTime);
|
|
|
|
// Resume all currently active commands which are pausable
|
|
foreach (StepCmd commandToPause in activePausableCommands)
|
|
{
|
|
// On the (theoretically impossible) chance a non-pausable command made it onto the list, don't try and pause it, and remove it from list
|
|
if (commandToPause is not IPausable)
|
|
{
|
|
RemoveActivePausableCommand(commandToPause);
|
|
continue;
|
|
}
|
|
|
|
// Resume command
|
|
(commandToPause as IPausable).Resume(_elapsedTime);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// When a command is activated which is pausable, add it to the list
|
|
/// </summary>
|
|
/// <param name="_commandToAddToList"></param>
|
|
public void AddActivePausableCommand(StepCmd _commandToAddToList)
|
|
{
|
|
// Ensure IPausable interface is present
|
|
if (_commandToAddToList is not IPausable)
|
|
return;
|
|
|
|
// Make sure command isn't already in list, if so don't add again
|
|
if (activePausableCommands.FirstOrDefault(a => a == _commandToAddToList) != null)
|
|
return;
|
|
|
|
// If this command implements ISingleInstance make sure there are no existing ISingleInstances in the active list (there can only be one active at a time..)
|
|
if (_commandToAddToList is ISingleInstance)
|
|
{
|
|
// remove all existing ISingleInstance from list before adding another. iterate through all current pausable commands backwards to check
|
|
for (int i = activePausableCommands.Count - 1; i >= 0; i--)
|
|
{
|
|
if (activePausableCommands[i] is ISingleInstance)
|
|
activePausableCommands.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
// Add to list
|
|
activePausableCommands.Add(_commandToAddToList);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// When a pausable command is deactivated, remove it from list
|
|
/// </summary>
|
|
/// <param name="_commandToRemoveToList"></param>
|
|
public void RemoveActivePausableCommand(StepCmd _commandToRemoveToList)
|
|
{
|
|
// Check command is actually in the list before doing anything
|
|
if (activePausableCommands.FirstOrDefault(i => i == _commandToRemoveToList) == null)
|
|
return;
|
|
|
|
// Remove from list
|
|
activePausableCommands.RemoveAll(r => r == _commandToRemoveToList);
|
|
}
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
/// EDITOR FUNCTIONS
|
|
|
|
[Title("Add to Timeline")]
|
|
[Button("Add Step")]
|
|
private void Editor_AddStep()
|
|
{
|
|
// Create new GameObject, and reparent it as a child of the Timeline parent object
|
|
GameObject stepGo = new GameObject();
|
|
stepGo.transform.parent = transform;
|
|
|
|
// Add Step component to this new GameObject
|
|
Step step = stepGo.AddComponent<Step>();
|
|
|
|
// Rename object
|
|
step.gameObject.name = "#" + (step.transform.GetSiblingIndex() + 1).ToString("00") + " - *ADD STEP NAME*";
|
|
|
|
}
|
|
|
|
|
|
[Button("Add Step Group")]
|
|
private void Editor_AddStepGroup()
|
|
{
|
|
// Create new GameObject, and reparent it as a child of the Timeline parent object
|
|
GameObject stepGroupGo = new GameObject();
|
|
stepGroupGo.transform.parent = transform;
|
|
|
|
// Add StepGroup component to this new GameObject
|
|
StepGroup step = stepGroupGo.AddComponent<StepGroup>();
|
|
|
|
// Rename object
|
|
step.gameObject.name = "#" + (step.transform.GetSiblingIndex() + 1).ToString("00") + " - GROUP - *ADD STEP NAME*";
|
|
|
|
}
|
|
|
|
|
|
[Button("Add Step Switch")]
|
|
private void Editor_AddStepSwitch()
|
|
{
|
|
// Create new GameObject, and reparent it as a child of the Timeline parent object
|
|
GameObject stepSwitchGo = new GameObject();
|
|
stepSwitchGo.transform.parent = transform;
|
|
|
|
// Player must add the specific StepSwitch type they like, remind them
|
|
Debug.Log("Empty StepSwitch object created, now remember to add the specific StepSwitch type you want to it.");
|
|
|
|
// Rename object
|
|
stepSwitchGo.gameObject.name = "#" + (stepSwitchGo.transform.GetSiblingIndex() + 1).ToString("00") + " - SWITCH - *ADD DESIRED SWITCH COMPONENT*";
|
|
}
|
|
|
|
|
|
[Button("Add Step Looper")]
|
|
private void Editor_AddStepLooper()
|
|
{
|
|
// Create new GameObject, and reparent it as a child of the Timeline parent object
|
|
GameObject stepLooperGo = new GameObject();
|
|
stepLooperGo.transform.parent = transform;
|
|
|
|
// Add StepLoop component to this new GameObject
|
|
StepLooper step = stepLooperGo.AddComponent<StepLooper>();
|
|
|
|
// Rename object
|
|
stepLooperGo.gameObject.name = "#" + (step.transform.GetSiblingIndex() + 1).ToString("00") + " - LOOP - *ADD STEP NAME*";
|
|
}
|
|
|
|
|
|
[Button("Add GoTo Step")]
|
|
private void Editor_AddGoToStep()
|
|
{
|
|
// Create new GameObject, and reparent it as a child of the Timeline parent object
|
|
GameObject gotoStepGo = new GameObject();
|
|
gotoStepGo.transform.parent = transform;
|
|
|
|
// Add GoToStep component to this new GameObject
|
|
GoToStep step = gotoStepGo.AddComponent<GoToStep>();
|
|
|
|
// Rename object
|
|
gotoStepGo.gameObject.name = "#" + (step.transform.GetSiblingIndex() + 1).ToString("00") + " - GOTO - *ADD STEP NAME*";
|
|
}
|
|
|
|
|
|
[Title("Editor Functions")]
|
|
[Button("Rename All")]
|
|
private void Editor_RenameAll()
|
|
{
|
|
foreach (Transform child in transform)
|
|
{
|
|
Step childStepComponent = child.GetComponent<Step>();
|
|
|
|
if (childStepComponent)
|
|
childStepComponent.UpdateStepName();
|
|
|
|
// If group / switch also rename children
|
|
if (childStepComponent is StepGroup)
|
|
(childStepComponent as StepGroup).RenameChildren();
|
|
else if (childStepComponent is StepSwitch)
|
|
(childStepComponent as StepSwitch).RenameChildren();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
}
|
|
} |