Search Unity

Exposed Reference Workflow Issues

Discussion in 'Timeline' started by tday_magicfuel, Oct 9, 2019.

  1. tday_magicfuel

    tday_magicfuel

    Joined:
    Apr 10, 2018
    Posts:
    21
    Exposed references and timelines behave in a frustrating manner. In our project we customize and use timelines for several features and we've set up some scenes prebuilt for editing certain timelines--with all the proper bindings set for the tracks, proper prefabs dragged into the scene, etc.

    upload_2019-10-9_13-36-0.png

    Here you can see a timeline from our project--note the red parent object field in the inspector. The object this was set to was set in one of our timeline scenes. As part of our natural workflow we modified the timeline in there, dragged in relevant game objects into the inspector and then carried on. The key issue here is that the exposed reference seems to be saved on whatever prefab would be playing the timeline. So if you don't save the prefab after editing an exposed reference field then it won't find anything at run time--resulting in the image above.

    From what I gather, there is no built in mechanism to notify us when the prefab needs saving. Moreover, the apply overrides button in the scene view does not specifically show when new exposed references need to be saved. The "fix" for us right now is to make sure we only do these links while in prefab mode for the prefab that contains the playable director.

    If necessary I can capture some video of this issue--it's a bit difficult to explain for sure. I also have a barebones project set up to show the issue as well. The steps below are just one way to reproduce the issue. The key to the issue is really in steps 7-10

    Steps to Repro:

    1. Create a prefab with a playable director on it set to play on awake. Make sure there is a child game object in this prefab for something to attach to, could be any primitive or an invisible transform point
    2. Create a separate prefab to spawn by the timeline (Some particles maybe, it doesn't really matter).
    3. Drag the playable director prefab into a scene
    4. Create a timeline and assign it to this director
    5. Add a control track to this timeline
    6. Add a control clip to the control track
    7. Assign the prefab from step 2 to be created by this track
    8. Assign the parent object field on the clip track to the child game object from step 1
    9. Save the scene (do _not_ apply any prefab changes)
    10. Open a new scene, drag the playable director prefab into the scene
    11. Hit play
    12. Pause the scene and inspect the control clip--you will see an unlinked red exposed reference field like in the above screenshot.



    The issue can be fixed by instead of editing the timeline in a scene you edit it in prefab mode for the prefab with the director. Then when you save prefab mode the exposed references are saved properly. However in prefab mode you might not be able to visualize the other elements in the timeline important to editing a sequence like you would be able to in a scene.

    Any suggestions/tips on this would be appreciated! Perhaps this could also be considered a bug. This can happen with any exposed reference field it seems. We've seen it first hand with the cinemachine shot clips and control track clips. The issue has been reproduced in 2018.4.9f1 and, at the time of writing, the most recent stable unity version (2019.2.8f1)
     
  2. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    The playable director stores the link between gameObject and the exposed reference, so if you are not applying the prefab changes, then the link will exist on other instances of the prefab.
     
  3. tday_magicfuel

    tday_magicfuel

    Joined:
    Apr 10, 2018
    Posts:
    21
    seant, I do understand that saving the prefab is what you are supposed to do. However the problem is this is exceedingly unclear that you need to do so. There is no indication while editing the timeline that you would need to do so.

    Also this is exacerbated by the new prefab workflow from 2018.3. It is more difficult to save a nested prefab which contains the director while editing within a scene. And we need to edit inside a scene so that we can visualize all the elements together properly.

    Basically, it's natural for someone who is editing a timeline to just edit the timeline in the timeline window. That they need to just remember to save the prefab with the director on it is definitely error prone. Furthermore, editing the timeline in prefab mode is undesirable for a variety of reasons.

    If there's something we can somehow hook onto when the exposed reference is set we could add the tooling ourselves to notify artists when they might need to save a prefab!
     
  4. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    Right, I see what you are saying.

    One option is to listen to Undo.postprocessModifications for changes to the playable director, specifically the m_ExposedReferences property. That will tell you when an exposed reference value has changed.
     
  5. tday_magicfuel

    tday_magicfuel

    Joined:
    Apr 10, 2018
    Posts:
    21
    Listening to postprocessModifications and checking for those properties does seem to help in detecting when this problem occurs. Since this even lets me resolve references to the affected components I ought to be able to actually save the affected prefab!

    Here is the code I run in the callback for post process modifications:
    Code (CSharp):
    1.  
    2.  
    3.         private static UndoPropertyModification[] PostProcessModifications(UndoPropertyModification[] modifications)
    4.         {
    5.             var savedDirectors = new List<PlayableDirector>();
    6.             var savedPlayables = new List<ControlPlayableAsset>();
    7.             var shouldTriggerSaveDialog = false;
    8.             foreach (var modification in modifications)
    9.             {
    10.                 var currentValueTarget = modification.currentValue.target;
    11.                 var targetType = currentValueTarget.GetType();
    12.                 if (targetType == typeof(PlayableDirector))
    13.                 {
    14.                     var playableDirector = currentValueTarget as PlayableDirector;
    15.                     if (!savedDirectors.Contains(playableDirector))
    16.                     {
    17.                         savedDirectors.Add(playableDirector);
    18.                     }
    19.                 }
    20.                 else if (targetType == typeof(ControlPlayableAsset))
    21.                 {
    22.                     var controlPlayableAsset = currentValueTarget as ControlPlayableAsset;
    23.                     if (!savedPlayables.Contains(controlPlayableAsset))
    24.                     {
    25.                         savedPlayables.Add(controlPlayableAsset);
    26.                     }
    27.                 }
    28.                
    29.                 var propertyPath = modification.currentValue.propertyPath;
    30.                 var isNewExposedReference = propertyPath.StartsWith("m_ExposedReferences") && propertyPath.EndsWith("first");
    31.                 var isExposedName = propertyPath.Contains("exposedName");
    32.                 if (!shouldTriggerSaveDialog)
    33.                 {
    34.                     shouldTriggerSaveDialog = isNewExposedReference || isExposedName;
    35.                 }
    36.             }
    37.  
    38.             var directorString = "";
    39.             savedDirectors.ForEach((director) => directorString = $"{directorString}{director.gameObject.name},\n");
    40.  
    41.             var timelineString = "";
    42.             savedPlayables.ForEach((playable) => timelineString = $"{timelineString}{playable.name},\n");
    43.  
    44.             if (shouldTriggerSaveDialog && EditorUtility.DisplayDialog(
    45.                     "Exposed References Changed",
    46.                     $"Exposed references were modified. The prefab with the playable director should be saved along with the projects assets. Do you wish to do this now? IF YOU DO NOT THINGS WILL BREAK!!!!\n\nAffected Directors:\n{directorString}\nAffected Clips:\n{timelineString}",
    47.                     "Save",
    48.                     "Cancel"))
    49.             {
    50.                 AssetDatabase.SaveAssets();
    51.                
    52.                 foreach (var director in savedDirectors)
    53.                 {
    54.                     var path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(director);
    55.                     Provider.Checkout(path, CheckoutMode.Both);
    56.                     PrefabUtility.RecordPrefabInstancePropertyModifications(director);
    57.                     PrefabUtility.ApplyObjectOverride(director, path, InteractionMode.AutomatedAction);
    58.                 }
    59.             }
    60.  
    61.             Debug.Log("Post save");
    62.             return modifications;
    63.         }
    It all seems to behave as expected except for the final bit--PrefabUtility.ApplyObjectOverride. This code is causing a null reference somewhere in the prefab utility.

    Code (CSharp):
    1. NullReferenceException: Object reference not set to an instance of an object
    2. UnityEditor.PrefabUtility.ApplySingleProperty (UnityEditor.SerializedProperty instanceProperty, UnityEditor.SerializedObject prefabSourceSerializedObject, System.String assetPath, System.Boolean isObjectOnRootInAsset, System.Boolean singlePropertyOnly, System.Boolean allowApplyDefaultOverride, System.Collections.Generic.List`1[T] serializedObjects, UnityEditor.InteractionMode action) (at C:/buildslave/unity/build/Editor/Mono/Prefabs/PrefabUtility.cs:686)
    3. UnityEditor.PrefabUtility.ApplyPropertyOverrides (UnityEngine.Object prefabInstanceObject, UnityEditor.SerializedProperty optionalSingleInstanceProperty, System.String assetPath, System.Boolean allowApplyDefaultOverride, UnityEditor.InteractionMode action) (at C:/buildslave/unity/build/Editor/Mono/Prefabs/PrefabUtility.cs:491)
    4. UnityEditor.PrefabUtility.ApplyObjectOverride (UnityEngine.Object instanceComponentOrGameObject, System.String assetPath, UnityEditor.InteractionMode action) (at C:/buildslave/unity/build/Editor/Mono/Prefabs/PrefabUtility.cs:715)
    5. MFGEditorCommon.Client.TimelineValidator.PostProcessModifications (UnityEditor.UndoPropertyModification[] modifications) (at Assets/Editor/Common/Client/Utils/TimelineValidator.cs:77)
    6. UnityEditor.Undo.InvokePostprocessModifications (UnityEditor.UndoPropertyModification[] modifications) (at C:/buildslave/unity/build/Editor/Mono/Undo/Undo.bindings.cs:189)
    7.  
    A bit curious what I'm doing wrong with the prefab utility saving here. I'm getting the path to the affected object (the one with the playable director) then trying to save the director component overrides to it. Any ideas where this is going wrong? This is in Unity 2018.4.9f1
     
  6. tday_magicfuel

    tday_magicfuel

    Joined:
    Apr 10, 2018
    Posts:
    21
    Wanted to follow up on this and note that we've been experiencing similar issues (with prefab saving) in other areas as well. It is unclear if this is a bug or we are using it incorrectly. Unfortunately I haven't found many great examples of the intended uses.