Search Unity

Managing PlayableAssets at runtime (maintain dictionary?)

Discussion in 'Timeline' started by VoodooDetective, Dec 8, 2019.

  1. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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.

    Code (csharp):
    1.  
    2. namespace EditorMods
    3. {
    4.     /// <summary>
    5.     /// Scans for new timelines.
    6.     /// </summary>
    7.     public class TimelineAssetScanner : AssetPostprocessor
    8.     {
    9.         private const string TimelineDirectory = "Assets/Timeline/";
    10.         static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    11.         {
    12.             // Look for changes to active timeline directory
    13.             string currentScene = SceneManager.GetActiveScene().name;
    14.             string timelineDirectory = TimelineDirectory + currentScene;
    15.             bool timelinesChanged = false;
    16.             foreach (string assetName in importedAssets)
    17.             {
    18.                 if (assetName.Contains(timelineDirectory))
    19.                 {
    20.                     timelinesChanged = true;
    21.                     break;
    22.                 }
    23.             }
    24.             if (!timelinesChanged)
    25.             {
    26.                 foreach (string assetName in deletedAssets)
    27.                 {
    28.                     if (assetName.Contains(timelineDirectory))
    29.                     {
    30.                         timelinesChanged = true;
    31.                         break;
    32.                     }
    33.                 }
    34.             }
    35.             if (!timelinesChanged)
    36.             {
    37.                 for (int i = 0; i < movedAssets.Length; i++)
    38.                 {
    39.                     if (movedAssets[i].Contains(timelineDirectory) || movedFromAssetPaths[i].Contains(timelineDirectory))
    40.                     {
    41.                         timelinesChanged = true;
    42.                         break;
    43.                     }
    44.                 }
    45.             }
    46.  
    47.             // Update timeline registry
    48.             if (timelinesChanged)
    49.             {
    50.                 GameObject currentSceneObject = GameObject.Find(currentScene);
    51.                 TimelineAssetRegistry registry = currentSceneObject.GetComponent<TimelineAssetRegistry>();
    52.                 if (currentSceneObject != null)
    53.                 {
    54.                     registry.playableAssetList.Clear();
    55.                     string[] assetGuids = AssetDatabase.FindAssets(null, new[] { timelineDirectory });
    56.                     foreach (string guid in assetGuids)
    57.                     {
    58.                         string path = AssetDatabase.GUIDToAssetPath(guid);
    59.                         UnityEngine.Object assetObject = AssetDatabase.LoadAssetAtPath(path, typeof(PlayableAsset));
    60.                         if (assetObject != null) registry.playableAssetList.Add(assetObject as PlayableAsset);
    61.                     }
    62.                 }
    63.             }
    64.         }
    65.     }
    66. }
    67.  
    68.  
    69. namespace Timeline
    70. {
    71.     /// <summary>
    72.     /// Registry of all playable assets for this scene.
    73.     /// </summary>
    74.     public class TimelineAssetRegistry : MonoBehaviour
    75.     {
    76.         // References
    77.         public List<PlayableAsset> playableAssetList;
    78.  
    79.         // Properties
    80.         private Dictionary<string, PlayableAsset> assetRegistry;
    81.  
    82.         void Awake()
    83.         {
    84.             assetRegistry = new Dictionary<string, PlayableAsset>();
    85.             foreach (PlayableAsset asset in playableAssetList)
    86.             {
    87.                 assetRegistry[asset.name] = asset;
    88.             }
    89.         }
    90.  
    91.         /// <summary>
    92.         /// Fetch a timeline from the registry.
    93.         /// </summary>
    94.         /// <param name="timelineName">Name of the timeline.</param>
    95.         /// <returns>A timeline.</returns>
    96.         public PlayableAsset GetTimeline(string timelineName)
    97.         {
    98.             if (!assetRegistry.ContainsKey(timelineName))
    99.             {
    100.                 throw new System.Exception(timelineName + " missing from registry!");
    101.             }
    102.             return assetRegistry[timelineName];
    103.         }
    104.     }
    105. }
    106.  
    107.  
     
  2. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
    VoodooDetective likes this.
  3. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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.
     
  4. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
    VoodooDetective likes this.
  5. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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?
     
  6. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
  7. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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:
    Code (csharp):
    1.  
    2. Game Object 1 Prefab:
    3. timeline 1
    4. timeline 2
    5. timeline 3
    6.  
    7. Game Object 2 Prefab:
    8. timeline 4
    9. timeline 5
    10. timeline 6
    11.  
    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.
     
  8. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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?
     
  9. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
    VoodooDetective likes this.
  10. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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.
     
  11. VoodooDetective

    VoodooDetective

    Joined:
    Oct 11, 2019
    Posts:
    239
    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?
     
  12. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
    VoodooDetective likes this.