Managing PlayableAssets at runtime (maintain dictionary?)

I’ve got one main PlaybableDirector i’m using to handle dialogue for my game. That means upwards of 100 different PlayableAssets will need to be loadable at runtime for any given scene. My question is, what’s the best way to load a playable asset at runtime to swap in.

Here’s the only idea I’ve had so far. I have an object named after the scene and then an editor script as follows that monitors my timeline folder for changes re-adding all the timeline assets to a timeline asset registry (dictionary) that sits on an object named after the scene that exists in every scene.

namespace EditorMods
{
    /// <summary>
    /// Scans for new timelines.
    /// </summary>
    public class TimelineAssetScanner : AssetPostprocessor
    {
        private const string TimelineDirectory = "Assets/Timeline/";
        static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {
            // Look for changes to active timeline directory
            string currentScene = SceneManager.GetActiveScene().name;
            string timelineDirectory = TimelineDirectory + currentScene;
            bool timelinesChanged = false;
            foreach (string assetName in importedAssets)
            {
                if (assetName.Contains(timelineDirectory))
                {
                    timelinesChanged = true;
                    break;
                }
            }
            if (!timelinesChanged)
            {
                foreach (string assetName in deletedAssets)
                {
                    if (assetName.Contains(timelineDirectory))
                    {
                        timelinesChanged = true;
                        break;
                    }
                }
            }
            if (!timelinesChanged)
            {
                for (int i = 0; i < movedAssets.Length; i++)
                {
                    if (movedAssets[i].Contains(timelineDirectory) || movedFromAssetPaths[i].Contains(timelineDirectory))
                    {
                        timelinesChanged = true;
                        break;
                    }
                }
            }

            // Update timeline registry
            if (timelinesChanged)
            {
                GameObject currentSceneObject = GameObject.Find(currentScene);
                TimelineAssetRegistry registry = currentSceneObject.GetComponent<TimelineAssetRegistry>();
                if (currentSceneObject != null)
                {
                    registry.playableAssetList.Clear();
                    string[] assetGuids = AssetDatabase.FindAssets(null, new[] { timelineDirectory });
                    foreach (string guid in assetGuids)
                    {
                        string path = AssetDatabase.GUIDToAssetPath(guid);
                        UnityEngine.Object assetObject = AssetDatabase.LoadAssetAtPath(path, typeof(PlayableAsset));
                        if (assetObject != null) registry.playableAssetList.Add(assetObject as PlayableAsset);
                    }
                }
            }
        }
    }
}


namespace Timeline
{
    /// <summary>
    /// Registry of all playable assets for this scene.
    /// </summary>
    public class TimelineAssetRegistry : MonoBehaviour
    {
        // References
        public List<PlayableAsset> playableAssetList;

        // Properties
        private Dictionary<string, PlayableAsset> assetRegistry;

        void Awake()
        {
            assetRegistry = new Dictionary<string, PlayableAsset>();
            foreach (PlayableAsset asset in playableAssetList)
            {
                assetRegistry[asset.name] = asset;
            }
        }

        /// <summary>
        /// Fetch a timeline from the registry.
        /// </summary>
        /// <param name="timelineName">Name of the timeline.</param>
        /// <returns>A timeline.</returns>
        public PlayableAsset GetTimeline(string timelineName)
        {
            if (!assetRegistry.ContainsKey(timelineName))
            {
                throw new System.Exception(timelineName + " missing from registry!");
            }
            return assetRegistry[timelineName];
        }
    }
}

Are you trying to only load the ones you need? Referencing a timeline from the scene in any way will cause to get packed with the scene. For 100s of timelines that might not be ideal.

1 Like

So i’m only loading every timeline used for the scene. Some scenes might have over 100 different dialogue options. I wasn’t sure how else to handle it.

Yeah, it’s a bit tricky. There are several options, each of which has it’s pros and cons.

One thing to note is if you are authoring your timelines with a single playable director in a scene and swapping out your timelines, it will remember the bindings and store references to all the timelines that have been used, meaning it will actually build the scene with all the timelines embedded. This is not obvious behavior, but if you use the debug inspector, you will see that the playable directors binding list will reference all the tracks from all timelines it has authored.

Timelines are assets, so any scene references to them (including their tracks) will cause them to load.

If you are only hoping to load a small number of possible timelines, this is something to watch out for.

You can make each timeline a prefab, but that means either the prefab has to also contains all the objects the timeline is bound to. That may or may not work. Alternately you can create some sort of binding scheme and set the bindings via script after the prefab is loaded. Something like making the track name the path in the scene.

You should be able to load the prefab timelines as resources. Asset bundles could also work.

Or you could auto-generate scenes for each timeline and load them as additive scenes as well. Which is similar to the prefabs above.

This is an issue that quite a few devs have now faced, and hopefully someone will share their experience and what works best.

I hope that helps.

2 Likes

Very interesting, thanks for all the advice!

One more question then. Are timelines expensive to load if they’re short? Mine average about 5 tracks and are only 3-10 seconds. Probably about 50-500 (very conservative estimate) per scene. Does that seem like it would bog down a simple 2D adventure game?

The timeline itself isn’t too bad, but it does reference other assets (animation clips, audio) that can add up pretty quickly. So it really depends on the contents of the timeline.

I would imagine that 50-500 would have a noticeable impact on the load time of the scene. The best thing to do is measure on the player with and without the timelines.

Ah OK so I guess this will limit my ability to use timelines for dialogue unless I come up with some scheme for storing timelines in prefabs?

Something like:

Game Object 1 Prefab:
timeline 1
timeline 2
timeline 3

Game Object 2 Prefab:
timeline 4
timeline 5
timeline 6

And then I’d load the object with the timeline I needed. Ah but then you’re saying this won’t work unless the game object prefab includes everything manipulated by the timeline. So that’s sort of a non-starter isn’t it?

So absent all the manipulated objects, the timeline will instantiate without any references. Maybe that scheme you mentioned would work for tracks with single track binding types, but if I used exposed references doesn’t that scheme break down a bit (ie get really ugly really fast)? Or am I misunderstanding?

Is there any movement afoot address the limitation? I have to decide now, as the game development beings, whether timeline is going to be a safe option for the game. Sorry to keep bugging you about the same question, I just want to make absolutely sure I understand the implications of what I’m planning to do before I adopt a technology that might kill me later.

Ah one more thought I just had, many of the objects being manipulated are going to be the same from timeline to timeline (main characters speaking and moving, no audio). There won’t be many unique animation tracks since it’ll be portrait’s moving with speech. I guess maybe for my use case, my original plan wouldn’t be so bad right?

For exposed references, you can actually do the same thing. The exposedName does not have to be a guid, it can be any string you want, like a path. The playable director is simply a name->object lookup table.

So the limitation is that references to scene objects can only occur in the same scene. This is why the bindings and exposed references are store inside the object that plays the timeline. For future versions of timeline, this is something we are actively looking into.

One potential solution is guid based references. https://blogs.unity3d.com/2018/07/19/spotlight-team-best-practices-guid-based-references/ . This is a potential solution to allowing prefabs to reference scene objects (it would require some custom scripting), but I haven’t had the opportunity to try it out.

Maybe not. The best thing to do is try, and measure the impact.

2 Likes

Oh interesting! OK so I read that code. So the idea is to create game object prefabs, each one with a custom script containing a timeline reference.

Then, once I instantiate those game object, I need to either add to the game object script so that it is aware of all the guids and then have it populate the timeline references in the playable director, or somehow modify the timeline object so that it uses guids instead of references. Am I understanding correctly? Sounds like option 2 (if that’s possible) would be cleaner maybe.

Hey sorry to dredge this up again, but just for clarification, the problem is having references to many different timeline assets. Is that correct? The more unique animations, audio clips, etc., the longer the load time?

If I have 100 timelines in a scene and they all have the same two animation clips and 1 audio clip, but each one also has 1 unique custom clip, are you saying I’d end up with 103 clips loaded, or 400 clips?

Also, I’m wondering what specifically is expensive? Having the 100 timelines, or the unique clips, or something else?

What would I be looking for in the profiler, increase in heap size?

100 Timelines, each with the same 2 animation clips, 1 audio clip and 1 unique custom clip will cause the following assets to be loaded

100 TimelineAssets.
100 AnimationTracks (assuming 1 track/per timeline)
100 AudioTracks
100 CustomTracks
200 AnimationPlayableAssets
200 AudioPlayableAssets
100 Custom PlayableAssets (this is the unique clip).
2 AnimationClips
1 AudioClip

The Timeline, Tracks and PlayableAssets are not large assets, but they are UnityEngine.Objects so they would be similar in size to a GameObject.

To answer the question, the animation and audio clips are not duplicated but the custom clip is.

Assets and Object Count under the memory tab. It will affect heap size as well.

2 Likes