step-timeline/Scripts/StepController.cs

388 lines
14 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System;
using UnityEngine;
using UnityEngine.Events;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Parent component for a collection of Steps and StepGroups, which are triggered externally (i.e. by level script) and play individually, rather than as part of a full timeline
/// </summary>
public class StepController : MonoBehaviour
{
[Title("Controller")]
[SerializeField] bool autoInit = true;
[Title("Events")]
[SerializeField] UnityEvent onParentStepPlayed;
[SerializeField] UnityEvent onActiveStepComplete;
// Private Variables
List<Step> steps = new List<Step>();
List<StepCmd> activePausableCommands = new List<StepCmd>();
int activeStepIndex;
bool init;
// Events
public event Action onStepCompleted;
// Properties
public Step activeStep { get; private set; }
public bool activeStepIsGroup { get; private set; }
public bool activeStepHasVariants { get; private set; }
public bool isPaused { get; private set; }
public bool isPlaying { get; private set; }
/// <summary>
/// Initialise steps ready to play on Start
/// </summary>
void Start()
{
if (autoInit)
Init();
}
/// <summary>
/// Initialise step controller ready for play
/// </summary>
public void Init()
{
// Avoid duplicated initialization
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 init 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 a set parent step
/// </summary>
/// /// <param name="_playStep">Step to play</param>
/// /// <param name="_overrideIfAlreadyPlaying">If another step is already playing, should it be cancelled and this one play instead? Defaults to false.</param>
public void Play(Step _playStep, bool _overrideIfAlreadyPlaying = false)
{
if (!init)
Init();
Step stepPlaying = null;
// TODO: Check the logic on this bit. All seems a bit odd to not distinguish between a step playing on a timeline as compared to the current controller (or another controller) when simply skipping
// TODO Lock for a step playing on a different controller?
if (StepTimelineManager.activeStepTimeline)
{
if (StepTimelineManager.activeStepTimeline.activeStepIsGroup)
{
StepGroup activeGroup = StepTimelineManager.activeStepTimeline.activeStep as StepGroup;
stepPlaying = activeGroup.activeChildStep;
}
else
{
stepPlaying = StepTimelineManager.activeStepTimeline.activeStep;
}
}
if (stepPlaying)
{
// TODO: Check this with Alejandro - if the stepPlaying is in a timeline, simply skipping to the next step in the timeline, at the same time as we're about to play a potentially conflicting step on the controller, is surely unintended?
// ! If the step playing is on a timeline (rather than controller) should it not simply be interrupted (timeline left halted where it is until we return to it) rather skipping to the next step in the timeline.. at the same time we're playing a specific step in a controller?
if (stepPlaying.CanBeInterrupted)
stepPlaying.Skip();
else if (stepPlaying.isActive && !stepPlaying.waitingForExternalTrigger)
// If the current step can't be interrupted and is still playing then do nothing
return;
}
// If we're already playing another step, should we cancel and jump to this one instead?
if (isPlaying)
{
if (!_overrideIfAlreadyPlaying)
{
// Don't try and start playing the timeline if it's already playing
Debug.Log("StepController was asked to play a step, but returned as another step was already playing.");
return;
}
else
{
// Otherwise, before we play from this step, end the current step, if one is active
if (activeStep)
activeStep.ForceEnd();
}
}
isPlaying = true;
StepTimelineManager.activeController = this;
// Set initial step index
activeStepIndex = steps.FindIndex(a => a == _playStep);
// Play the requested step
SetActiveStep(activeStepIndex);
activeStep.Play();
// Invoke event
onParentStepPlayed.Invoke();
}
/// <summary>
/// Internal only function to set activeStep and associated vars
/// </summary>
/// <param name="_newActiveStepIndex"></param>
void SetActiveStep(int _newActiveStepIndex)
{
activeStep = steps[_newActiveStepIndex];
activeStepIsGroup = (activeStep is StepGroup);
activeStepHasVariants = (activeStep is StepSwitch);
}
/// <summary>
/// Called when a child step has completed, stop operation and wait for a new instruction to play a specific step
/// </summary>
public void StepCompleted()
{
isPlaying = false;
activeStep = null;
activeStepIndex = 0;
activeStepIsGroup = false;
activeStepHasVariants = false;
onActiveStepComplete.Invoke();
// Invoke step completed event
onStepCompleted?.Invoke();
// If GameTimeline thinks this is the currently active timeline, set null now controller finished the requested step
if (StepTimelineManager.activeController == this)
StepTimelineManager.activeController = null;
}
/// <summary>
/// Called when we want the timeline to resume actions and avoid any switch or reminder from continue playing in the background
/// </summary>
public void ForceEnd()
{
if (activeStep)
activeStep.ForceEnd();
isPlaying = false;
activeStep = null;
activeStepIndex = 0;
activeStepIsGroup = false;
activeStepHasVariants = false;
// If GameTimeline thinks this is the currently active timeline, set null now controller finished the requested step
if (StepTimelineManager.activeController == this)
StepTimelineManager.activeController = 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 Controller")]
[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*";
}
[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
}
}