How can I rename my creature's model's hierarchy without breaking animations?

Viewed 428

I am a beginner at Unity in terms of skill so please explain as if you were talking to a child if you can!

PROBLEM

I would like to change these names here:

enter image description here

I would like to rename them for two reasons:

  1. so they are more intelligible

  2. because I am using many different assets from the store and each has a different hierarchy with different names and I want to standardize the names so that I can use the below code to determine which part of the creature's body was shot so that it works for every creature

     public    void            CreatureHit(string bodyPart, GunInfo usedWeapon, float intensity)                    // for guns
     {
    
         usedWeapon.PlayHit(creatureSounds);
         if (creatureInfo.healthPoints > 0)                                                                        // to prevent dead creatures from being shot
         {
             if ("Torso" == bodyPart || "LeftUpperArm" == bodyPart                                                // if the part that was hit was the arms or torso
                     || "RightUpperArm" == bodyPart || "LeftLowerArm" == bodyPart                                // if the part that was hit was the arms or torso
                     || "RightLowerArm" == bodyPart)
             {
                 creatureInfo.healthPoints -= usedWeapon.damage * intensity;                                     // deal standard dmg
    
                 if (creatureInfo.healthPoints <= 0)
                     creatureInfo.deathType = CreatureInfo.BODYSHOT;
             }
             else if ("Head" == bodyPart)                                                                        // if the part that was hit was the head
             {
                 creatureInfo.healthPoints -= usedWeapon.damage * 10 * intensity;                                // deal 10x dmg
                 audioSource.PlayOneShot(creatureSounds.hitHead, 1);
    
    
                 if (creatureInfo.healthPoints <= 0)
                     creatureInfo.deathType = CreatureInfo.HEADSHOT;
             }
             else if ("RightUpperLeg" == bodyPart || "LeftUpperLeg" == bodyPart
                 || "RightLowerLeg" == bodyPart || "LeftLowerLeg" == bodyPart)
             {
                 creatureInfo.healthPoints -= usedWeapon.damage / 2 * intensity;                                    // deal half dmg
                 if (creatureInfo.healthPoints <= 0)
                     creatureInfo.deathType = CreatureInfo.BODYSHOT;
             }
         }
     }   
    

WHAT I TRIED

I renamed them in the hierarchy but then the animations stopped working. I found an old thread from the Unity forum asking if this was possible in 2015 and the OP was told that it wasn't. There were some later technical replies and I felt overwhelmed so I thought I should just create my own thread.

NOTE: there are multiple dozens of characters each with 10+ animations so ideally I need a very efficient solution.

2 Answers

In general you still can't unfortunately. (At least not that simple see below).

The AnimationClips are based on strings storing the relative path from the Animator to the according GameObject the type of the according component and finally the name of the animated serialized fields and properties.

If any of those change e.g. because you renamed the object or change the hierarchy in general the connection is lost and the animation breaks.

You could implement an editor script method that

  • goes through the affected Animator (GetComponentInParent) of the object
  • iterates through all used AnimationClips
  • iterates through each clips property bindings
  • redirects the property path accordingly to your renaming

This could look somewhat like this

private static void RenameObject(GameObject gameObject, Animator animator, string newName)
{
    if (!gameObject)
    {
        throw new ArgumentException("No object provided", nameof(gameObject));
    }

    if (string.IsNullOrWhiteSpace(newName))
    {
        throw new ArgumentException("Object name may not be empty!", nameof(newName));
    }

    if (!animator)
    {
        throw new ArgumentException($"Selected object {gameObject} is not a child of an {nameof(Animator)}!", nameof(gameObject));
    }

    if (gameObject.transform == animator.transform)
    {
        return;
    }

    // get the relative path from the animator root to this object's parent
    var path = AnimationUtility.CalculateTransformPath(gameObject.transform.parent, animator.transform);

    if (gameObject.transform.parent != animator.transform)
    {
        path += "/";
    }

    // then append the old and new names
    var oldPath = path + gameObject.name;
    var newPath = path + newName;

    // get the runtime Animation controller
    var controller = animator.runtimeAnimatorController;

    // get all clips used by this controller
    var clips = controller.animationClips;

    var changeableObjects = new List<Object>(clips.Length + 1) { gameObject };
    changeableObjects.AddRange(clips);

    Undo.RecordObjects(changeableObjects.ToArray(), "Change animated object name");

    // Go through all clips
    foreach (var clip in clips)
    {
        var floatBindingInfo = new List<AnimationFloatBindingInfo>();

        // Get and store all FLOAT keyframe bindings
        foreach (var binding in AnimationUtility.GetCurveBindings(clip))
        {
            var curve = AnimationUtility.GetEditorCurve(clip, binding);
            var curveInfo = new AnimationFloatBindingInfo(binding, curve);

            ReplaceBindingPath(curveInfo, oldPath, newPath);

            floatBindingInfo.Add(curveInfo);
        }

        var objectBindingInfos = new List<AnimationObjectBindingInfo>();

        // also do the same for all reference keyframe bindings 
        foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
        {
            var curve = AnimationUtility.GetObjectReferenceCurve(clip, binding);
            var curveInfo = new AnimationObjectBindingInfo(binding, curve);

            ReplaceBindingPath(curveInfo, oldPath, newPath);

            objectBindingInfos.Add(curveInfo);
        }

        // a little check to avoid unnecessary work -> are there any affected property curves at all?
        if (floatBindingInfo.Count + objectBindingInfos.Count > 0)
        {
            // Now erase all curves 
            clip.ClearCurves();

            // and assign back the stored ones
            AnimationUtility.SetEditorCurves(clip, floatBindingInfo.Select(info => info.Binding).ToArray(), floatBindingInfo.Select(info => info.Curve).ToArray());
            AnimationUtility.SetObjectReferenceCurves(clip, objectBindingInfos.Select(info => info.Binding).ToArray(), objectBindingInfos.Select(info => info.Curve).ToArray());

            EditorUtility.SetDirty(clip);
        }
    }

    // finally rename the object
    gameObject.name = newName;

    EditorUtility.SetDirty(gameObject);
}

Since this use case is quite common I took some time to implement an EditorWindow for this. It is still a bit raw but works and supports also undo redo ;) You can find it here

-> Select the object in the Hierarchy -> right click -> "Rename safe for Animator"

You could of course add some shortcut to it etc. that's up to you ;)

Here a little demo of the dialog in action, renaming some nested objects and also performing some undo/redo

enter image description here


However, an alternative in your use case to simply get your code to work with the names as they are might be using tags instead.

As I see it your code is based on three different cases so you could simply have a tag for each like e.g. Head, Arms, Legs and assign and check those accordingly (GameObject.CompareTag) and not touch the names and animations at all.

I have 2 plans.

  1. Create an empty GameObject under the node you want to rename, and attach the collider compoent on it.

    CATRigHub001Bone004
     └ CATRigHub001Bone004Bone001
        └ Rig  <-------- Collider
    
  2. Rename the bone in editor and create a script to automatically rename it to its original name while playing.

    public class Rename : MonoBehaviour
    {
        public string boneName;
        [NonSerialized] public string partName;
    
        void Awake()
        {
            partName = name;
            name = boneName;
        }
    }
    
Related