Initial commit of refactored StepTimeline package

This commit is contained in:
Robbie Cargill 2023-12-14 22:59:25 +00:00
commit 2189cfc787
78 changed files with 4691 additions and 0 deletions

53
.gitignore vendored Normal file
View File

@ -0,0 +1,53 @@
# Visual Studio Cache
.vs/
.vscode/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.opendb
*.sublime-*
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.unitypackage.meta
# Unity3D Generated File On Crash Reports
sysinfo.txt
# Builds
*.apk
*.unitypackage
*.ipa
# Documentation
Documentation/api/
Documentation/scripting/
_site/
/**/obj/
/**/obj.meta
Documentation/obj*
UserSettings
# OS junk
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Thumbs.db.meta
.vsconfig

19
R0bbie.Timeline.asmdef Normal file
View File

@ -0,0 +1,19 @@
{
"name": "R0bbie.Timeline",
"rootNamespace": "R0bbie.Timeline",
"references": [
"Unity.TextMeshPro",
"Unity.InputSystem",
"TriInspector",
"NaughtyAttributes.Core"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 683a8bf2a8db6154b9177e122c046565
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Timeline System
Package to allow setup of a linear timeline of steps with attached commands.
# Dependencies
- TriInspector
These plugins/packages must be in the project prior to import, or they will throw some errors in the console.

7
README.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0e45445281b15f746a15206faf2e745a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Scripts.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e5271432087219a46bb979a961ec809c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

3
Scripts/Commands.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f92b03cc00774d3a960183ae6aba004b
timeCreated: 1647379457

View File

@ -0,0 +1,78 @@
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Animation StepCmd type, used to control an animation
/// </summary>
[AddComponentMenu("Timeline/Commands/Animation (Step Command)")]
[RequireComponent(typeof(Step))]
public class AnimationCmd : StepCmd
{
[Title("Animation Options")]
[SerializeField] protected Animator animator;
[SerializeField] bool forceAnimation;
[InfoBox("If forcing the animation, the animationTrigger string should be the name on the AnimatorState(not the trigger parameter) to be able to force it")]
[SerializeField] protected string animationTrigger;
/// <summary>
/// Initialise command (called before Activate)
/// </summary>
protected override void Init()
{
init = true;
}
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// Check the necessary command values are set before continuing
if (animator == null || string.IsNullOrEmpty(animationTrigger))
{
Debug.LogWarning("Tried to activate an AnimationCmd with a null animator or trigger. Couldn't continue.");
DeactivateEarly();
return;
}
// Ensure movement resumed to normal on animator (in case paused before)
// The "Speed" float of the animator is used just to set a character idle at the start of an anim so it needs to be setup on the editor
// For the resume function we just need to resume the speed on the animator itself
animator.speed = 1f;
// Play animation
if (forceAnimation)
animator.Play(animationTrigger);
else
animator.SetTrigger(animationTrigger);
// Ensure GO attached to animator is enabled
if (!animator.isActiveAndEnabled)
animator.gameObject.SetActive(true);
}
protected override void Cleanup()
{
active = false;
}
#if UNITY_EDITOR
public Animator GetAnimator() { return animator; }
public string GetAnimatorString() { return animationTrigger; }
public bool GetForceAnimation() { return forceAnimation; }
public void SetAnimatorEditor(Animator editorAnimator)
{
animator = editorAnimator;
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9bf3c00707389af459ed4de79649f977
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace R0bbie.Timeline
{
/// <summary>
/// DeactivateCommands StepCmd type, used to deactivate a list of commands which may have been left activated from a previous step
/// </summary>
[AddComponentMenu("Timeline/Commands/Deactivate Commands (Step Command)")]
[RequireComponent(typeof(Step))]
public class DeactivateCommandsCmd : StepCmd
{
[SerializeField] private List<StepCmd> commandsToDeactivate = new List<StepCmd>();
/// <summary>
/// Initialise command (called before Activate)
/// </summary>
protected override void Init()
{
init = true;
}
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// Loop through and deactivate all desired commands
foreach (StepCmd command in commandsToDeactivate)
{
command.DeactivateEarly();
}
// Then job of this command is done, complete it and deactivate
Complete();
}
protected override void Cleanup()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c8be5dd5a090c8d4d94bfd7d55003569
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,46 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace R0bbie.Timeline
{
[AddComponentMenu("Timeline/Commands/Destroy GameObjects (Step Command)")]
[RequireComponent(typeof(Step))]
public class DestroyObjectsCmd : StepCmd
{
[SerializeField] private List<GameObject> objectsToDestroy = new List<GameObject>();
/// <summary>
/// Initialise command (called before Activate)
/// </summary>
protected override void Init()
{
init = true;
}
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// Loop through and destroy all objects
foreach (GameObject go in objectsToDestroy)
{
if (go != null)
Destroy(go);
}
// Then job of this command is done, complete it and deactivate
Complete();
}
protected override void Cleanup()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6f8c50fae92c281498c7ded14e874c3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,54 @@
using System.Collections.Generic;
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Activate GameObjects from a list
/// </summary>
[AddComponentMenu("Timeline/Commands/GameObjects Set Active (Step Command)")]
[RequireComponent(typeof(Step))]
public class GameObjectsSetActiveCmd : StepCmd
{
[Title("Set Objects Status")]
[SerializeField] List<GameObject> objectsToActivate = new List<GameObject>();
[SerializeField] List<GameObject> objectsToDeactivate = new List<GameObject>();
protected override void Init()
{
init = true;
}
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// Activate all requested GameObjects
foreach (var objectToActivate in objectsToActivate)
{
objectToActivate.SetActive(true);
}
// Deactivate all requested GameObjects
foreach (var objectToDeactivate in objectsToDeactivate)
{
objectToDeactivate.SetActive(false);
}
// Then job of this command is done, complete it and deactivate
Complete();
}
protected override void Cleanup()
{
active = false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23f22643cffb7374cbece05dc0dce17b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
namespace R0bbie.Timeline
{
/// <summary>
/// Interface to use on the commands that needs specific commands if the timeline is paused
/// </summary>
public interface IPausable
{
public void Pause();
public void Resume(float _elapsedTime);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8ddbd29540db3046a25c59e5834e204
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
namespace R0bbie.Timeline
{
public interface ISingleInstance
{
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5cb9d7a8a31607740b23f763821cdb07
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,55 @@
using TriInspector;
using UnityEngine;
using UnityEngine.Events;
namespace R0bbie.Timeline
{
/// <summary>
/// Command for simply invoking a Unity event
/// </summary>
[AddComponentMenu("Timeline/Commands/Invoke Unity Event (Step Command)")]
[RequireComponent(typeof(Step))]
public class InvokeEventCmd : StepCmd
{
// Exposed Variables
[Title("Event")]
[SerializeField] UnityEvent commandEvent;
/// <summary>
/// Initialise the command
/// </summary>
protected override void Init()
{
init = true;
}
/// <summary>
/// Do whatever this command is designed to do
/// </summary>
/// <param name="_parentStep"></param>
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// Play the event
commandEvent?.Invoke();
// Immediately complete command now event invoked
Complete();
}
/// <summary>
/// Do any cleanup stuff here, resetting command state, clearing garbage, etc. Called by parent StepCmd after Complete, should never be called within child command itself
/// </summary>
protected override void Cleanup()
{
active = false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49d6743615421ce4f89ecc78205d3836
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

140
Scripts/Commands/StepCmd.cs Normal file
View File

@ -0,0 +1,140 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Abstract class inherited by all StepCmd types
/// </summary>
public abstract class StepCmd : MonoBehaviour
{
[ShowIf("Inspector_ImplementsPausable")]
[SerializeField] protected bool dontRepeatOnPauseResume;
public event Action onCompleteCommand;
protected Step parentStep;
public bool init { get; protected set; }
public bool active { get; protected set; }
public bool completed { get; private set; }
protected abstract void Init();
/// <summary>
/// Base Activate function, called by inheriting command before its own custom activation command
/// </summary>
/// <param name="_parentStep">The step this command is attached to</param>
public virtual void Activate(Step _parentStep)
{
parentStep = _parentStep;
if (!init)
Init();
active = true;
// If this is a pausable command, add it to activePausableCommands list
if (this is IPausable)
{
if (parentStep.attachedStepTimeline)
parentStep.attachedStepTimeline.AddActivePausableCommand(this);
else if (parentStep.attachedController)
parentStep.attachedController.AddActivePausableCommand(this);
}
}
/// <summary>
/// Called by any commands which have a limited number of components on a particular step, to see if max has been reached
/// </summary>
/// <param name="_currentActivatingStep">The Step we want to check for max commands (the one currently being activated)</param>
/// <param name="_maxCommandsOfTypeOnStep">How many the max no. of commands of that type on this step there should be</param>
/// <param name="_activeCommandsOfTypeOnStep">All instances of this command type on the step</param>
/// <returns></returns>
protected virtual bool CheckMaxCommandsOnStep(Step _currentActivatingStep, int _maxCommandsOfTypeOnStep, List<StepCmd> _activeCommandsOfTypeOnStep)
{
// Check if too many commands of this type have already been activated on this step?
if (_currentActivatingStep != null && _currentActivatingStep == parentStep)
{
// If so, return a warning, and return without ever activating this command
if (_activeCommandsOfTypeOnStep.Count >= _maxCommandsOfTypeOnStep)
{
Debug.LogWarning("Tried to activate a command when the maximum of " + _maxCommandsOfTypeOnStep.ToString() + " of this command type per step have already been activated. Make sure there are only " + _maxCommandsOfTypeOnStep.ToString() + " of this type of command type on each Step.");
return false;
}
}
else
{
// First command of this type activated on this step. Clear our list from previous step (if there was one)
_currentActivatingStep = null;
_activeCommandsOfTypeOnStep.Clear();
}
// Otherwise, we're fine, keep a log of the step this command was activated on, then proceed to activate it
_currentActivatingStep = parentStep;
_activeCommandsOfTypeOnStep.Add(this);
return true;
}
/// <summary>
/// Called by the command once whatever it does has been completed, and the command is to deactivate. Performs generic completion stuff, then calls back into the command itself to Cleanup
/// </summary>
protected virtual void Complete()
{
completed = true;
active = false;
Cleanup();
// If this is a pausable command, ensure it's removed from activePausableCommands list
if (this is IPausable)
{
if (parentStep.attachedStepTimeline)
parentStep.attachedStepTimeline.RemoveActivePausableCommand(this);
else if (parentStep.attachedController)
parentStep.attachedController.RemoveActivePausableCommand(this);
}
onCompleteCommand?.Invoke();
}
/// <summary>
/// Called by an external component (such as DeactivateCommandsCmd) to deactivate a command before it's actually completing - i.e. stop whatever it's doing then clean it up
/// </summary>
public void DeactivateEarly()
{
// TODO: Look at actually deactivating whatever the command behaviour is, rather than just setting in inactive and tidying up?
active = false;
Cleanup();
}
/// <summary>
/// Implemented be command to tidy up any specific stuff in that command that needs tidied up when the command has been deactivated
/// </summary>
protected abstract void Cleanup();
#if UNITY_EDITOR
#region Editor Inspector functions
bool Inspector_ImplementsPausable()
{
return (this is IPausable);
}
#endregion
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c07706edea72534daeb8c9db1e9b651
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,125 @@
using TriInspector;
using UnityEngine;
using UnityEngine.Video;
namespace R0bbie.Timeline
{
/// <summary>
/// Video StepCmd type, used to control a video
/// </summary>
[AddComponentMenu("Timeline/Commands/Video (Step Command)")]
[RequireComponent(typeof(Step))]
public class VideoCmd : StepCmd, IPausable
{
// Exposed variables
[Title("Video Settings")]
[SerializeField] protected VideoPlayer videoPlayer;
[SerializeField] protected VideoClip clip;
bool eventSubscribed_VideoEndReached;
/// <summary>
/// Initialise command (called before Activate)
/// </summary>
protected override void Init()
{
init = true;
}
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// TODO: Consider what the command itself should control, or pass to an external controller
// What about looping videos etc? Would you want the command to start a video looping and leave it looping once the command was complete?
// Complete conditions - On video end, on number of loops?
// Ensure video player component is active, and set clip to the one we want to play
videoPlayer.gameObject.SetActive(true);
videoPlayer.clip = clip;
// Listen for event when video reaches end
videoPlayer.loopPointReached += VideoEndReached;
eventSubscribed_VideoEndReached = true;
// Play video
videoPlayer.Play();
}
private void VideoEndReached(VideoPlayer _videoPlayer)
{
// Unsubscribe from event
videoPlayer.loopPointReached -= VideoEndReached;
eventSubscribed_VideoEndReached = false;
// Ensure video stopped
videoPlayer.Stop();
// Clear clip
videoPlayer.clip = null;
// Complete command
Complete();
}
protected override void Cleanup()
{
active = false;
}
public void Pause()
{
if (!active)
return;
// Pause video
videoPlayer.Pause();
}
public void Resume(float _elapsedTime)
{
if (!active)
return;
// If the time paused is longer than RepeatOnResumeDelay const, then we'll repeat the animation from the start
if (_elapsedTime >= StepTimeline.RepeatOnResumeDelay && !dontRepeatOnPauseResume)
{
RepeatFromStart();
}
else
{
// Resume video (if currently paused)
if (videoPlayer.isPaused)
{
videoPlayer.Play();
}
}
}
void RepeatFromStart()
{
// Ensure we're still subscribed to video end event
if (!eventSubscribed_VideoEndReached)
{
videoPlayer.loopPointReached += VideoEndReached;
eventSubscribed_VideoEndReached = true;
}
// Reset video player to start then play
videoPlayer.frame = 0;
videoPlayer.Play();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44bbcdad2ef168240a8f9d7dcc79f291
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,71 @@
using TriInspector;
using UnityEngine;
using UnityEngine.Events;
using R0bbie.Timeline.Events;
namespace R0bbie.Timeline
{
/// <summary>
/// Just watches for custom GameEvent being raised, then auto-completes
/// </summary>
[AddComponentMenu("Timeline/Commands/Watch for Event (Step Command)")]
[RequireComponent(typeof(Step))]
public class WatchForEventCmd : StepCmd, IGameEventListener
{
// Exposed Variables
[Title("Event Reference")]
[SerializeField] GameEvent gameEventToWatchFor;
bool registered;
/// <summary>
/// Initialise command (called before Activate)
/// </summary>
protected override void Init()
{
init = true;
}
public override void Activate(Step _parentStep)
{
base.Activate(_parentStep);
// Start watching for GameEvent...
if (!registered)
Register();
}
void Register()
{
gameEventToWatchFor.RegisterListener(this);
registered = true;
}
void Unregister()
{
gameEventToWatchFor.UnregisterListener(this);
registered = false;
}
public void OnEventRaised()
{
Unregister();
Complete();
}
protected override void Cleanup()
{
active = false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4df40ed0726adf34b8051b16b021d598
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Scripts/Editor.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ed442ae6eba152b4b82258f39a41222c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
{
"name": "R0bbie.Timeline.Editor",
"rootNamespace": "R0bbie.Timeline.Editor",
"references": [
"GUID:683a8bf2a8db6154b9177e122c046565",
"GUID:e851236b9ac2b9b4eaaa99506366edea"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: af285bff4afff234c98e8dc81deeef5f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,69 @@
using UnityEditor;
using UnityEngine;
using TriInspector.Editors;
using UnityEngine.UIElements;
namespace R0bbie.Timeline
{
/// <summary>
/// Editor class to update the GameObject name based on the shortName of the step and child position
/// </summary>
[CustomEditor(typeof(Step),true)]
public class StepEditor : TriEditor
{
private TriEditorCore _core;
/// <summary>
/// Unity OnEnable function - called here when object is selected to be viewed in inspector
/// </summary>
void OnEnable()
{
Rename();
// Re-implement base TriEditor.OnEnable logic (not overridable sadly as its private)
_core = new TriEditorCore(this);
}
/// <summary>
/// Unity OnDisable function - called here when object selection in inspector view is cleared
/// </summary>
void OnDisable()
{
Rename();
// Re-implement base TriEditor.OnDisable logic
_core.Dispose();
}
/// <summary>
/// Rename the Step GameObject to reflect latest settings
/// </summary>
void Rename()
{
// If the object has been destroyed avoid changing the name
if (!target)
return;
// Get target as Step
Step step = target as Step;
if (!step)
return;
step.UpdateStepName();
}
/// <summary>
/// Function from base TriEditor class which frustratingly we have to re-implement here due to accessibility issues in the base class
/// </summary>
/// <returns></returns>
public override VisualElement CreateInspectorGUI()
{
return _core.CreateVisualElement();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efbff33e8d18109479bfa8a74e42ce2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

3
Scripts/Events.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e49844d1f2e94213bd017f9ad63b2039
timeCreated: 1661478048

View File

@ -0,0 +1,62 @@
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using TriInspector;
#endif
namespace R0bbie.Timeline.Events
{
/// <summary>
/// Scriptable Object to handle in game events without the need of direct references
/// </summary>
[CreateAssetMenu(fileName = "GameEvent", menuName = "Timeline/Game Event")]
public class GameEvent : ScriptableObject
{
// Private Variables
List<IGameEventListener> listeners = new List<IGameEventListener>();
/// <summary>
/// Call all listeners registered under this event
/// </summary>
public void Raise()
{
for (int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}
/// <summary>
/// Register listener to be called when event raised
/// </summary>
/// <param name="listener"></param>
public void RegisterListener(IGameEventListener listener)
{
// Check that the listener is not on the list before adding it to avoid duplicates
if(!listeners.Contains(listener))
listeners.Add(listener);
}
/// <summary>
/// Unregister listener to not be called anymore
/// </summary>
/// <param name="listener"></param>
public void UnregisterListener(IGameEventListener listener)
{
// Check that the listener is part of the list before trying to remove it
if(listeners.Contains(listener))
listeners.Remove(listener);
}
#if UNITY_EDITOR
[Button("Test Raise")]
void Test()
{
Raise();
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 33416bcf642caac4998573878bb99f91
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,89 @@
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
namespace R0bbie.Timeline.Events
{
/// <summary>
/// Register to the linked GameEvent scriptable object, and trigger any function assign to the unity event
/// </summary>
[AddComponentMenu("Timeline/Game Events/Event Listener")]
public class GameEventListener : MonoBehaviour, IGameEventListener
{
// Exposed Variables
[Header("Event Listener")]
public bool registerAutomatically = true;
public bool triggerOnce;
[Space(10)]
[FormerlySerializedAs("animationStateEvent")]
public GameEvent gameEvent;
public UnityEvent response = new UnityEvent();
// Private Variables
bool registered;
bool triggered;
void OnEnable()
{
if (!gameEvent)
return;
// If is not set to registered automatically, and haven't been registered yet, do nothing
if (!registerAutomatically && !registered)
return;
// If already triggered, do nothing
if (triggerOnce && triggered)
return;
Register();
}
void OnDisable()
{
// If there is no game event attached at the moment the object is disabled, do nothing
if (!gameEvent)
return;
// Unregister listener but remember status
gameEvent.UnregisterListener(this);
}
public void OnEventRaised()
{
// If for some reason the event is still registered when it shouldn't, do nothing
if (triggerOnce && triggered)
return;
triggered = true;
response?.Invoke();
// Unregister and disable to avoid calling onDisable when unloading the level
if (triggerOnce)
Unregister();
}
public void Register()
{
gameEvent.RegisterListener(this);
registered = true;
}
public void Unregister()
{
gameEvent.UnregisterListener(this);
registered = false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 192fcff1e69520e49b2bf437f3079b1a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
using UnityEngine;
namespace R0bbie.Timeline.Events
{
public class GameEventMultipleTrigger : MonoBehaviour
{
[Header("Linked Event")]
public GameEvent[] gameEvent;
public void Trigger(int index)
{
// Only trigger if there is an event at that index
if (index < gameEvent.Length)
gameEvent[index].Raise();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96bf9479be478e747898fbce37e42829
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,13 @@
namespace R0bbie.Timeline.Events
{
/// <summary>
/// Interface to create custom event listeners inside another classes
/// </summary>
public interface IGameEventListener
{
/// <summary>
/// Action to do when the event is raised
/// </summary>
public void OnEventRaised();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f66c81ba53a2e824ca292c53bdcbb0ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

388
Scripts/StepController.cs Normal file
View File

@ -0,0 +1,388 @@
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
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3baa379cd674430998fe97d77e1f5c2e
timeCreated: 1659436582

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a554e01126d3416ebb3404d8dac980c9
timeCreated: 1648757375

View File

@ -0,0 +1,151 @@
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Wait for an action to be completed, allowing to trigger an incorrect step
/// </summary>
[AddComponentMenu("Timeline/StepSwitches/Correct Action (Step Switch)")]
public class CorrectActionStepSwitch : StepSwitch
{
[System.Serializable]
struct ActionSwitch
{
public TimelineTrigger trigger;
public Step stepOption;
}
// Exposed Variables
[Title("Correct Action Step Switch")]
[SerializeField] ActionSwitch incorrectStep;
[SerializeField] bool keepPlaying;
[Space(10)]
[SerializeField] ActionSwitch correctStep;
// Private variables
bool incorrectPlayed;
bool correctPlayed;
/// <summary>
/// Init by Timeline
/// </summary>
/// <param name="_stepTimeline"></param>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs such as parent timeline
attachedStepTimeline = _stepTimeline;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
if (incorrectStep.stepOption)
incorrectStep.stepOption.Init(attachedStepTimeline, this);
incorrectStep.trigger.AddStep(this);
if (correctStep.stepOption)
correctStep.stepOption.Init(attachedStepTimeline, this);
correctStep.trigger.AddStep(this);
init = true;
}
/// <summary>
/// Init switch by StepController
/// </summary>
/// <param name="_controller"></param>
public override void Init(StepController _controller)
{
// Setup necessary refs such as parent controller
attachedController = _controller;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
if (incorrectStep.stepOption)
incorrectStep.stepOption.Init(attachedController, this);
incorrectStep.trigger.AddStep(this);
if (correctStep.stepOption)
correctStep.stepOption.Init(attachedController, this);
correctStep.trigger.AddStep(this);
init = true;
}
public override void Play()
{
Debug.Log(name + " - Started");
isActive = true;
waitingForExternalTrigger = true;
// Wait for event from the player
activeMode = Mode.WaitingForEvent;
}
/// <summary>
/// Trigger command received from external TimelineTrigger
/// </summary>
public override void TriggerContinue(TimelineTrigger _trigger)
{
if (!isActive || activeMode != Mode.WaitingForEvent)
return;
if (_trigger == incorrectStep.trigger)
{
if (!incorrectPlayed || keepPlaying)
PlaySelectedStepOption(incorrectStep.stepOption);
incorrectPlayed = true;
}
else
{
// Change the status only if the correct action have been completed
// If is not completed we still want to be able to receive input from triggers
activeMode = Mode.PlayingChildStep;
correctPlayed = true;
PlaySelectedStepOption(correctStep.stepOption);
}
}
void PlaySelectedStepOption(Step _option)
{
activeStepOption = _option;
// Play the selected option, unless it's null (in which case just play next step in timeline)
if (activeStepOption)
{
activeStepOption.Play();
}
else
{
Debug.Log("The option selected by the step switch was null, so just proceeding to next step in timeline.");
Continue();
}
}
public override void Continue()
{
if (!correctPlayed)
return;
// If the correct step have been played, then continue to the next step
base.Continue();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10abc55aef1f11d4ea461c302df84e31
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,138 @@
using System.Collections.Generic;
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
[AddComponentMenu("Timeline/StepSwitches/Counter (Step Switch)")]
public class CounterStepSwitch : StepSwitch
{
// Assign the step, and how long should the timeline wait before trigger the next step
[System.Serializable]
struct CountSwitch
{
[SerializeField] public Step stepOption;
[SerializeField] public int triggerAfter;
}
// Private Variables
[Title("Switch Options")]
[SerializeField] List<CountSwitch> countSwitches = new List<CountSwitch>();
[SerializeField] TimelineTrigger externalTrigger;
[SerializeField] bool repeatStepsInSwitch;
int count;
int activeStepIndex = -1;
/// <summary>
/// Init switch with Timeline
/// </summary>
/// <param name="_stepTimeline"></param>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs such as parent timeline
attachedStepTimeline = _stepTimeline;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
foreach (CountSwitch countSwitch in countSwitches)
{
if (countSwitch.stepOption)
countSwitch.stepOption.Init(attachedStepTimeline, this);
}
externalTrigger.AddStep(this);
init = true;
}
/// <summary>
/// Init switch by StepController
/// </summary>
/// <param name="_controller"></param>
public override void Init(StepController _controller)
{
// Setup necessary refs such as parent controller
attachedController = _controller;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
foreach (CountSwitch countSwitch in countSwitches)
{
if (countSwitch.stepOption)
countSwitch.stepOption.Init(attachedController, this);
}
externalTrigger.AddStep(this);
init = true;
}
/// <summary>
/// Play this StepSwitch, in turn playing whichever step its conditions point to
/// </summary>
public override void Play()
{
Debug.Log(name + " - Started");
isActive = true;
waitingForExternalTrigger = true;
activeMode = Mode.WaitingForEvent;
}
public void IncreaseCount()
{
if (!isActive)
return;
count++;
// Reset step index, or exit this method if all steps in the switch have already been performed
if (repeatStepsInSwitch)
{
if (countSwitches.Count <= activeStepIndex + 1)
activeStepIndex = -1;
else return;
}
if (count >= countSwitches[activeStepIndex + 1].triggerAfter && (activeStepIndex + 1) <= countSwitches.Count)
{
activeStepIndex++;
countSwitches[activeStepIndex].stepOption.Play();
// If it's the last one, disable the counter for now
if (activeStepIndex >= countSwitches.Count)
isActive = false;
}
}
public override void Continue()
{
// Leave empty as we want to wait until the player completes the actual task
// And the steps are triggered through increase of the count tries
}
public override void TriggerContinue(TimelineTrigger _trigger)
{
if (!isActive || activeMode != Mode.WaitingForEvent)
return;
base.Continue();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 736f7f84c60d0d248a617b0cf796dd6e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,188 @@
using UnityEngine;
using System.Collections.Generic;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Toggle between step option based on what external TimelineTrigger is received
/// </summary>
[AddComponentMenu("Timeline/StepSwitches/External Trigger (Step Switch)")]
public class ExternalTriggerStepSwitch : StepSwitch
{
enum IfStepAlreadyPlayedAction
{
PlayAgain,
DoNothing,
SkipSwitch
}
[System.Serializable]
struct TriggerSwitch
{
[SerializeField] public TimelineTrigger trigger;
[SerializeField] public Step stepOption;
[SerializeField] public IfStepAlreadyPlayedAction ifStepPrevPlayed;
}
[Title("Switch Options")]
[SerializeField] List<TriggerSwitch> triggerSwitches = new List<TriggerSwitch>();
[Title("Adjust Trigger Behaviour")]
[SerializeField, Tooltip("If a trigger attached to this switch was triggered previously, set the corresponding step option as active immediately, rather than waiting for a live trigger.")] private bool switchImmediatelyIfPrevTriggered;
[ShowIf(nameof(switchImmediatelyIfPrevTriggered))]
[SerializeField, Tooltip("If no triggers attached to this switch have been triggered previously, just skip this switch altogether.")] private bool skipImmediatelyIfNotPrevTriggered;
/// <summary>
/// Initialise the StepSwitch by Timeline
/// </summary>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs such as parent timeline
attachedStepTimeline = _stepTimeline;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
foreach (TriggerSwitch triggerSwitch in triggerSwitches)
{
triggerSwitch.stepOption.Init(attachedStepTimeline, this);
triggerSwitch.trigger.AddStep(this);
}
init = true;
}
/// <summary>
/// Initialise the StepSwitch by StepController
/// </summary>
public override void Init(StepController _controller)
{
// Setup necessary refs such as parent controller
attachedController = _controller;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
foreach (TriggerSwitch triggerSwitch in triggerSwitches)
{
triggerSwitch.stepOption.Init(attachedController, this);
triggerSwitch.trigger.AddStep(this);
}
init = true;
}
/// <summary>
/// Play this StepSwitch, in turn playing whichever step its conditions point to
/// </summary>
public override void Play()
{
Debug.Log(name + " - Started");
isActive = true;
activeMode = Mode.WaitingForEvent;
// Option to play a child option immediately if the paired trigger was triggered previously
if (switchImmediatelyIfPrevTriggered)
{
// Loop through all triggers and check if any were triggered before
foreach (TriggerSwitch triggerSwitch in triggerSwitches)
{
// If so we've got our step option to play
if (triggerSwitch.trigger.triggered)
{
activeStepOption = triggerSwitch.stepOption;
break;
}
}
// If no triggers were previously triggered, option to just skip over this switch altogether, and inform the timeline to play the next step
if (skipImmediatelyIfNotPrevTriggered)
{
SkipSwitch();
return;
}
// If active step is now setup, play it!
if (activeStepOption)
{
activeMode = Mode.PlayingChildStep;
activeStepOption.Play();
}
}
// If we've got here then we want to wait for an external trigger to send instruction to this switch..
}
void SkipSwitch()
{
activeMode = Mode.Inactive;
Continue();
}
/// <summary>
/// Trigger command received from external TimelineTrigger
/// </summary>
public override void TriggerContinue(TimelineTrigger _trigger)
{
if (!isActive || activeMode != Mode.WaitingForEvent)
return;
// Find this trigger in the list, and set the corresponding child step as active
int switchIndexInList = triggerSwitches.FindIndex(x => x.trigger == _trigger);
// If this trigger wasn't found in the list, we can't play any child step. Don't do anything
if (switchIndexInList == -1)
{
Debug.LogWarning("ExternalTriggerStepSwitch received a command from a trigger not in the switch list, can't do anything.");
return;
}
// With the trigger found in the list, set active step option to corresponding step
activeStepOption = triggerSwitches[switchIndexInList].stepOption;
// Check if step was previously played, and if we want to execute any custom action here if so
if (activeStepOption.wasCompleted)
{
if (triggerSwitches[switchIndexInList].ifStepPrevPlayed == IfStepAlreadyPlayedAction.DoNothing)
{
activeStepOption = null;
return;
}
else if (triggerSwitches[switchIndexInList].ifStepPrevPlayed == IfStepAlreadyPlayedAction.SkipSwitch)
{
SkipSwitch();
return;
}
}
// Target step option to play is now definitely set..
// Play the selected option, unless it's null (in which case just play next step in timeline)
if (activeStepOption)
{
activeMode = Mode.PlayingChildStep;
activeStepOption.Play();
}
else
{
Debug.Log("The option selected by the step switch was null, so just proceeding to next step in timeline.");
SkipSwitch();
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 834a12c2ec5a4e6ea419e14e319d809f
timeCreated: 1648758186

View File

@ -0,0 +1,121 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Plays one of its sub steps at random
/// </summary>
[AddComponentMenu("Timeline/StepSwitches/Randomised (Step Switch)")]
public class RandomisedStepSwitch : StepSwitch
{
// Add ability to avoid repeating the same option twice
[SerializeField, Tooltip("Try to avoid playing the same step twice in a row (only applicable if a StepLooper is repeating this switch!)")]
bool avoidRepeats;
List<Step> childSteps = new List<Step>();
int lastSelectedStepIndex = -1;
/// <summary>
/// Initialise the StepSwitch
/// </summary>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs such as parent timeline
attachedStepTimeline = _stepTimeline;
// Get refs to all child steps (only 1 level deep)
foreach (Transform t in transform)
{
if (t.GetComponent<Step>())
{
childSteps.Add(t.GetComponent<Step>());
}
}
// Init all child steps
foreach (var step in childSteps)
{
step.Init(attachedStepTimeline, this);
}
// Reset stored values
lastSelectedStepIndex = -1;
init = true;
}
/// <summary>
/// Initialise the StepSwitch by StepController
/// </summary>
public override void Init(StepController _controller)
{
// Setup necessary refs such as parent controller
attachedController = _controller;
// Get refs to all child steps (only 1 level deep)
foreach (Transform t in transform)
{
if (t.GetComponent<Step>())
{
childSteps.Add(t.GetComponent<Step>());
}
}
// Init all child steps
foreach (var step in childSteps)
{
step.Init(attachedController, this);
}
// Reset stored values
lastSelectedStepIndex = -1;
init = true;
}
/// <summary>
/// Play this StepSwitch, in turn playing one randomly selected child step
/// </summary>
public override void Play()
{
isActive = true;
// If no child step options exist, do nothing, and just skip onto the next step in the timeline
if (childSteps.Count < 1)
Continue();
// Select a child step at random
int randomStepIndex = Random.Range(0, (childSteps.Count - 1));
// If selected step is same as last one, try selecting avoid
if (avoidRepeats && randomStepIndex == lastSelectedStepIndex)
randomStepIndex = Random.Range(0, (childSteps.Count - 1));
// Set the active step option to the one selected
activeStepOption = childSteps[randomStepIndex];
// Play the selected option, unless it's null (in which case just play next step in timeline)
if (activeStepOption)
{
activeMode = Mode.PlayingChildStep;
activeStepOption.Play();
}
else
{
Debug.Log("The option selected by the step switch was null, so just proceeding to next step in timeline.");
Continue();
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cb482d05f9754bd7ac2c629b36b1eeef
timeCreated: 1648765606

View File

@ -0,0 +1,170 @@
using System.Collections;
using System.Collections.Generic;
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Trigger the next step on the switch after a delay if the player doesn't realize the desired action
/// </summary>
[AddComponentMenu("Timeline/StepSwitches/Reminder (Step Switch)")]
public class ReminderStepSwitch : StepSwitch
{
// Assign the step, and how long should the timeline wait before trigger the next step
[System.Serializable]
struct ReminderSwitch
{
[SerializeField] public Step stepOption;
[SerializeField] public float secondsToTrigger;
}
// Exposed Variables
[Title("Switch Options")]
[SerializeField] List<ReminderSwitch> reminderSwitches = new List<ReminderSwitch>();
[SerializeField] bool keepTriggeringSteps;
// Private Variables
int stepIndex;
/// <summary>
/// Init switch by Timeline
/// </summary>
/// <param name="_stepTimeline"></param>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs such as parent timeline
attachedStepTimeline = _stepTimeline;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
foreach (ReminderSwitch reminderSwitch in reminderSwitches)
{
reminderSwitch.stepOption.Init(attachedStepTimeline, this);
}
init = true;
}
/// <summary>
/// Init switch by StepController
/// </summary>
/// <param name="_controller"></param>
public override void Init(StepController _controller)
{
// Setup necessary refs such as parent controller
attachedController = _controller;
// Inactive by default
activeMode = Mode.Inactive;
// Init all step switch options
foreach (ReminderSwitch reminderSwitch in reminderSwitches)
{
reminderSwitch.stepOption.Init(attachedController, this);
}
init = true;
}
public override void Play()
{
Debug.Log(name + " - Started");
isActive = true;
waitingForExternalTrigger = true;
activeMode = Mode.WaitingForEvent;
// Trigger the first step
reminderSwitches[stepIndex].stepOption.Play();
// Start the coroutine in case the player doesn't press the button before the first reminder
StartCoroutine(TriggerReminder(reminderSwitches[stepIndex].secondsToTrigger));
}
IEnumerator TriggerReminder(float delayTime)
{
yield return new WaitForSeconds(delayTime);
// If by the time this part is triggered, the step has skipped through external Step Controller then avoid continue
if (wasSkipped || !isActive)
yield break;
// If the step haven't been completed yet, skip it and trigger the next reminder on the list
if (!wasCompleted)
{
reminderSwitches[stepIndex].stepOption.Skip();
}
}
public override void Continue()
{
// If the active step was skipped because of the delay, trigger the next one on the switch
if (reminderSwitches[stepIndex].stepOption.wasSkipped && !reminderSwitches[stepIndex].stepOption.wasCompleted)
{
stepIndex++;
// If we want to loop again through the steps
if (keepTriggeringSteps)
{
// Reset the index to 0
if (stepIndex >= reminderSwitches.Count)
stepIndex = 0;
}
// Play the next one if it's inside the length of the list
if (stepIndex < reminderSwitches.Count)
{
// Trigger the next step
reminderSwitches[stepIndex].stepOption.Play();
// If the actual step is not the last one, or we want to loop through all the steps, start the coroutine to trigger the reminder
if (stepIndex < reminderSwitches.Count - 1 || keepTriggeringSteps)
StartCoroutine(TriggerReminder(reminderSwitches[stepIndex].secondsToTrigger));
}
}
else // Triggered through the timeline trigger
{
// If the step switch was already completed and is called through the same timeline trigger twice, only register one
if (wasCompleted)
return;
Debug.Log(name + " - Completed");
// Complete the switch
isActive = false;
activeMode = Mode.Inactive;
wasCompleted = true;
// Tell the timeline to play the next step (or step group, or controller)
if (parentStepGroup)
{
parentStepGroup.Continue();
}
else
{
if (attachedStepTimeline != null)
attachedStepTimeline.PlayNextStep();
else
attachedController.StepCompleted();
}
// TODO Complete the current switch and steps inside
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 075dbe4732565d84d8559bedda870952
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,99 @@
using System.Collections.Generic;
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Trigger the child steps in order, increasing index each time is played
/// </summary>
public class StepPoolSwitch : StepSwitch
{
// Exposed Variables
[Title("Pool Settings")]
[InfoBox("Only compatible with steps inside a StepController", TriMessageType.Warning)]
[SerializeField] bool storeIndexOnDevice;
// Private Variables
List<Step> childSteps = new List<Step>();
int activeIndex;
/// <summary>
/// Initialise StepPool via controller
/// </summary>
/// <param name="_controller"></param>
public override void Init(StepController _controller)
{
attachedController = _controller;
InitPool();
}
/// <summary>
/// Initialise StepPool via controller, if child of StepGroup
/// </summary>
/// <param name="_controller"></param>
public override void Init(StepController _controller, StepGroup _stepGroup)
{
parentStepGroup = _stepGroup;
Init(_controller);
}
void InitPool()
{
// If getting the store from memory, use the step name as the key, otherwise start at value 0
if (storeIndexOnDevice)
activeIndex = PlayerPrefs.GetInt(shortName);
// Setup necessary refs to parent timeline, and child steps (only 1 level deep to avoid getting children of StepSwitches etc)
foreach (Transform t in transform)
{
Step childStep = t.GetComponent<Step>();
if (childStep)
{
childSteps.Add(childStep);
childStep.Init(attachedController, this);
}
}
init = true;
}
/// <summary>
/// Play this StepGroup with the current active step index
/// </summary>
public override void Play()
{
// Set the index to start at the first step
Debug.Log(name + " - Started");
isActive = true;
// Only play the active step
Step activeChildStep = childSteps[activeIndex];
activeChildStep.Play();
activeIndex++;
// If index out of range, reset it
if (activeIndex >= childSteps.Count)
activeIndex = 0;
if (storeIndexOnDevice)
PlayerPrefs.SetInt(shortName, activeIndex);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7d75fa3fe456914f89d063bba8a2d8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,114 @@
using UnityEngine;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Allow the selection of the next step based on a player action
/// </summary>
public abstract class StepSwitch : Step
{
protected enum Mode
{
Inactive,
WaitingForEvent,
PlayingChildStep
}
protected Mode activeMode;
protected Step activeStepOption;
/// <summary>
/// Tell timeline to continue once step option associated with this Switch has completed
/// </summary>
public override void Continue()
{
Debug.Log(name + " - Completed");
isActive = false;
activeMode = Mode.Inactive;
wasCompleted = true;
// Tell the timeline to play the next step (or step group, or controller)
if (parentStepGroup)
{
parentStepGroup.Continue();
}
else if (parentStepSwitch)
{
parentStepSwitch.Continue();
}
else
{
if (attachedStepTimeline != null)
attachedStepTimeline.PlayNextStep();
else
attachedController.StepCompleted();
}
}
#if UNITY_EDITOR
// ODIN INSPECTOR BUTTONS
[Title("Add Option to Switch")]
[Button("Add Step")]
private void Editor_AddStepOption()
{
// Create new GameObject, and reparent it as a child of the StepSwitch parent object
GameObject stepGo = new GameObject();
stepGo.transform.parent = transform;
// Add Step component to this new GameObject
Step step = stepGo.AddComponent<Step>();
// Rename object
int parentStepNo = transform.GetSiblingIndex() + 1;
int childStepNo = step.transform.GetSiblingIndex() + 1;
step.gameObject.name = "#" + parentStepNo.ToString("00") + "." + childStepNo.ToString("00") + " - Switch Option - *ADD STEP NAME*";
}
[Button("Add GoTo Step")]
private void Editor_AddGoToStepOption()
{
// Create new GameObject, and reparent it as a child of the StepSwitch parent object
GameObject stepGo = new GameObject();
stepGo.transform.parent = transform;
// Add GoToStep component to this new GameObject
GoToStep step = stepGo.AddComponent<GoToStep>();
// Rename object
int parentStepNo = transform.GetSiblingIndex() + 1;
int childStepNo = step.transform.GetSiblingIndex() + 1;
step.gameObject.name = "#" + parentStepNo.ToString("00") + "." + childStepNo.ToString("00") + " - GOTO - Switch Option - *ADD STEP NAME*";
}
[Title("Editor Functions")]
[Button("Rename Child Steps")]
private void Editor_RenameChildren()
{
RenameChildren();
}
// OTHER EDITOR HELPER FUNCTIONS
public void RenameChildren()
{
foreach (Transform child in transform)
{
if (child.GetComponent<Step>())
child.GetComponent<Step>().UpdateStepName();
}
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6f1388074bc9495291bc0198c990fb7e
timeCreated: 1647378284

View File

@ -0,0 +1,195 @@
using UnityEngine;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Watch for an event to happen, and if it doesn't after some delay, trigger a step to assist the player
/// </summary>
[AddComponentMenu("Timeline/StepSwitches/Watch and Assist (Step Switch)")]
public class WatchAndAssistStepSwitch : StepSwitch
{
public enum WatchForEvent
{
OnCommandComplete,
OnExternalTrigger
}
[Title("Switch Setup")]
[SerializeField] WatchForEvent watchForEvent;
[ShowIf("watchForEvent", WatchForEvent.OnCommandComplete)]
[SerializeField] StepCmd watchOnCommandComplete;
[ShowIf("watchForEvent", WatchForEvent.OnExternalTrigger)]
[SerializeField] TimelineTrigger watchTrigger;
[SerializeField, Tooltip("If watching for OnCommandComplete will skip if command previously completed, if OnExternalTrigger then if prev triggered")] bool skipIfPreviouslyTriggered;
[SerializeField] float waitForSeconds;
[Title("Switch Options")]
[SerializeField, Tooltip("Step switched to if event isn't detected before waitForSeconds runs out")]
Step assistAfterDelayStep;
[SerializeField, Tooltip("Step switched to if event is detected before waitForSeconds is up. If left null will just continue to next step in timeline")]
Step watchedEventPerformedStep;
float timer;
bool timedOut;
/// <summary>
/// Initialise the StepSwitch with Timeline
/// </summary>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs such as parent timeline
attachedStepTimeline = _stepTimeline;
// Inactive by default
activeMode = Mode.Inactive;
// Init all child steps
if (assistAfterDelayStep)
assistAfterDelayStep.Init(attachedStepTimeline, this);
if (watchedEventPerformedStep)
watchedEventPerformedStep.Init(attachedStepTimeline, this);
ResetTimer();
init = true;
}
/// <summary>
/// Initialise the StepSwitch with StepController
/// </summary>
public override void Init(StepController _controller)
{
// Setup necessary refs such as parent controller
attachedController = _controller;
// Inactive by default
activeMode = Mode.Inactive;
// Init all child steps
if (assistAfterDelayStep)
assistAfterDelayStep.Init(attachedController, this);
if (watchedEventPerformedStep)
watchedEventPerformedStep.Init(attachedController, this);
ResetTimer();
init = true;
}
void ResetTimer()
{
timer = 0;
timedOut = false;
}
/// <summary>
/// Play this StepSwitch
/// </summary>
public override void Play()
{
isActive = true;
Debug.Log(name + " - Started");
// Set mode to waiting for event
activeMode = Mode.WaitingForEvent;
// Continue immediately if action we're watching for was previously performed?
if (skipIfPreviouslyTriggered)
{
if (watchForEvent == WatchForEvent.OnCommandComplete && watchOnCommandComplete)
{
if (watchOnCommandComplete.completed)
{
activeStepOption = watchedEventPerformedStep;
PlaySetStepOption();
}
}
else if (watchForEvent == WatchForEvent.OnExternalTrigger && watchTrigger)
{
waitingForExternalTrigger = true;
if (watchTrigger.triggered)
{
activeStepOption = watchedEventPerformedStep;
PlaySetStepOption();
}
}
}
}
void PlaySetStepOption()
{
// Play the selected option, unless it's null (in which case just play next step in timeline)
if (activeStepOption)
{
activeMode = Mode.PlayingChildStep;
activeStepOption.Play();
}
else
{
Debug.Log("The option selected by the step switch was null, so just proceeding to next step in timeline.");
Continue();
}
}
void Update()
{
// If step switch isn't active, is paused, has already timed out, or isn't currently waiting for an event to happen (i.e. may be playing a child step already), then don't handle timer
if (!isActive || isPaused || timedOut || activeMode != Mode.WaitingForEvent)
return;
// Add to timer
timer += Time.deltaTime;
// Time out once we've reached a set target seconds
if (timer > waitForSeconds)
timedOut = true;
if (timedOut)
{
activeStepOption = assistAfterDelayStep;
PlaySetStepOption();
return;
}
// Wait for delay / action to be performed
if (watchForEvent == WatchForEvent.OnCommandComplete && watchOnCommandComplete)
{
if (watchOnCommandComplete.completed)
{
activeStepOption = watchedEventPerformedStep;
PlaySetStepOption();
}
}
else if (watchForEvent == WatchForEvent.OnExternalTrigger && watchTrigger)
{
if (watchTrigger.triggered)
{
activeStepOption = watchedEventPerformedStep;
PlaySetStepOption();
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06537e8bbb6340099bfdcf33ffabb298
timeCreated: 1659446099

479
Scripts/StepTimeline.cs Normal file
View File

@ -0,0 +1,479 @@
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
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bbf728ffa6714547ba853641784e5b7f
timeCreated: 1647378107

View File

@ -0,0 +1,66 @@
using UnityEngine;
namespace R0bbie.Timeline
{
public class StepTimelineManager : MonoBehaviour
{
public static StepTimeline activeStepTimeline;
public static StepController activeController;
public static Step lastPlayedStep;
static Step pausedOnStep;
public static void ClearState()
{
activeStepTimeline = null;
activeController = null;
lastPlayedStep = null;
pausedOnStep = null;
}
public static bool WasLastStepOnTimeline()
{
if (lastPlayedStep && lastPlayedStep.attachedStepTimeline)
return true;
else
return false;
}
/// <summary>
/// Pause all commands on the currently active timeline or controller (if one is active)
/// </summary>
public static void PauseCommands()
{
if (lastPlayedStep && lastPlayedStep.isActive)
{
if (lastPlayedStep.attachedStepTimeline)
lastPlayedStep.attachedStepTimeline.Pause();
else if (lastPlayedStep.attachedController)
lastPlayedStep.attachedController.Pause();
pausedOnStep = lastPlayedStep;
}
}
/// <summary>
/// Resume all commands on the currently active timeline or controller (if one was active when game was paused)
/// </summary>
public static void ResumeCommands(float _elapsedTime)
{
// Resume
if (pausedOnStep)
{
if (pausedOnStep.attachedStepTimeline)
pausedOnStep.attachedStepTimeline.Resume(_elapsedTime);
else if (pausedOnStep.attachedController)
pausedOnStep.attachedController.Resume(_elapsedTime);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c2d2b5b3411b479e8bdc69e012664290
timeCreated: 1669563502

3
Scripts/Steps.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9cdcf7fa370f4df383a1da6982849f37
timeCreated: 1702505969

68
Scripts/Steps/GoToStep.cs Normal file
View File

@ -0,0 +1,68 @@
using TriInspector;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Step type which simply tells the timeline to immediately play another step (anywhere in the timeline). Should generally be paired with a step switch (with the GoToStep as a child option), in order to jump the timeline back or forward to a particular step based on a set condition
/// </summary>
public class GoToStep : Step
{
[Title("Next Step")]
[SerializeField] Step stepToJumpTo;
[Tooltip("Passes true to _overrideIfAlreadyPlaying on StepController, making it force end the current step, then play this one.")]
[SerializeField] bool forceStepPlayIfOnController;
/// <summary>
/// Play this step, which here means telling the timeline to immediately play another step
/// </summary>
public override void Play()
{
Debug.Log(name + " - Started");
isActive = true;
Continue();
}
/// <summary>
/// Complete this step by simply telling the timeline to play the set step
/// </summary>
public override void Continue()
{
Debug.Log(name + " - Completed");
isActive = false;
wasCompleted = true;
// If no step to "go to" defined, just go to next step in timeline
if (stepToJumpTo == null)
{
Debug.Log("stepToJumpTo was null on GoToStep, just proceeding to next step in the timeline.");
base.Continue();
return;
}
// Instruct timeline to play the requested step
if (attachedStepTimeline != null)
{
attachedStepTimeline.GoToStepAndPlay(stepToJumpTo);
}
else if (attachedController)
{
if (forceStepPlayIfOnController)
attachedController.Play(stepToJumpTo, true);
else
attachedController.Play(stepToJumpTo);
}
else
Debug.LogError("Tried to play a GoToStep without a Timeline or Controller attached, which isn't currently supported.");
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f5311fef721b4c1192ec1af3f02074e2
timeCreated: 1650379654

666
Scripts/Steps/Step.cs Normal file
View File

@ -0,0 +1,666 @@
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();
InitializeContinueConditions();
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();
InitializeContinueConditions();
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>
/// Initialize any reference needed for the continue condition
/// </summary>
void InitializeContinueConditions()
{
// Only need to initialize the external trigger
if (continueCondition == ContinueCondition.OnExternalTrigger)
{
// Assign this step to the trigger
if (externalContinueTrigger != null)
externalContinueTrigger.AddStep(this);
else
Debug.LogWarning("No external trigger assigned to step: " + name);
}
}
/// <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;
}
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");
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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c6f45d2c3c9d79459e201c582d8bb67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

353
Scripts/Steps/StepGroup.cs Normal file
View File

@ -0,0 +1,353 @@
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Events;
using TriInspector;
namespace R0bbie.Timeline
{
public class StepGroup : Step
{
// Exposed Variables
[Title("Events")]
public UnityEvent onGroupPlayEvent;
public UnityEvent onGroupCompleteEvent;
// Private Variables
List<Step> childSteps = new List<Step>();
int activeChildStepIndex;
// Properties
public Step activeChildStep { get; private set; }
public bool activeChildStepIsSwitch { get; private set; }
/// <summary>
/// Initialise the StepGroup, setting up required external refs, and getting refs to all child steps
/// </summary>
public override void Init(StepTimeline _stepTimeline)
{
// Setup necessary refs to parent timeline, and child steps (only 1 level deep to avoid getting children of StepSwitches etc)
attachedStepTimeline = _stepTimeline;
foreach (Transform t in transform)
{
if (t.GetComponent<Step>())
{
childSteps.Add(t.GetComponent<Step>());
}
}
// Init all child steps
foreach (var step in childSteps)
{
step.Init(attachedStepTimeline, this);
}
init = true;
}
/// <summary>
/// Initialise the StepGroup if StepController initialising
/// </summary>
public override void Init(StepController _controller)
{
// Setup necessary refs to parent Controller, and child steps (only 1 level deep to avoid getting children of StepSwitches etc)
attachedController = _controller;
foreach (Transform t in transform)
{
if (t.GetComponent<Step>())
{
childSteps.Add(t.GetComponent<Step>());
}
}
// Init all child steps
foreach (var step in childSteps)
{
step.Init(attachedController, this);
}
init = true;
}
/// <summary>
/// Play this StepGroup. Starting with the first step in it.
/// </summary>
public override void Play()
{
// Set the index to start at the first step
activeChildStepIndex = 0;
Debug.Log(name + " - Started");
wasCompleted = false;
isActive = true;
onGroupPlayEvent?.Invoke();
activeChildStep = childSteps[activeChildStepIndex];
activeChildStep.Play();
}
/// <summary>
/// Continue and play the next step in the group, unless we're at the end of the group
/// </summary>
public override void Continue()
{
activeChildStepIndex++;
// Check we've not reached the end of the step group, before playing the next step (if there is one)
if (activeChildStepIndex >= childSteps.Count)
{
GroupCompleted();
}
else
{
activeChildStep = childSteps[activeChildStepIndex];
if (activeChildStep is StepSwitch)
activeChildStepIsSwitch = true;
activeChildStep.Play();
}
}
/// <summary>
/// Skip this entire step group
/// </summary>
public override void Skip()
{
wasSkipped = true;
isActive = false;
// TODO: Check if currently active child step needs tidied up before skipping?
// TODO: Mark all steps within the step group not played before Skip was called as skipped also?
if (parentStepGroup)
{
parentStepGroup.Continue();
}
else if (parentStepSwitch)
{
parentStepSwitch.Continue();
}
else
{
if (attachedStepTimeline != null)
attachedStepTimeline.PlayNextStep();
else
attachedController.StepCompleted();
}
}
/// <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, as part of the group</param>
public void GoToStepAndPlay(Step _step)
{
// If group wasn't already active, we'll set it active now
if (!isActive)
{
isActive = true;
onGroupPlayEvent?.Invoke();
}
// If requested step has parent step groups above this one, make sure their status is set to active. Otherwise we're at the highest group in the hierarchy, so make sure this is active in the timeline
if (parentStepGroup)
parentStepGroup.SetActiveOnGoToChild(this);
else
attachedStepTimeline.SetSpecifiedHighestStepGroupActive(this);
// Set the index to the newly requested step
activeChildStepIndex = childSteps.IndexOf(_step);
// Set new active step, then play it
activeChildStep = childSteps[activeChildStepIndex];
activeChildStep.Play();
}
/// <summary>
/// When a child step is played using GoToStep, if that step was nested within multiple step groups, this function may be called by a child step group to tell this step group that it's now active
/// </summary>
private void SetActiveOnGoToChild(StepGroup _childStepGroup)
{
// If group wasn't already active, we'll set it active now
if (!isActive)
{
isActive = true;
onGroupPlayEvent?.Invoke();
}
// Set the index and active step on this group
activeChildStepIndex = childSteps.IndexOf(_childStepGroup);
activeChildStep = childSteps[activeChildStepIndex];
// Then set any other parent step groups active too (all the way up the hierarchy, till we make it to the timeline, then till timeline to mark highest level group as active)
if (parentStepGroup)
parentStepGroup.SetActiveOnGoToChild(this);
else
attachedStepTimeline.SetSpecifiedHighestStepGroupActive(this);
}
/// <summary>
/// Called externally to force this step to end, without automatically continuing
/// </summary>
public override void ForceEnd()
{
isActive = false;
if (activeChildStep)
activeChildStep.ForceEnd();
}
/// <summary>
/// Called when the last step in the group has completed
/// </summary>
private void GroupCompleted()
{
if (wasCompleted)
return;
wasCompleted = true;
isActive = false;
// We are currently having just one depth child, so it's likely that parentStepGroup & parentStepSwitch here won't ever play on a GroupCompleted call
// Tell the timeline to play the next step (or step group, or controller)
if (parentStepGroup)
{
parentStepGroup.Continue();
}
else if (parentStepSwitch)
{
parentStepSwitch.Continue();
}
else
{
if (attachedStepTimeline != null)
attachedStepTimeline.PlayNextStep();
else
attachedController.StepCompleted();
}
// Play the event at the end to make sure all the functionality and bools have been disabled
onGroupCompleteEvent?.Invoke();
}
#if UNITY_EDITOR
// ODIN INSPECTOR BUTTONS
[Title("Add to Group")]
[Button("Add Step")]
private void Editor_AddStep()
{
// Create new GameObject, and reparent it as a child of the StepGroup parent object
GameObject stepGo = new GameObject();
stepGo.transform.parent = transform;
// Add Step component to this new GameObject
Step step = stepGo.AddComponent<Step>();
// Rename object
int parentStepNo = transform.parent.GetSiblingIndex() + 1;
int childStepNo = step.transform.GetSiblingIndex() + 1;
step.gameObject.name = "#" + parentStepNo + "." + childStepNo + " - *ADD STEP NAME*";
}
[Button("Add Step Switch")]
private void Editor_AddStepSwitch()
{
// Create new GameObject, and reparent it as a child of the StepGroup 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 as child of StepGroup, now remember to add the specific StepSwitch type you want to it.");
// Rename object
int parentStepNo = transform.parent.GetSiblingIndex() + 1;
int childStepNo = stepSwitchGo.transform.GetSiblingIndex() + 1;
stepSwitchGo.gameObject.name = "#" + parentStepNo + "." + childStepNo + " - 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
int parentStepNo = transform.parent.GetSiblingIndex() + 1;
int childStepNo = step.transform.GetSiblingIndex() + 1;
step.gameObject.name = "#" + parentStepNo + "." + childStepNo + " - 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
int parentStepNo = transform.parent.GetSiblingIndex() + 1;
int childStepNo = step.transform.GetSiblingIndex() + 1;
gotoStepGo.gameObject.name = "#" + parentStepNo + "." + childStepNo + " - GOTO - *ADD STEP NAME*";
}
[Title("Editor Functions")]
[Button("Rename Child Steps")]
private void Editor_RenameChildren()
{
RenameChildren();
}
// OTHER EDITOR HELPER FUNCTIONS
public void RenameChildren()
{
foreach (Transform child in transform)
{
if (child.GetComponent<Step>())
child.GetComponent<Step>().UpdateStepName();
}
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2b7cdfaad11143a4ae986711f8923e15
timeCreated: 1647378293

124
Scripts/Steps/StepLooper.cs Normal file
View File

@ -0,0 +1,124 @@
using System.Collections;
using System.Collections.Generic;
using System;
using System.Linq;
using UnityEngine;
using TriInspector;
namespace R0bbie.Timeline
{
/// <summary>
/// Simple step type which loops back to a previous step, until a set condition is met
/// </summary>
public class StepLooper : Step
{
[Title("Loop Setup")]
[SerializeField] Step loopBackToStep;
enum LoopCondition
{
NumberLoops,
IfStepPlayed,
IfCommandCompleted,
IfTriggerTriggered
}
[SerializeField] LoopCondition loopUntil;
[ShowIf(nameof(loopUntil), LoopCondition.NumberLoops)]
[SerializeField] int loopForIterations;
[ShowIf(nameof(loopUntil), LoopCondition.IfStepPlayed)]
[SerializeField] Step stepToCheck;
[ShowIf(nameof(loopUntil), LoopCondition.IfCommandCompleted)]
[SerializeField] StepCmd commandToCheck;
[ShowIf(nameof(loopUntil), LoopCondition.IfTriggerTriggered)]
[SerializeField] TimelineTrigger triggerToCheck;
int loopCounter = 0;
/// <summary>
/// Play this step, which here means triggering the loop back to a previous step
/// </summary>
public override void Play()
{
Debug.Log(name + " - Started");
isActive = true;
Continue();
}
/// <summary>
/// Continue (either looping back to a previous step, or playing ahead to next in timeline if loop has been broken)
/// </summary>
public override void Continue()
{
bool breakConditionReached = false;
// Increase loop counter
loopCounter++;
// First check if this loop has reached its break condition
switch (loopUntil)
{
case LoopCondition.NumberLoops:
if (loopCounter >= loopForIterations)
breakConditionReached = true;
break;
case LoopCondition.IfStepPlayed:
if (stepToCheck.wasCompleted)
breakConditionReached = true;
break;
case LoopCondition.IfCommandCompleted:
if (commandToCheck.completed)
breakConditionReached = true;
break;
case LoopCondition.IfTriggerTriggered:
if (triggerToCheck.triggered)
breakConditionReached = true;
break;
}
if (breakConditionReached)
{
Debug.Log(name + " - Completed");
// Play the next step after this looper in the timeline
if (parentStepGroup != null)
parentStepGroup.Continue();
else
attachedStepTimeline.PlayNextStep();
isActive = false;
wasCompleted = true;
return;
}
// Otherwise, loop again!
// Play the step we're looping back to
if (attachedStepTimeline != null)
attachedStepTimeline.GoToStepAndPlay(loopBackToStep);
else
Debug.LogError("Tried to play a StepLooper with a StepController, which isn't currently compatible. Must be a child of a Timeline to function.");
isActive = false;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9bd2a69d7f1f4b6eba035850624bc3b5
timeCreated: 1648763328

3
Scripts/Triggers.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 24897a938f0446319ef84b7c2243edfa
timeCreated: 1647379464

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using UnityEngine;
namespace R0bbie.Timeline
{
/// <summary>
/// Generic TimelineTrigger which simply calls TriggerContinue on all attached steps
/// </summary>
public class TimelineTrigger : MonoBehaviour
{
[SerializeField] string description;
// Private variables
List<Step> triggerContinueOnSteps = new List<Step>();
// Properties
public bool triggered { get; protected set; }
public void AddStep(Step _stepToTrigger)
{
// Only add step to the list if it's not already in it..
if (triggerContinueOnSteps.Contains(_stepToTrigger))
return;
// Add step to list
triggerContinueOnSteps.Add(_stepToTrigger);
}
///
public void Trigger()
{
triggered = true;
// Add message to trigger continue on all attached steps
foreach (Step triggerContinueOnStep in triggerContinueOnSteps)
{
triggerContinueOnStep.TriggerContinue(this);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f2d0bd9c3ad049298ed7c9d7f5b6de22
timeCreated: 1647378303

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "com.r0bbie.timeline",
"displayName": "Step Timeline",
"version": "0.2.0",
"unity": "2021.3",
"author": {
"name" : "Robbie Cargill",
"email" : "contact@robbiecargill.com",
"url" : "https://r0bbie.com"
},
"description": "Timeline utility, allows a serious of steps to be setup in a scene, each with attached commands.",
"keywords": [
"timeline",
"steps",
"step",
"commands"
],
"dependencies": {
"com.codewriter.triinspector": "1.13.2"
}
}

7
package.json.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c4b8106b70c204846bcdbd9b66ed06ce
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: