step-timeline/Scripts/StepTimeline.cs

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 when a deeper child step group has been activated via GoToStep
/// </summary>
public void SetSpecifiedHighestStepGroupActive(StepGroup _group)
{
// Set the index in the timeline to that of new step's highest parent group
activeStepIndex = steps.IndexOf(_group);
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
}
}