As promised, here’s the context & workarounds (because I found almost nothing when I searched for this stuff).
Context
Many things in a game involve co-ordinating the animation of properties across arbitrary sets of objects - from characters opening doors, to attack animations, to environmental object effects - controlling / syncing animation, Vfx, audio, collider enabling etc etc. all of which is very set-up heavy & often involve custom editor tooling etc.
The Timeline package does all of the above, and has a mature editor integration so seemed a logical choice for the team early in pre-production, and the testing the pre-production team did didn’t show horrendous performance hits (on PC…) so the game went on to makes very heavy use of Timeline for core gameplay interactions throughout its development.
I’m currently working on a multiplatform title targeting PC / Mac / Steam Deck and all current gen consoles.
The game runs at a very solid 60fps on PC, but when we hit beta and started the serious process of profiling and optimising on some of the less computationally powerful hardware platforms we found that Timeline had some very unfortunate characteristics which were seriously affecting framerate stability.
PlayableDirector.Play() was costing as much as 20ms (on the first call, subsequent calls were cheaper but still typically 1-2ms. This meant that:
PlayableDirector.Play() (the 1st time each PlayableDirector is used) is a guaranteed frame drop (1 frame @60fps : 16.66…ms @30fps : 33.33…ms)
- even the lower 1-2ms cost of re-
Play()ing is a pretty big problem - especially since we often have 2 or 3 calls on the same frame in active gameplay
Attempted Workaround: Manage Our Own Playable
Initially I assumed it would be easy to just use TimelineAsset.CreatePlayable() and manage our own PlayableGraph.
It turns out that this is actually non-trivial:
- partly because the game uses custom timeline tracks, some of which rely on there being a
PlayableDirector…
- …but also because the playables API is

I spent a morning trying to work out how to use the binding data on a PlayableDirector instance to set up the outputs of a PlayableGraph (created using playableDirector.PlayableAsset.CreatePlayable()) and gave up.
I’m sure it’s completely doable, but I couldn’t find any sample code which explained how to do it, or find anything in the documentation which shed much light on it either, and because the objects in a PlayableGraph arre all handles I found that my usual approach of using the debugger to try to make sense of the data at runtime didn’t work, so because time pressure I decided to try something else.
Actual Workaround: Abuse the PlayableDirector API to prevent it destroying its PlayableGraph
On a hunch, I tried setting the PlayableDirector wrap mode to hold and discovered that this prevents it ever properly stopping and therefore it doesn’t destroy or recreate its PlayableGraph.
There was another fly in the ointment though, even with the wrap mode set to hold, if the parent GameObject of a PlayableDirector is disabled then this also causes the PlayableGraph to be destroyed; the way the game manages much of the behaviour of the objects using Timeline is to (en/dis)able them so this needed an additional layer of workaround…
The final workaround for was to clone all PlayableDirector instances in the game, and cache them in an area of the hierarchy which is never disabled.
For each PlayableDirector used in active gameplay:
- create a new
PlayableDirector…
- …copy all the generic bindings from the original…
- …for tracks which use other binding approaches (e.g.
ControlTrack) manually iterate the original’s asset bindings and copy them over
- prevent the cost on initial
Play() by using RebuildGraph()
- avoid the automatic destruction a
PlayableDirector.playableGraph by
- setting
playableDirector.extrapolationMode = DirectorWrapMode.Hold (which prevents the timeline stopping automatically)
- check for the
Timeline ending with Time vs. Duration of the timeline Playable and use this to send an event equivalent to PlayableDirector.stopped
- instead of
Stop() call Pause() then reset PlayableDirector.time
Summary
Several days and a lot of stress would all have been saved if PlayableDirector had an option to reuse its PlayableGraph rather than destroy / recreate it constantly.