Search Unity

Resolved Changing Blend tree through sciprt (Not Bug but feature)

Discussion in 'Animation' started by DSivtsov, Jul 25, 2021.

  1. DSivtsov

    DSivtsov

    Joined:
    Feb 20, 2019
    Posts:
    151
    Unity 2020.3.14 (win10 x64)
    The primary task was to update the values (ChildMotion.position) of Motions in BlendTree (2D Freedom Directional) by script. For possibility to set for animation clips the same parameters as in main code - the maximum WalkSpeed & RunSpeed. which can be change in Inspector.
    BlendTree.JPG

    Note.
    The ChildMotion[] arrChildMotion = blendTree.children, I not change and use only as source information the main methods is blendTree.AddChild(...) and blendTree.RemoveChild(index). The issue related to them.

    Was made two version
    One Version - get the reference to AnimatorController through the RuntimeAnimatorController, attached to Monobehavior script and after AnimatorStateMachine -> BlendTree
    Code (CSharp):
    1.  void UpdateWalkRunSpeedInBlendTree()
    2.     {
    3.         RuntimeAnimatorController runtimeController = PlayerAnimator.runtimeAnimatorController;
    4.         if (runtimeController == null)
    5.         {
    6.             Debug.LogErrorFormat("RuntimeAnimatorController must not be null.");
    7.             return;
    8.         }
    9.  
    10.         AnimatorController assetAnimatorController =
    11.             UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(UnityEditor.AssetDatabase.GetAssetPath(runtimeController));
    12.         if (assetAnimatorController == null)
    13.         {
    14.             Debug.LogErrorFormat("AnimatorController must not be null.");
    15.             return;
    16.         }
    17.  
    18.         AnimatorStateMachine stateMachine = assetAnimatorController.layers[0].stateMachine;
    19.  
    20.         AnimatorState stateWithBlendTree = stateMachine.states[0].state;
    21.  
    22.         BlendTree blendTree = (BlendTree)stateWithBlendTree.motion;
    23.  
    24.         ChildMotion[] arrChildMotion = blendTree.children;
    25.         ChildMotion currentChildMotion;
    26.         for (int index = 0; index < arrChildMotion.Length; index++)
    27.         {
    28.             currentChildMotion = arrChildMotion[index];
    29.             if (currentChildMotion.motion.name.Contains("Run"))
    30.             {
    31.                 //Debug.Log($"Run : {item.motion.name} new position={item.position.normalized * _maximumRunSpeed}");
    32.                 blendTree.AddChild(currentChildMotion.motion, currentChildMotion.position.normalized * _maximumRunSpeed);
    33.             }
    34.             else if (!currentChildMotion.motion.name.Contains("Idle"))
    35.             {
    36.                 //Debug.Log($"Walk : {item.motion.name} new position={item.position.normalized * _maximumWalkSpeed}");
    37.                 blendTree.AddChild(currentChildMotion.motion, currentChildMotion.position.normalized * _maximumWalkSpeed);
    38.             }
    39.                 else
    40.                     //Idle Motion
    41.                     blendTree.AddChild(currentChildMotion.motion, Vector2.zero);
    42.             blendTree.RemoveChild(index);
    43.         }
    44. }
    And the Second Version (because First Version worked unstable) - get directly access to the asset controller
    through (AnimatorController)AssetDatabase.LoadAssetAtPath("Assets/Animations/ControllerOrig.controller",...) and after AnimatorStateMachine -> BlendTree. Also in the Second Variant used the "NewPlayerControlMove refToScript = Selection.activeGameObject.GetComponent<NewPlayerControlMove>();" to access the Serializable variables in "NewPlayerControlMove" Script only.
    Code (CSharp):
    1. [MenuItem("AnimTools/Update Controller")]
    2.     static void UpdateController()
    3.     {
    4.         //Misc_ClearLogConsole();
    5.  
    6.         if (!Selection.activeGameObject)
    7.         {
    8.             Debug.LogErrorFormat("Not selected GameObject");
    9.             return;
    10.         }
    11.  
    12.         NewPlayerControlMove refToScript = Selection.activeGameObject.GetComponent<NewPlayerControlMove>();
    13.         if (!refToScript)
    14.         {
    15.             Debug.LogErrorFormat("Ref to NewPlayerControlMove is null");
    16.             return;
    17.         }
    18.  
    19.         float _maximumWalkSpeed = (float)System.Math.Round((refToScript.WalkingSpeed / 3600 * 1000), 1);
    20.         float _maximumRunSpeed = (float)System.Math.Round((refToScript.RunningSpeed / 3600 * 1000), 1);
    21.  
    22.         AnimatorController assetAnimatorController = (AnimatorController)AssetDatabase.LoadAssetAtPath("Assets/Animations/ControllerOrig.controller", typeof(AnimatorController));
    23.  
    24.         if (assetAnimatorController != null)
    25.         {
    26.             Debug.Log($"AnimatorController {assetAnimatorController.name}");
    27.  
    28.         }
    29.         else
    30.         {
    31.             Debug.LogErrorFormat("Ref ro AnimatorController is null");
    32.             return;
    33.         }
    34.  
    35.  
    36.         AnimatorStateMachine stateMachine = assetAnimatorController.layers[0].stateMachine;
    37.  
    38.         AnimatorState stateWithBlendTree = stateMachine.states[0].state;
    39.  
    40.         BlendTree blendTree = (BlendTree)stateWithBlendTree.motion;
    41.         ChildMotion[] arrChildMotion = blendTree.children;
    42.         ChildMotion currentChildMotion;
    43.         for (int index = 0; index < arrChildMotion.Length; index++)
    44.         {
    45.             currentChildMotion = arrChildMotion[index];
    46.             if (currentChildMotion.motion.name.Contains("Run"))
    47.             {
    48.                 //Debug.Log($"Run : {item.motion.name} new position={item.position.normalized * _maximumRunSpeed}");
    49.                 blendTree.AddChild(currentChildMotion.motion, currentChildMotion.position.normalized * _maximumRunSpeed);
    50.             }
    51.             else if (!currentChildMotion.motion.name.Contains("Idle"))
    52.             {
    53.                 //Debug.Log($"Walk : {item.motion.name} new position={item.position.normalized * _maximumWalkSpeed}");
    54.                 blendTree.AddChild(currentChildMotion.motion, currentChildMotion.position.normalized * _maximumWalkSpeed);
    55.             }
    56.             else
    57.                 //Idle Motion
    58.                 blendTree.AddChild(currentChildMotion.motion, Vector2.zero);
    59.             blendTree.RemoveChild(index);
    60.         }
    61.  
    62.         AssetDatabase.SaveAssets();
    63.     }
    All Variants work unstable - the values fully and correctly was changed only after 2-3 run of methods. After the first run some records (motions) didn't change its values (2-3 from 7 motions). Effect was repeated constantly.

    For testing purpose I'm simplified the scripts (based on second variant). I left only process of deleting old records from original Blend Tree
    ....
    Code (CSharp):
    1.         Debug.Log($"arrChildMotion.Length = {arrChildMotion.Length}");
    2.         for (int index = 0; index < arrChildMotion.Length; index++)
    3.         {
    4.             Debug.Log($"[{index}] - Del");
    5.             blendTree.RemoveChild(index);
    6.         }
    7.         AssetDatabase.SaveAssets();
    8.     }
    And received very strange results. This script became work with errors "ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.", the previous variants work without any errors.
    All records ("motions") from BlendTree are deleted only after 3 times run of script. The 2 times script stop with the error "Out of range". Between attempts (and at beginning) I run the "simple test script" to check the current records at BlendTree and output these information to log. (I the cut more interesting part of Editor.log and attach it ).
     

    Attached Files:

    Last edited: Jul 25, 2021
  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    You're getting the
    stateWithBlendTree.motion;
    but never re-assigning it so the
    stateWithBlendTree
    might not know that its
    motion
    has been modified. So try setting
    stateWithBlendTree.motion = blendTree;
    at the end. Try that for each thing you modify.

    I haven't done any scripted Animator Controller modification myself, so I don't actually know if it will help, but that's how some other parts of the engine work.
     
  3. DSivtsov

    DSivtsov

    Joined:
    Feb 20, 2019
    Posts:
    151
    Thank you for your attention and participation.

    But after sleep and little walking:(, I found the main problem of these strange results:
    The main problem is the specific of realization of blendTree.RemoveChild(index) - after each call it rebuild the ChildMotion[] arrChildMotion (=blendTree.children) and the count of element in the array is changed and also are changed it position (!!!) in array (it "shift up" and not to leave empty spaces in the list of motions of BlendTree) and therefore ... at the end "the snake bites its tail" - "Index was out of range".

    To be True it isn't a bug, it's a feature and not very clear documentation (Unity not only "save the tree, but also save bytes in documentation" :)).
    Will be good if they add some notes to documentation related to blendTree.RemoveChild(index).

    The work variant of Code will be that (I remove the Motion records moving from End to Beginning of the array) and also save records in temp arrays (Motions list and its positions)
    Code (CSharp):
    1.     private static int BackUpAndDelMotionsController(BlendTree blendTree, out Motion[] arrListMotions, out Vector2[] arrOldVectors)
    2.     {
    3.         ChildMotion[] arrChildMotion = blendTree.children;
    4.         ChildMotion currentChildMotion;
    5.         arrListMotions = new Motion[arrChildMotion.Length];
    6.         arrOldVectors = new Vector2[arrChildMotion.Length];
    7.         for (int index = arrChildMotion.Length - 1; index >= 0; index--)
    8.         {
    9.             currentChildMotion = arrChildMotion[index];
    10.             arrListMotions[index] = currentChildMotion.motion;
    11.             arrOldVectors[index] = currentChildMotion.position;
    12.  
    13.             blendTree.RemoveChild(index);
    14.         }
    15.         AssetDatabase.SaveAssets();
    16.         return arrChildMotion.Length;
    17.     }
    and After create the new updated records based on these arrays
    Code (CSharp):
    1.  
    2.     //Time Solution to Update Blend Tree ("Assets/Animations/ControllerOrig.controller")
    3.     // with Data from Class NewPlayerControlMove (_maximumWalkSpeed and _maximumRunSpeed)
    4.     [MenuItem("AnimTools/Update Blend Tree")]
    5.     static void UpdateBlendTreeController()
    6.     {
    7.         BlendTree blendTree;
    8.         Motion[] arrListMotions;
    9.         Vector2[] arrOldVectors;
    10.         // Skipped not interesting part of code......to get accees to the _maximumWalkSpeed and _maximumRunSpeed variables
    11.         BackUpAndDelMotionsController(blendTree, out  arrListMotions, out arrOldVectors);
    12.  
    13.         Motion currentMotion;
    14.         for (int index = 0; index < arrListMotions.Length; index++)
    15.         {
    16.             currentMotion = arrListMotions[index];
    17.             if (currentMotion.name.Contains("Run"))
    18.             {
    19.                 //Debug.Log($"Run : {item.motion.name} new position={item.position.normalized * _maximumRunSpeed}");
    20.                 blendTree.AddChild(currentMotion, arrOldVectors[index].normalized * _maximumRunSpeed);
    21.             }
    22.             else if (!currentMotion.name.Contains("Idle"))
    23.             {
    24.                 //Debug.Log($"Walk : {item.motion.name} new position={item.position.normalized * _maximumWalkSpeed}");
    25.                 blendTree.AddChild(currentMotion, arrOldVectors[index].normalized * _maximumWalkSpeed);
    26.             }
    27.             else
    28.                 //Idle Motion
    29.                 blendTree.AddChild(currentMotion, Vector2.zero);
    30.         }
    31.         AssetDatabase.SaveAssets();
    Also made very nice (IMHO) a method to show the main parameters of BlendTree in Console
    Code (CSharp):
    1.     [MenuItem("AnimTools/Show Blend Tree")]
    2.     static void TestNewController()
    3.     {
    4.         BlendTree blendTree;
    5.         // After the switch the blendTree variable will contain the correspondent object BlendTree
    6.         switch (Selection.activeObject)
    7.         {
    8.             case AnimatorState tmpsAnimatorState:
    9.                 blendTree = tmpsAnimatorState.motion as BlendTree;
    10.                 if (!blendTree)
    11.                 {
    12.                     Debug.LogErrorFormat("The selected AnimatorState isn't a BlendTree");
    13.                     return;
    14.                 }
    15.                 break;
    16.             case BlendTree tempBlendTree:
    17.                 blendTree = tempBlendTree;
    18.                 break;
    19.             default:
    20.                 Debug.LogErrorFormat("Not selected a BlendTree or correspondent an AnimatorState");
    21.                 return;
    22.         }
    23.  
    24.         ChildMotion[] arrChildMotion = blendTree.children;
    25.         Debug.Log($"Number Motions in BlendTree = {arrChildMotion.Length}");
    26.         ChildMotion currentChildMotion;
    27.         for (int index = 0; index < arrChildMotion.Length; index++)
    28.         {
    29.             currentChildMotion = arrChildMotion[index];
    30.             if (currentChildMotion.motion.name.Contains("Run"))
    31.             {
    32.                 Debug.Log($"[{index}] - Run : {currentChildMotion.motion.name} new position={currentChildMotion.position}");
    33.             }
    34.             else if (!currentChildMotion.motion.name.Contains("Idle"))
    35.             {
    36.                 Debug.Log($"[{index}] - Walk : {currentChildMotion.motion.name} new position={currentChildMotion.position}");
    37.             }
    38.             else
    39.                 Debug.Log($"[{index}] - Idle : {currentChildMotion.motion.name} new position={Vector2.zero}");
    40.         }
    41.     }
     
    Last edited: Jul 26, 2021
  4. salikahicham5

    salikahicham5

    Joined:
    Apr 18, 2024
    Posts:
    5
    Hi guys how i can get the weight of each child motion of the blend tree (influence to the interpolated motion)