I dread making a new timeline because I will need to hunt for all the hierarchy objects and re-bind them. It should be a stress free process but it is not.
There seems no particular reason that the duplication mechanism could not also duplicate the bindings.
It makes workflow that much slower, because you have to re-find and re-bind the targets. I have chosen 2 bindings here, but my production Timlines have many more.
I have a scripted sequence using a few common objects. So the Timelines are very similar, but I have at least 15 timelines with many tracks each.
If you can follow my simplified example timeline:
Audio Track - One speaks - Bound to an AudioSource A
Animation Track - Another animates - Bound to an AnimationController B
There is a problem, that when I duplicate a Timeline, the bindings are lost.
So when I duplicate the example above, there is a Timeline and it has two tracks. 1. AudioTrack, 2. Animation Track. But the duplicate is bound to neither AudioSource A nor AnimationController B
I have to hit the heirarchy pane, locate both AudioSource A and AnimationController B and drag them to their respective slots.
The reason for this is the asset does not include any bindings. The tracks are themselves assets, and new tracks are made. We should add a âduplicate with bindings optionâ, but to help you out here is a helper script that will copy a playableDirector, timeline, and all the bindings.
public class DuplicateTimeline
{
[MenuItem("Timeline/Duplicate With Bindings", true)]
public static bool DuplicateWithBindingsValidate()
{
if (UnityEditor.Selection.activeGameObject == null)
return false;
var playableDirector = UnityEditor.Selection.activeGameObject.GetComponent<PlayableDirector>();
if (playableDirector == null)
return false;
var playableAsset = playableDirector.playableAsset;
if (playableAsset == null)
return false;
var path = AssetDatabase.GetAssetPath(playableAsset);
if (string.IsNullOrEmpty(path))
return false;
return true;
}
[MenuItem("Timeline/Duplicate With Bindings")]
public static void DuplicateWithBindings()
{
if (UnityEditor.Selection.activeGameObject == null)
return;
var playableDirector = UnityEditor.Selection.activeGameObject.GetComponent<PlayableDirector>();
if (playableDirector == null)
return;
var playableAsset = playableDirector.playableAsset;
if (playableAsset == null)
return;
var path = AssetDatabase.GetAssetPath(playableAsset);
if (string.IsNullOrEmpty(path))
return;
string newPath = path.Replace(".", "(Clone).");
if (!AssetDatabase.CopyAsset(path, newPath))
{
Debug.LogError("Couldn't Clone Asset");
return;
}
var newPlayableAsset = AssetDatabase.LoadMainAssetAtPath(newPath) as PlayableAsset;
var gameObject = GameObject.Instantiate(UnityEditor.Selection.activeGameObject);
var newPlayableDirector = gameObject.GetComponent<PlayableDirector>();
newPlayableDirector.playableAsset = newPlayableAsset;
var oldBindings = playableAsset.outputs.ToArray();
var newBindings = newPlayableAsset.outputs.ToArray();
for (int i = 0; i < oldBindings.Length; i++)
{
newPlayableDirector.SetGenericBinding(newBindings[i].sourceObject,
playableDirector.GetGenericBinding(oldBindings[i].sourceObject)
);
}
}
}
Thanks a lot Sean !! ( I guess from your username ). That worked a treat. There was a small compilation error in your script, but I am using the experimental 4.6 .Net version so that may be it. There was no ToArray() method on the outputs.
For other forum users, I have included my fixes here :
using UnityEngine;
using UnityEditor;
using UnityEngine.Playables ;
---------------------
var oldBindings = playableAsset. outputs.GetEnumerator();
var newBindings = newPlayableAsset. outputs.GetEnumerator();
while( oldBindings.MoveNext() )
{
var oldBindings_sourceObject = oldBindings.Current.sourceObject ;
newBindings.MoveNext();
var newBindings_sourceObject = newBindings.Current.sourceObject ;
newPlayableDirector.SetGenericBinding(
newBindings_sourceObject,
playableDirector.GetGenericBinding( oldBindings_sourceObject )
);
}
Got it working⌠Hereâs the combination of the two scripts for those interested.
Thanks to @Tezza and @seant_unity for the code.
Unity Timeline is amazing!!!
using UnityEngine;
using UnityEditor;
using UnityEngine.Playables ;
public class DuplicateTimeline
{
[MenuItem("Timeline/Duplicate With Bindings", true)]
public static bool DuplicateWithBindingsValidate()
{
if (UnityEditor.Selection.activeGameObject == null)
return false;
var playableDirector = UnityEditor.Selection.activeGameObject.GetComponent<PlayableDirector>();
if (playableDirector == null)
return false;
var playableAsset = playableDirector.playableAsset;
if (playableAsset == null)
return false;
var path = AssetDatabase.GetAssetPath(playableAsset);
if (string.IsNullOrEmpty(path))
return false;
return true;
}
[MenuItem("Timeline/Duplicate With Bindings")]
public static void DuplicateWithBindings()
{
if (UnityEditor.Selection.activeGameObject == null)
return;
var playableDirector = UnityEditor.Selection.activeGameObject.GetComponent<PlayableDirector>();
if (playableDirector == null)
return;
var playableAsset = playableDirector.playableAsset;
if (playableAsset == null)
return;
var path = AssetDatabase.GetAssetPath(playableAsset);
if (string.IsNullOrEmpty(path))
return;
string newPath = path.Replace(".", "(Clone).");
if (!AssetDatabase.CopyAsset(path, newPath))
{
Debug.LogError("Couldn't Clone Asset");
return;
}
var newPlayableAsset = AssetDatabase.LoadMainAssetAtPath(newPath) as PlayableAsset;
var gameObject = GameObject.Instantiate(UnityEditor.Selection.activeGameObject);
var newPlayableDirector = gameObject.GetComponent<PlayableDirector>();
newPlayableDirector.playableAsset = newPlayableAsset;
var oldBindings = playableAsset. outputs.GetEnumerator();
var newBindings = newPlayableAsset. outputs.GetEnumerator();
while( oldBindings.MoveNext() )
{
var oldBindings_sourceObject = oldBindings.Current.sourceObject ;
newBindings.MoveNext();
var newBindings_sourceObject = newBindings.Current.sourceObject ;
newPlayableDirector.SetGenericBinding(
newBindings_sourceObject,
playableDirector.GetGenericBinding( oldBindings_sourceObject )
);
}
}
}
Create a C# script, copy and paste this code into MonoDevelop when it opens, and name it something like duplicateTimeline. Now with your timeline selected from the Timeline menu at the top there is an option to Duplicate With Bindings. Awesome! Thanks @Tezza and @seant_unity for the code.
Any way to edit this script to keep the duplicated timeline gameObject on the canvas? When duplicate a timeline asset it moves out of canvas. Then when I bring it back into the canvas its the wrong size.
Hey @grossim I think it is working in 2018.2, it just works in a kind of weird way. Some of the issues I had were:
You need to have the GameObject that has the Playable Director component selected in your scene in order to duplicate it (not in your Project window)
When you duplicate it, it creates a Clone GameObject in your scene with the duplicated Timeline in its PlayableDirector slot (rather than modifying the original GameObject)
Most confusing is the fact that the duplicated Timeline still references the original GameObjects, even if it created new ones.
So in our scene, we have all our animated elements as children of the object with the Playable Director component, and when we duplicated the Timeline it duplicated all those objects. However, the new Timeline still controlled the original objects, so instead of using the Cloned object, I simply copied the Playable Director component and pasted it on the original parent object.
It clones the Timeline but without any bindings, it is doing exactly the same as ctrl D in the project window.
I followed all instructions I could find here without any positive result.
any help would be very appreciated.
thanks,
Mike
Edit:
my bad,
wasnât noticing that only the new created Director hold all bindings,
what is logical when you think about it.
So, yeah it is working.
This works great and I get no errors compiling it or using it during production. The Timeline Menu Item shows up in my editor and the duplication-with-bindings works perfect. Thanks for this!
However, I do get errors when trying to publish a Build to WebGL. The build fails with this error in the console:
âAssets/Timelines/DuplicateTimeline.cs(7,6): error CS0246: The type or namespace name âMenuItemâ could not be found (are you missing a using directive or an assembly reference?)â
I cannot find any problems with the script, and I found that I can build if I just remove it from the project. Not a big issue, but if anyone has a solution so the script does not need removing, I would appreciate any info.
Put the script in a folder named Editor. It will be excluded from player builds then. Or if you need it in a player, wrap the using UnityEditor; and [MenuItem(âŚ)] lines with a #if UNITY_EDITOR / #endif.
This doesnt work, all it does is duplicates my hierarchy objects and makes a timeline clone, then when i try to reassign the timeline clone to a different playable component all my bindings are gone again, so basically making this script useless. why is this so hard, all i want to be able to do is copy a timeline and then edit it so i have several different variations of the same timeline that can be assigned to a single playable object but every time i try to do this i loose all object bindings, my timeline has at least 30 object bindings and i need to copy it 7 times, it is far too tedious to manually reassign all the bindings every time i make a duplicate timeline, please create a solution for this, it has been an issue since the timeline was implemented, for a tool that is supposed to make you life easier every time i find myself using timeline i have issues like this that make me absolutely hat it, the system just feels broken and illogical to me. Not to mention all the other problems i have with the design of this tool, ( why is every new track put to the bottom of the timeline list) Little things like this make the timeline system a complete pain to use and i dread anytime i need to make an animated sequence with this tool please fix it Unity.
for those who say it doesnât work because it copies the hierarchy of objects, of course it does! you donât read the code you just copy paste it and run it without trying to have at least a grasp of what it does?? I mean itâs ok for non programmers because an artist might not even know what the code is doing and he just wants things to get done, but devs, please.
the code is instantiating the original gameObject ALONG with all itâs children, and then rebinds the director bindings in the duplicate director, to the bindings in the original director, meaning that if you are using objects that are children of the game object that has the director component, then you will have a duplicate hierarchy of objects that are doing nothing, you still have bindings to the original objects.
what you want to do is: instead of instantiate(), just create a new gameobject, add a playableDirector to it, and then only do the rebinding part of the code.
P.S: this should really be an out of the box feature, at least when we right click the PlayableDirector component and do âCopy Componentâ then âPaste Component Valuesâ should automatically rebind the bindings, which is not the case
I created a simple script to do this, to use it simply copy the Playable Asset on the project, youâll see it will lose all bindings when applied to the PlayableDirector Component, add this script below, and assign the PlayableDirector component to the TimelineComponent slot, the source PlayableAsset (with working binding) and the target PlayableAsset (the copied one), and then choose âDuplicate Timelineâ from the context menu. After this, youâll be able to assign the copied timeline to the original PlayableDirector component, with the right bindings intact.
using UnityEngine;
using UnityEngine.Playables;
public class CopyTimeline : MonoBehaviour
{
[SerializeField] PlayableDirector timelineComponent;
[SerializeField] PlayableAsset sourceTimeline, targetTimeline;
[ContextMenu("Duplicate Timeline")]
public void Duplicate()
{
var oldBindings = sourceTimeline.outputs.GetEnumerator();
var newBindings = targetTimeline.outputs.GetEnumerator();
while (oldBindings.MoveNext())
{
var oldBindings_sourceObject = oldBindings.Current.sourceObject;
newBindings.MoveNext();
var newBindings_sourceObject = newBindings.Current.sourceObject;
timelineComponent.SetGenericBinding(
newBindings_sourceObject,
timelineComponent.GetGenericBinding(oldBindings_sourceObject)
);
}
}
}