When syncing Mecanim layers, the synced states have no motion assigned to them. For normal states, this is good because I can drag and drop a new animation state, but how do you replace a blend tree in the synced layer?
When selecting the blend tree on the synced layer, in the Inspector it shows you that there is no blend tree assigned to it. So then how do I change the animations in that blend tree?
Sync layer do not recreate all the blend tree hierarchy for you, because you can choose that for this sync layer this state is not a blend tree but only one clip.
If you want to recreate a blend tree, you can right click on the state in the graph view and click on Create New Blend Tree in state or by script find out the original blend tree, make a copy and assign it to the sync layer state.
Bumping since I’m interested in the method of copying the blend tree… I tried to manually change values in the controller file but Unity didn’t like it…
Maybe Blend Trees should also be saved as assets, so it would be easier to reuse/duplicate them…
Here’s where I got. Perhaps @Mecanim-Dev can tell me where I’ve gone wrong.
The below successfully fixed the non-copying / syncing blendtrees in the new layer. But, ultimately doesn’t solve the problem b/c it syncs them while breaking override. Meaning, that if I update a blend tree on layer 1, it updates the layer 0 blend tree. Is there a way around this?
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Reflection;
using UnityEditor.Animations;
public class DuplicateAnimatorLayer : EditorWindow
{
UnityEditor.Animations.AnimatorController source;
string layerToCopy_Value;
int layerToFix;
[MenuItem("2Ton/Duplication Animator Layer")]
static void GetWindow()
{
EditorWindow.GetWindow(typeof(DuplicateAnimatorLayer));
}
void OnGUI()
{
source = EditorGUILayout.ObjectField("Source", source, typeof(AnimatorController),
true, GUILayout.ExpandWidth(true)) as AnimatorController;
layerToFix = EditorGUILayout.IntField("Layer to Fix: ", layerToFix);
// EditorGUILayout.TextField("Object Name: ", Selection.activeGameObject.name);
if (GUILayout.Button("Copy", GUILayout.ExpandWidth(true), GUILayout.Height(24)))
{
if (source != null && layerToFix != 0)
{
copy();
}
}
}
void copy()
{
AnimatorStateMachine baseStateMachine = source.layers[0].stateMachine;
// as per unity documentation: https://docs.unity3d.com/ScriptReference/Animations.AnimatorController-layers.html
// "It's important to note that the AnimatorControllerLayer are returned as a copy. The array should be set back into the property when changed."
// so we need to explicitly get the controller's layers, then below we update their motions, then we explicitly update them
AnimatorControllerLayer[] layers = source.layers;
// go through all the sub-state machines
for (int i = 0; i < baseStateMachine.stateMachines.Length; i++)
{
AnimatorStateMachine stateMachine = baseStateMachine.stateMachines[i].stateMachine;
// update motions
for (int m = 0; m < stateMachine.states.Length; m++)
{
// we only care about blend trees, they are the ones that don't sync
if (stateMachine.states[m].state.motion.GetType() == typeof(BlendTree))
{
AnimatorState state = stateMachine.states[m].state;
UnityEngine.Motion motion = layers[layerToFix].GetOverrideMotion(state);
if (motion == null)
{
Debug.Log("Overriding: " + state.name);
layers[layerToFix].SetOverrideMotion(state, state.motion);
}
}
}
// explicitly update the controller's layers so the copy gets updated
source.layers = layers;
}
}
// I wish this worked...
// if (stateMachine.states[m].state.motion.GetType() == typeof(BlendTree))
// {
// AnimatorState state = stateMachine.states[m].state;
// UnityEngine.Motion motion = layers[layerToFix].GetOverrideMotion(state);
// if (motion == null)
// {
// BlendTree baseBlendTree = state.motion as BlendTree;
// BlendTree newBlendTree = new BlendTree();
// SerializedObject SO = new SerializedObject(newBlendTree);
// SerializedProperty SP = SO.GetIterator();
// SP = SO.FindProperty("m_UseAutomaticThresholds");
// SP.boolValue = false;
// SO.ApplyModifiedProperties();
// newBlendTree.blendParameter = baseBlendTree.blendParameter;
// newBlendTree.blendType = baseBlendTree.blendType;
// for (int j = 0; j < baseBlendTree.children.Length; j++)
// {
// newBlendTree.AddChild(baseBlendTree.children[j].motion, baseBlendTree.children[j].threshold);
// }
// for (int j = 0; j < baseBlendTree.children.Length; j++)
// {
// UnityEditor.Animations.ChildMotion[] newChildren = newBlendTree.children;
// newChildren[j].timeScale = baseBlendTree.children[j].timeScale;
// newChildren[j].position = baseBlendTree.children[j].position;
// newChildren[j].threshold = baseBlendTree.children[j].threshold;
// newChildren[j].directBlendParameter = baseBlendTree.children[j].directBlendParameter;
// newBlendTree.children = newChildren;
// }
// motion = newBlendTree;
// Debug.Log("Overriding: " + state.name);
// }
// }
}
This is not something that a lot of user are aware of but statemachine and blend tree are asset that can be saved. In the normal editor workflow they are embedded into the controller asset.
But it also have a side effect if you share a blend tree it does mean that if you change it, it will also change all other reference. That exactly what @v2-Ton-Studios is experiencing.
I’ll dig into this in a few hours, but are you saying that if I use Instantiate in my above code, parameterize so it matches the “base blend tree”, and then pass it in as SetOverrideMotion (state, instantiatedBlendTree); … it should work i.e. it would be a true copy, not a reference?
Had to try this now… . Using Instantiate makes no difference…
if (motion == null)
{
BlendTree baseBlendTree = state.motion as BlendTree;
BlendTree newBlendTree = Instantiate <BlendTree> (baseBlendTree);
// feels like this is the issue. SetOverride "syncs" the blendtrees?
layers[layerToFix].SetOverrideMotion(state, newBlendTree);
Debug.Log("Overriding: " + state.name);
}
What am I missing here?
The above still results in what seems like a reference between baseBlendTree “Layer0.Foo” and newBlendTree “Layer1.Foo”… :(.
hard to tell without debugging the code, I can tell you that we have unit test covering this area and it does work so there is something specific to you’re project.
One thing you can do is force your asset serialization to text mode, then open up you’re controller asset file and look at what the file look like before and after you’re operation.
Each object serialized into the file have a unique ID, something like this:
— !u!91 &9100000
AnimatorController:
Find the animator state that you are trying to edit, you should have two: one for the base layer and one for the sync layer and check for m_Motion field
— !u!1102 &1102000010750584734
AnimatorState:
…
m_Motion: {fileID: 7400000, guid: 1d531f81c90784a79b0591821aa8875d, type: 2}
the two animator state should have a unique id for the motion, if they are the same then it does mean that they reference the same object.
or you could you log a bug with you’re script ? we will investigate
[quote=“Mecanim-Dev, post:7, topic: 575753, username:Mecanim-Dev”]
They can be saved as asset and reused.
[/quote] How? They don’t show up as assets in the asset folder, nor is there any “save as” option as far as I can see for blendtrees.
Ok, I’ve written a quick script to make a duplicate of a selected blendtree and save it as an asset, as Mecanim-Dev suggested. Make sure you actually have a blendtree selected (for the Selection.activeObject as Blendtree to work).
using UnityEditor;
using UnityEngine;
using UnityEditor.Animations;
public class CreateBlendtreeAsset : MonoBehaviour {
[MenuItem("AnimTools/GameObject/Create Blendtree")]
static void CreateBlendtree()
{
BlendTree BT = Selection.activeObject as BlendTree;
BlendTree BTcopy = Instantiate<BlendTree>(BT);
AssetDatabase.CreateAsset(BTcopy, ("Assets/" + BT.name + ".asset"));
}
}
Here’s a GUI version that works pretty well. Select a state in your animator, the script will find the BlendTree and select it for you based on the state’s “motion” value.
using UnityEditor;
using UnityEngine;
using UnityEditor.Animations;
public class CreateBlendtreeAsset : EditorWindow
{
BlendTree _blendTree;
AnimatorState _animatorState;
string _motionName, _newName, _createPath;
[MenuItem("Window/Duplicate Blendtree")]
public static void ShowWindow()
{
//Show existing window instance. If one doesn't exist, make one.
EditorWindow.GetWindow(typeof(CreateBlendtreeAsset));
}
void OnGUI()
{
GUILayout.Label("Base Settings", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("BlendTree");
_blendTree = EditorGUILayout.ObjectField(_blendTree, typeof(BlendTree), true) as BlendTree;
EditorGUILayout.EndHorizontal();
if (_blendTree)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label("BlendTree");
EditorGUILayout.LabelField(_motionName);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("New Name");
_newName = EditorGUILayout.TextField(_newName);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Create Path");
_createPath = EditorGUILayout.TextField(_createPath);
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Duplicate BlendTree"))
{
int canRun = 0;
if (_newName == null || _newName == "")
{
ShowNotification(new GUIContent("Provide a Name for BlendTree"));
}
else
{
canRun++;
}
if (_createPath == null || _createPath == "")
{
ShowNotification(new GUIContent("Provide a path for BlendTree"));
}
else
{
canRun++;
}
if (canRun == 2)
{
BlendTree BTcopy = Instantiate<BlendTree>(_blendTree);
AssetDatabase.CreateAsset(BTcopy, AssetDatabase.GenerateUniqueAssetPath(_createPath + _newName + ".asset"));
}
}
}
else
{
setBlendTree();
}
}
void Update ()
{
setBlendTree();
}
private void setBlendTree()
{
AnimatorState AS = Selection.activeObject as AnimatorState;
if (AS != null && AS != _animatorState && AS.motion is BlendTree)
{
_animatorState = AS;
_motionName = _animatorState.name;
_blendTree = _animatorState.motion as BlendTree;
}
}
}