Skip to content

Instantly share code, notes, and snippets.

@darbotron
Last active December 16, 2024 10:10
Show Gist options
  • Save darbotron/37386f74a9a45c5efc088a1dfd9adbcb to your computer and use it in GitHub Desktop.
Save darbotron/37386f74a9a45c5efc088a1dfd9adbcb to your computer and use it in GitHub Desktop.
How to create a PlayableGraph from a TimelineAsset whilst copying over all the bindings from a PlayableDirector
///////////////////////////////////////////////////////////////////////////////
// License: https://opensource.org/licenses/unlicense
//
// TL;DR:
// 1) you may do what you like with it...
// 2) ...except blame me for any consequence of acting on rule 1)
//
///////////////////////////////////////////////////////////////////////////////
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Audio;
using UnityEngine.Playables;
using UnityEngine.Pool;
using UnityEngine.Timeline;
//////////////////////////////////////////////////////////////////////////////
//
// see these unity discussions topics for context:
// https://discussions.unity.com/t/simple-suggestion-to-prevent-performance-issues-caused-by-timeline-playabledirector/1569312
// https://discussions.unity.com/t/when-using-timelineasset-createplayable-to-make-a-playablegraph-playablegraph-getresolver-is-always-null-how-do-we-create-a-resolver-for-it/1569623
//
// Add an instance of this to a GameObject with a PlaybleDirector
//
// It will create a PlayableGraph from the PlaybleDirector's Timeline
// and copy all the binding data from the PlayableDirector into it so that you
// can just use the PlaybleGraph instead of the PlayableDirector.
//
// This is SUPER handy, since the default behaviour of PlayableDirector is to:
// 1. create a new PlayableGraph when you call Play() and
// 2. destroy it when either: you call Stop(), playback ends naturally, or the
// GameObject owning the PlayableDirector is disabled
//
// Creating a PlayableGraph can be very expensive - up to 20ms on the least
// powerful current gen console platform when I'm typing this...
//
// This is fine if you're using it for a cinematic, but if you're using it for
// active gameplay reasons then 20ms for calling PlayableDirector.Play() is
// "somewhat problematic".
//
// Known limitations:
// * any timeline track which uses ExposedReference< T > on its timeline clips
// (like ControlTrack) will need special handling in initialisation to copy
// all of the property bindings over to the cloned Timeline in
// (see: CreatePlayableGraphExposedReferenceResolver() )
//////////////////////////////////////////////////////////////////////////////
public class CloneTimelineToPlayableGraph : MonoBehaviour
{
//------------------------------------------------------------------------
private void Awake()
{
m_director = GetComponent< PlayableDirector >();
m_director.enabled = false;
var playbleGraphGo = new GameObject( "customPlayable" );
m_playableGraph = CreateStandalonePlayableGraphWithBindingsFromDirector( m_director, playbleGraphGo, CreatePlayableGraphExposedReferenceResolver( m_director ) );
LogTimelineSummary( m_playableGraph );
m_timelineAsPlayable = m_playableGraph.GetRootPlayable( 0 );
Debug.Assert( m_timelineAsPlayable.GetPlayableType() == typeof( TimelinePlayable ) );
}
//------------------------------------------------------------------------
private void Update()
{
void Stop()
{
m_playableGraph.Stop();
m_timelineAsPlayable.SetTime( 0f );
m_playableGraph.Evaluate( 0f );
}
var spacePressed = Input.GetKeyDown( KeyCode.Space );
if( m_playableGraph.IsPlaying() )
{
if( spacePressed
|| ( m_timelineAsPlayable.GetTime() == m_timelineAsPlayable.GetDuration() ) )
{
Stop();
}
}
else if( spacePressed )
{
m_playableGraph.Play();
}
}
//------------------------------------------------------------------------
private static PlayableGraphExposedReferenceResolver CreatePlayableGraphExposedReferenceResolver( PlayableDirector directorOwningTimelineAsset )
{
var exposedReferenceResolver = new PlayableGraphExposedReferenceResolver();
foreach( var timelineOutput in directorOwningTimelineAsset.playableAsset.outputs )
{
var timelineTrackAsset = timelineOutput.sourceObject;
if( timelineTrackAsset is ControlTrack controlTrack )
{
if( null != controlTrack )
{
foreach( var timelineClip in controlTrack.GetClips() )
{
var clipAsset = timelineClip.asset as ControlPlayableAsset;
var controlClipBinding = directorOwningTimelineAsset.GetReferenceValue( clipAsset.sourceGameObject.exposedName, out var isValid );
if( isValid )
{
exposedReferenceResolver.SetReferenceValue( clipAsset.sourceGameObject.exposedName, controlClipBinding );
}
}
}
}
}
return exposedReferenceResolver;
}
//------------------------------------------------------------------------
public static PlayableGraph CreateStandalonePlayableGraphWithBindingsFromDirector( PlayableDirector director, GameObject graphOwner, IExposedPropertyTable exposedReferenceResolver )
{
var playableGraph = PlayableGraph.Create( $"{graphOwner.name}-Standalone-{director.playableAsset.name}" );
playableGraph.SetResolver( exposedReferenceResolver );
director.playableAsset.CreatePlayable( playableGraph, graphOwner );
using( DictionaryPool< int, PlayableOutput >.Get( out var tempDictTrackInstanceIDToPlayableOutput ) )
{
int graphOutputCount = playableGraph.GetOutputCount();
for( int outputIndex = 0; outputIndex < graphOutputCount; ++outputIndex )
{
var output = playableGraph.GetOutput( outputIndex );
if( output.IsOutputValid() )
{
var timelineTrack = output.GetReferenceObject();
if( timelineTrack && ( timelineTrack is TrackAsset trackAsset ) )
{
tempDictTrackInstanceIDToPlayableOutput.Add( trackAsset.GetInstanceID(), output );
}
}
}
foreach( var timelineOutput in director.playableAsset.outputs )
{
//
// Context for this code:
// * in a timeline asset, timelineOutput.sourceObject is an object derived from TrackAsset
// * when the asset is loaded, each TrackAsset (like all Unity.Object) is created with an instance ID
// * when a PlayableGraph is created from a TimelineAsset it references these _same_ TrackAssets
// * we can use the instanceID to match the timelineOutput to the PlayableGraph outputs
//
var timelineTrackAsset = timelineOutput.sourceObject;
if( timelineTrackAsset == null )
{
Debug.Log( $"sourceDirectorOutput {timelineOutput.streamName} ({timelineOutput.outputTargetType}) has no track asset" );
}
var outputTrackBoundObject = director.GetGenericBinding( timelineTrackAsset );
if( outputTrackBoundObject == null )
{
Debug.Log( $"no generic binding set for sourceDirectorOutput {timelineOutput.streamName} ({timelineOutput.outputTargetType})" );
}
Debug.Log
(
$"cloning generic binding on {timelineOutput.streamName} ({timelineOutput.outputTargetType}): " +
$"{( ( timelineOutput.sourceObject != null ) ? $"{timelineTrackAsset.name}({timelineOutput.sourceObject.GetInstanceID()})" : "null" )} -> " + $"{( ( outputTrackBoundObject != null ) ? outputTrackBoundObject.name : "null" )}"
);
if( timelineTrackAsset
&& outputTrackBoundObject )
{
if( tempDictTrackInstanceIDToPlayableOutput.TryGetValue( timelineTrackAsset.GetInstanceID(), out var graphOutput ) )
{
if( graphOutput.GetPlayableOutputType() == typeof(AnimationPlayableOutput) )
{
Debug.Assert( outputTrackBoundObject is Animator );
( (AnimationPlayableOutput)graphOutput ).SetTarget( outputTrackBoundObject as Animator );
}
else if( graphOutput.GetPlayableOutputType() == typeof(AudioPlayableOutput) )
{
Debug.Assert( outputTrackBoundObject is AudioSource );
( (AudioPlayableOutput)graphOutput ).SetTarget( outputTrackBoundObject as AudioSource );
}
else
{
graphOutput.SetUserData( outputTrackBoundObject );
}
}
else
{
Debug.Log( $"timelineTrackAsset: {timelineTrackAsset.name}({timelineTrackAsset.GetInstanceID()}) for output not found in TimelineAsset" );
}
}
}
}
return playableGraph;
}
private PlayableDirector m_director;
private PlayableGraph m_playableGraph;
private Playable m_timelineAsPlayable;
//------------------------------------------------------------------------
public static void LogTimelineSummary( PlayableGraph timelineGraph )
{
// using type names so framework doesn't rely on the Timeline package...
const string k_TypeName_TimlinePlayable = "TimelinePlayable";
const string k_TypeName_ControlTrack = "ControlTrack";
// still needs to compile if the Timeline
if( timelineGraph.GetRootPlayable( 0 ).GetPlayableType().Name != k_TypeName_TimlinePlayable )
{
Debug.LogError( $"{nameof(timelineGraph)}: {timelineGraph.GetEditorName()} doesn't appear to have been created by a TimelineAsset" );
return;
}
Debug.Log( $"Logging info for PlayableGraph: {timelineGraph.GetEditorName()}" );
int graphOutputCount = timelineGraph.GetOutputCount();
for( int outputIndex = 0; outputIndex < graphOutputCount; ++outputIndex )
{
var output = timelineGraph.GetOutput( outputIndex );
Debug.Assert( output.IsOutputValid() );
var outputTrack = output.GetReferenceObject();
var outputPlayable = output.GetSourcePlayable();
if( outputTrack == null )
{
Debug.Log( $"output[{outputIndex}]: null output track - {outputPlayable.GetPlayableType()}" );
}
else if( output.GetPlayableOutputType() == typeof(AnimationPlayableOutput) )
{
var target = ( (AnimationPlayableOutput)output ).GetTarget();
Debug.Log( $"output[{outputIndex}]: {outputTrack.name} ({outputTrack.GetType().Name}({outputTrack.GetInstanceID()}))) - bound to: {( ( (bool)target ) ? target.name : "null" )}" );
}
else if( output.GetPlayableOutputType() == typeof(AudioPlayableOutput) )
{
var target = ( (AudioPlayableOutput)output ).GetTarget();
Debug.Log( $"output[{outputIndex}]: {outputTrack.name} ({outputTrack.GetType().Name}({outputTrack.GetInstanceID()}))) - bound to: {( ( (bool)target ) ? target.name : "null" )}" );
}
else
{
Debug.Assert( ( output.GetPlayableOutputType() == typeof(ScriptPlayableOutput) ) );
var binding = output.GetUserData();
var notes = string.Empty;
if( outputTrack.GetType().Name == k_TypeName_ControlTrack )
{
notes = "(each ControlTrack clip has its own binding; the track has none)";
}
Debug.Log( $"output[{outputIndex}]: {outputTrack.name} ({outputTrack.GetType().Name}({outputTrack.GetInstanceID()}))) - bound to: {( ( (bool)binding ) ? binding.name : "null" )} {notes}" );
}
}
}
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
public class PlayableGraphExposedReferenceResolver : IExposedPropertyTable
{
private Dictionary< PropertyName, Object > m_propertyTable = new Dictionary< PropertyName, Object >( 256 );
public void SetReferenceValue( PropertyName id, Object value ) => m_propertyTable[ id ] = value;
public Object GetReferenceValue( PropertyName id, out bool idValid )
{
idValid = m_propertyTable.TryGetValue( id, out var value );
return idValid ? value : null;
}
public void ClearReferenceValue( PropertyName id ) => m_propertyTable.Remove( id );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment