using System.Collections.Generic; using System.Linq; using System; using UnityEngine; using UnityEngine.Events; using TriInspector; namespace R0bbie.Timeline { /// /// 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 /// public class StepController : MonoBehaviour { [Title("Controller")] [SerializeField] bool autoInit = true; [Title("Events")] [SerializeField] UnityEvent onParentStepPlayed; [SerializeField] UnityEvent onActiveStepComplete; // Private Variables List steps = new List(); List activePausableCommands = new List(); 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; } /// /// Initialise steps ready to play on Start /// void Start() { if (autoInit) Init(); } /// /// Initialise step controller ready for play /// 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()) { steps.Add(t.GetComponent()); } } // Initialise Steps (and StepGroups and StepSwitches) foreach (var step in steps) { step.Init(this); } init = true; } /// /// Play a set parent step /// /// /// Step to play /// /// If another step is already playing, should it be cancelled and this one play instead? Defaults to false. 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(); } /// /// Internal only function to set activeStep and associated vars /// /// void SetActiveStep(int _newActiveStepIndex) { activeStep = steps[_newActiveStepIndex]; activeStepIsGroup = (activeStep is StepGroup); activeStepHasVariants = (activeStep is StepSwitch); } /// /// Called when a child step has completed, stop operation and wait for a new instruction to play a specific step /// 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; } /// /// Called when we want the timeline to resume actions and avoid any switch or reminder from continue playing in the background /// 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; } /// /// Pause currently active commands /// 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(); } } /// /// Resume paused commands /// /// Amount of time in seconds the game was paused i.e. player was in The Fold 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); } } /// /// When a command is activated which is pausable, add it to the list /// /// 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); } /// /// When a pausable command is deactivated, remove it from list /// /// 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(); // 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(); // 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(); 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 } }