Using custom PlayableBehaviours to create an event mechanism for Unity's Timeline
Introduction
As a one-team indie, Timeline has revolutionized how I use Unity. It was released in 2017, and having been an Adobe Flash developer for several years at the start of my career, I could instantly see the appeal.
I started out by introducing a couple of timelines in my project and quickly found so many useful patterns and efficiencies that I refactored much of my codebase to take advantage of it (you can see the results in my Oculus and HTC Vive title “The Incredible VR Gameshow").
Code and asset described in this post
I've released a drop-in asset, including example scene in the Unity asset store, which is currently under review.
The source code can be found here.
In addition, here's a video walkthrough of what we will achieve:
What problems does Timeline solve for me
I utilize a lot of enum states in my code — I find it highly readable for myself, and others (which includes my future self ). I often have code like this:
public void SetState(RobotState state)
{
currentState = state;
switch (state)
{
case RobotState.Sleeping:
Agent.speed = 0f;
NavMeshFollower.Target = null;
break;
case RobotState.GoToWork:
NavMeshFollower.Target = Work;
Agent.speed = 3;
break;
case RobotState.GoHome:
NavMeshFollower.Target = Home;
Agent.speed = 6f;
break;
default:
throw new ArgumentOutOfRangeException("state", state, null);
}
}
While I’m happy with this style of code, it has some drawbacks:
- Designers can’t easily change the behavior
- It can be tedious to make changes
- Timing becomes especially hard, particularly making effects and activation execute in concert with game logic
- You need to run often to verify changes
Timelines solve all of these problems. I have since refactored my games to make use of timelines in many situations and have built a library of support classes and patterns, which will be the subject of a future blog post.
Timeline limitations
However, Timelines do not, as of Unity 2018.1 have any events(!). This is problematic as there are many situations where we want to:
- Know when a timeline animation or clip has ended,
- Want to call some method, synchronized with the timeline’s position (such as marking an enemy for respawn, assigning a score, updating a UI, playing a sound, etc),
- Have some mechanisms that can’t be triggered by simple gameObject activations (this is a technique many people use to overcome the lack of timeline events).
For example, I make use of the AudtioToolkit for playing all of my game’s sounds, and have custom methods to make the main character in my game narrate things. Both of those mechanisms use enum based abstractions like the snippet I shared above.
Overcoming these limitations
There are several common approaches to improving our game-logic/timeline integration/orchestration:
Writing a custom timeline track, and clips.
This is the recommended approach. Unity has provided an asset store asset, which contains a wizard to rapidly create tracks, and has many examples of custom timeline tracks, which do things like:
- Adjust lighting
- Control Navigation agents
- Perform tweens
- etc
Setting a gameObject as active, which will execute script
This is a real sledgehammer-to-crack-a-nut solution, in my opinion. It requires the creation of more gameObjects, and ever more constrained scopes for behaviors. In my case, I’d have to make individual activation tracks and clips like:
- MakeMainCharacterNarrate
- PlayLevelSound
- PlayGenericSound
- TeleportPlayerToPosition
- ToggleVRFade
This would not only be a fair amount of identical boilerplate code; but would end up making the timeline illegible, and difficult to work with.
Executing arbitrary code
This the holy grail of timeline solutions: if we’re able to execute any method we want, on any gameObject, with any parameters, then we have a complete timeline solution that has many benefits, not limited to:
- Less code to support (no specialized tracks/clips for each type of activity)
- Easy-to-read Timelines
- Designers can hook into and even drive the game flow, and orchestrate events that are usually the domain of the programmer
- Can create highly orchestrated interactions
- Can leverage timeline for more than just cutscenes (I now drive a couple of my mini-games main characters with this Timeline-Orientated-Design approach)
- Create less interdependance between designers and programmers
However, Unity’s event system is not yet released, and will not likely make the cut until Unity 2018.3, which could be several months away.
Implementing a timeline event system
I scoured the web looking for info on custom Timeline tracks, and looking for clues about executing arbitrary methods on my behaviors. I was fortunate to find this thread, and specifically a code example by Marius George from https://twitter.com/infingame.
I quickly set out to work on my own system — I had the following requirements:
- Have code executed, whenever a timeline clip is entered
- Execute methods with zero or one parameters
- Have support for basic types, string, int, float, bool
- Have support for enums — this was particularly important to me
I looked at existing solutions for this, but none of them quite had the feature-set, so I went about writing my own.
Custom Timelines 101
Making any timeline track requires the creation of the following classes:
- Clip
- Track
- PlayableBehaviour (often referred to as the BehaviourMixer)
- Behaviour
- Editor
Unity has provided a wizard to assist in the creation of these classes, which is bundled with their default playable asset, which provides useful examples and reference code, and there’s plenty of reference videos, such as this.
(At time of writing) the documentation for extending your own timeline behaviors is still a bit fragmented. The following quote from the Unity forums (by Unity employee seant_unity) is one of the most succinct descriptions I’ve yet found:
Timeline is authored using Scriptable Objects (TimelineAsset, TrackAssets and PlayableAssets). They act as assets.
These get compiled into a PlayableGraph when an instance of the timeline is needed. This allows one asset to be have several instances running. Using the Playable system means an instance is more performant, uses less memory and is compatible with other systems (Animation, Audio, with Video and others on the way).
The PlayableAsset represents custom data for a timeline clip. The Playable Behaviour is an instance of the clip.
A TrackAsset represents the track. A mixer is a PlayableBehaviour that represents the instance of the track, and is only needed if you need to define how clips are blended or you are writing a final result to a binding, such as a component.
For example, for a custom track that writes text on a screen would have
A PlayableAsset that defines and serializes what data is needed for a clip (e.g. a string and a color)
A PlayableBehaviour that is an instance of that data, and possibly does any modification of that data required at runtime. (e.g. does a localization translate)
A custom TrackAsset. It would be the container for clips of that data, and defines
- It holds clips that support text
- What mixer to use
- What the track binds to (e.g. a UI Text component)
A Mixer (PlayableBehaviour) that determines how those clips should blend. For example, the colours could blend, and the text could fade. It will also write the the final result to the text component.
My implementation
After watching videos such as the one linked above, messing with the wizards, and looking at a few reference implementations, I was ready to get started. I implemented the following classes as such:
TimelineEventDrawer
The inspectable properties of the clip are displayed via Unity PropertyDrawer
mechanism, so I started by extending PropertyDrawer
and implementing the OnGuiMethod
, with the following behavior:
- Add a flag to indicate if code would be executed at runtime/in the editor
- Add a flag to indicate if we’re looking for methods with/without a parameter
- Pretty print the list of methods for the TrackTargetObject in a combo box
- Provide a basic editor for the parameter value
- Show enum parameter types in a drop down, if the parameter is an enum
- Validate the input
- Serialize/deserialize the value to a string, to make it easy/flexible to work with
TimelineEventBehaviour
This was the PlayableBehaviour
implementation. I did the following:
- Add a means of storing and caching the method invocation info, to improve performance. I did this by creating the class
MethodInvocation
, which encapsulates the invocation details for the desired target method, and is responsible for executing when the event is fired - Ascertaining if the current MethoodInvocation is still valid (the method being invoked might’ve been changed by the programmer/designer), by checking the
HandlerKey
- Deserializing the string parameter value and passing it to the invocation when the timeline is entered.
TimelineEventClip
This the PlayableAsset
and ITimelineClipAsset
implementation, in this class I:
Pass on the TrackTargetObject property (which is the object on which methods will be invoked), in the CreatePlayable
override.
TrackMixer
I originally had a clip start/end time which I’d monitor in the mixer. This was to overcome a known issue with the OnBehaviourPlay
method of BasicaPlayableBehaviour
not being invoked at the exact start of a clip (due to dropped frames on certain devices, like mobile). However, I’ve opted to omit that functionality to simplify the code, and because I haven't had a use case that would make it a deal-breaker. If you can think of a good reason to change it back, then please raise an issue in the GitHub issue tracker.
The TrackMixer now lays conspicuously bare, and will likely be removed
TimelineEventTrack
This is the TrackAsset
implementation. My implementation:
- Sets the track colors
- Passes on the TrackTargetObject to the clips, so they can look up methods in the property drawer and invoke them
A note about TrackTargetObject
In order to lookup methods at edit time, and then invoke them at runtime, we need an object on which to invoke them. Our clips are actually ScriptableObjects, which mean they are not in the context of our scene/gameObjects.
In fact, this is the source of much confusion regarding scripting Timeline PlayableAssets, as most developers (myself included) expect them to use a similar and contextually compatible paradigm. These limitations are why we can’t just put a UnityEvent
in our EditorDrawer.
We can include a gameObject reference in each of our clips, but I found this cumbersome and undesirable after several days of using it in this way. Fortunately, the TrackAsset provides a mechanism with which we can get access to a gameObject passed into the track as a whole.
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
var director = go.GetComponent<PlayableDirector>();
var trackTargetObject = director.GetGenericBinding(this) as GameObject;
...
This is the mechanism I now use to pass a gameObject into the entire track, I find this beneficial for many reasons:
- Most often, I find myself invoking an event on the same object repeatedly, such as the gameObject that is conceptually associated with a timeline (i.e. a player/enemy/UIManager)
- I don’t have to assign/reassign constantly as I edit clips
- It improves readability — in each of my timelines I can identify what the events pertain to with just a glance.
So how does it look?
The YouTube video at the top of this article talks through how the finished mechanism functions, but I'll briefly go over it here as well:
Timeline event tracks
A lot of my timelines now have these tracks at the top (often times grouped). The number of tracks and events varies depending on the complexity of the timeline and the amount of code integration required, but I find it immediately readable. I actually prefer it to animation events, as it goes, which I find somehwat cryptic and fiddly to visually parse.
Editing details for an event
Event editing, is (IMHO) straight forward. Simply create a new event clip and then select the method from the drop down. If the method has paremeters, check the box and enter the value (float, bool, int, string), or select the enum value.
Putting it all together
Here we have a very simple timeline, which demonstrates how I am orchestrating code execution with event clips. The clips are passing parameters such as scores and enum states, which in turn drive all manners of other functionality (such as setting NavMeshAgent targets, and other logic).
Where to go from here: explore Timeline-orientated-design
- Download the code from GitHub, or get it from the Asset Store (coming soon!)
- Look out for my next blog post, in which I will dive into the source of my new game release ("The Incredible VR Gameshow") to further explore the patterns, utility classes, and tools I've devised to leverage Timeline-orienteated-design in my games
- Follow my profile for more posts
- Get in touch if you want some advice/direction/expert insight on your Unity3d project!
Thanks for reading, now get orchestrating!
Nice work George. I have tried it today and I love it. A few things I notice and would like to see if it can be fixed.
Cant wait to see this great work on AssetStore.