[Unity3D] 将 AnimationClip 指定帧的姿态数据应用回游戏对象

背景

游戏对象录制好动画后,再次拖回到首帧重新录制首帧动画,此时游戏对象的实际姿态跟 Animator 播放时首帧动画不一致
可能出现闪现现象,同时在关闭 Animation 录制面板的 "预览/Preview" 时,游戏对象的姿态回恢复到与动画首帧姿态不一样(姿态=Position+Rotation)的状态;
我亟需将这个首帧的姿态同步给游戏对象,确保游戏对象默认状态和首帧状态是一致的。

实现

可以通过手动将每个游戏对象的姿态从Animation面板拷贝过去,但是这样非常耗时间,使用下面代码实现将指定帧姿态应用到游戏对象上:

using System.Collections.Generic;
using UnityEngine;
namespace Bian
{
#if UNITY_EDITOR
    using UnityEditor;
    using UnityEditor.Animations;

    [CustomEditor(typeof(AnimationClipFrameSyncer))]
    public class AnimationClipFrameSyncerEditor : Editor
    {
        GUIContent syncButtonContent = new GUIContent("", "将动画状态机中指定帧的姿态同步到物体上");

        class StateInfo
        {
            public string stateName;
        }

        readonly List<StateInfo> stateInfos = new List<StateInfo>();
        string[] stateNames = System.Array.Empty<string>();
        int selectedIndex = -1;

        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            var myScript = (AnimationClipFrameSyncer)target;

            // 收集 Animator 的 State 列表
            FetchStateInfos(myScript.animator);

            // 为 fullStateName 提供下拉
            var propFullStateName = serializedObject.FindProperty("fullStateName");
            stateNames = new string[stateInfos.Count];
            selectedIndex = -1;
            for (int i = 0; i < stateInfos.Count; i++)
            {
                stateNames[i] = stateInfos[i].stateName;
                if (myScript.fullStateName == stateNames[i]) selectedIndex = i;
            }

            bool isMissing = false;
            if (!string.IsNullOrEmpty(myScript.fullStateName) && selectedIndex == -1 && stateInfos.Count > 0)
            {
                isMissing = true;
                var tempList = new List<string>(stateNames);
                tempList.Insert(0, myScript.fullStateName);
                stateNames = tempList.ToArray();
                selectedIndex = 0;
            }

            // 绘制除 fullStateName 以外的字段
            var iterator = serializedObject.GetIterator();
            bool enterChildren = true;
            while (iterator.NextVisible(enterChildren))
            {
                enterChildren = false;
                using var disabledScopeScr = new EditorGUI.DisabledScope(iterator.name == "m_Script");
                if (iterator.name == "fullStateName") continue;
                EditorGUILayout.PropertyField(iterator, true);
            }

            // FullStateName 下拉或提示
            if (stateInfos.Count > 0 || isMissing)
            {
                EditorGUI.BeginChangeCheck();
                int newIndex = EditorGUILayout.Popup("FullStateName", selectedIndex, stateNames);
                if (EditorGUI.EndChangeCheck() && newIndex >= 0 && newIndex < stateNames.Length)
                {
                    propFullStateName.stringValue = stateNames[newIndex];
                }

                if (isMissing)
                {
                    EditorGUILayout.HelpBox($"StateMissing: {myScript.fullStateName}", MessageType.Error);
                }
            }
            else
            {
                EditorGUILayout.HelpBox("没有找到 AnimatorStates。", MessageType.Warning);
            }

            serializedObject.ApplyModifiedProperties();

            // 同步按钮
            EditorGUILayout.Space();
            syncButtonContent.text = $"同步姿态 (Frame: {myScript.frame})";
            if (GUILayout.Button(syncButtonContent))
            {
                TrySyncPose(myScript);
            }
        }

        void FetchStateInfos(Animator animator)
        {
            stateInfos.Clear();
            if (animator == null || animator.runtimeAnimatorController == null) return;

            var controller = GetAnimatorController(animator);
            if (controller == null) return;

            for (int i = 0; i < controller.layers.Length; i++)
            {
                var layer = controller.layers[i];
                var layerName = layer.name;
                var states = layer.stateMachine.states;
                foreach (var item in states)
                {
                    var state = item.state;
                    if (state == null) continue;
                    stateInfos.Add(new StateInfo { stateName = string.Format("{0}.{1}", layerName, state.name) });
                }
            }
        }

        void TrySyncPose(AnimationClipFrameSyncer syncer)
        {
            if (syncer == null || syncer.animator == null)
            {
                Debug.LogError("AnimationClipFrameSyncer: 请先指定 Animator");
                return;
            }

            if (string.IsNullOrEmpty(syncer.fullStateName))
            {
                Debug.LogError("AnimationClipFrameSyncer: 请填写 fullStateName (格式: Layer.State)");
                return;
            }

            // 解析 Layer.State
            string layerName, stateName;
            if (!ParseFullStateName(syncer.fullStateName, out layerName, out stateName))
            {
                Debug.LogError("AnimationClipFrameSyncer: fullStateName 格式错误,应为 Layer.State");
                return;
            }

            var controller = GetAnimatorController(syncer.animator);
            if (controller == null)
            {
                Debug.LogError("AnimationClipFrameSyncer: 无法获取 AnimatorController");
                return;
            }

            // 查找 Layer
            int layerIndex = -1;
            AnimatorStateMachine stateMachine = null;
            for (int i = 0; i < controller.layers.Length; i++)
            {
                if (controller.layers[i].name == layerName)
                {
                    layerIndex = i;
                    stateMachine = controller.layers[i].stateMachine;
                    break;
                }
            }

            if (layerIndex < 0 || stateMachine == null)
            {
                Debug.LogError($"AnimationClipFrameSyncer: 未找到 Layer: {layerName}");
                return;
            }

            // 查找 State
            AnimatorState targetState = FindStateRecursive(stateMachine, stateName);
            if (targetState == null)
            {
                Debug.LogError($"AnimationClipFrameSyncer: 未找到状态: {stateName}");
                return;
            }

            // 解析 Motion 到 AnimationClip(支持 BlendTree)
            var clip = ResolveToClip(targetState.motion);
            if (clip == null)
            {
                Debug.LogError("AnimationClipFrameSyncer: 目标状态未关联 AnimationClip(或不支持的 Motion 类型)");
                return;
            }

            // 将指定帧数转换为时间
            float fps = Mathf.Max(1e-3f, clip.frameRate);
            float time = Mathf.Clamp(syncer.frame / fps, 0f, clip.length);

            // 准备采样 Root(使用 Animator 挂载对象作为根)
            var rootGO = syncer.animator.gameObject;
            var allTransforms = rootGO.GetComponentsInChildren<Transform>(true);

            // 预览采样,捕获整棵层级的局部位姿
            var localPositions = new Vector3[allTransforms.Length];
            var localRotations = new Quaternion[allTransforms.Length];

            AnimationMode.StartAnimationMode();
            try
            {
                AnimationMode.SampleAnimationClip(rootGO, clip, time);
                for (int i = 0; i < allTransforms.Length; i++)
                {
                    var t = allTransforms[i];
                    localPositions[i] = t.localPosition;
                    localRotations[i] = t.localRotation;
                }
            }
            finally
            {
                AnimationMode.StopAnimationMode();
            }

            // 应用到整棵层级(仅同步 Transform 的本地位置与旋转)
            Undo.RecordObjects(allTransforms, "Sync Hierarchy Pose From Animation");
            for (int i = 0; i < allTransforms.Length; i++)
            {
                allTransforms[i].localPosition = localPositions[i];
                allTransforms[i].localRotation = localRotations[i];
                EditorUtility.SetDirty(allTransforms[i]);
            }

            Debug.Log($"AnimationClipFrameSyncer: 已从 {syncer.fullStateName} 的第 {syncer.frame} 帧同步整层级姿态({allTransforms.Length} 个 Transform)");
        }

        static bool ParseFullStateName(string full, out string layer, out string state)
        {
            layer = null;
            state = null;
            if (string.IsNullOrEmpty(full)) return false;
            var parts = full.Split('.');
            if (parts.Length != 2) return false;
            layer = parts[0];
            state = parts[1];
            return !string.IsNullOrEmpty(layer) && !string.IsNullOrEmpty(state);
        }

        static AnimatorController GetAnimatorController(Animator animator)
        {
            if (animator == null) return null;
            var rac = animator.runtimeAnimatorController;
            if (rac == null) return null;

            // 处理 OverrideController
            var aoc = rac as AnimatorOverrideController;
            if (aoc != null)
            {
                return aoc.runtimeAnimatorController as AnimatorController;
            }

            return rac as AnimatorController;
        }

        static AnimatorState FindStateRecursive(AnimatorStateMachine sm, string stateName)
        {
            if (sm == null) return null;
            foreach (var cs in sm.states)
            {
                if (cs.state != null && cs.state.name == stateName)
                    return cs.state;
            }

            foreach (var child in sm.stateMachines)
            {
                var found = FindStateRecursive(child.stateMachine, stateName);
                if (found != null) return found;
            }

            return null;
        }

        static AnimationClip ResolveToClip(Motion motion)
        {
            if (motion == null) return null;
            var clip = motion as AnimationClip;
            if (clip != null) return clip;

            var tree = motion as BlendTree;
            if (tree != null)
            {
                // 选择第一个子节点的 Clip(简单策略)
                if (tree.children != null && tree.children.Length > 0)
                {
                    return ResolveToClip(tree.children[0].motion);
                }
            }

            return null;
        }
    }

#endif
    /// <summary>
    ///  用于将动画状态机中的指定的帧Property获取,如果是 Position 就同步改位置到物体,如果是 Rotation 就同步改旋转到物体。
    ///  多用于解决物体初始状态与动画机初始帧不一致的问题。
    ///  这里扩展成可以指定帧数的功能。
    /// </summary>
    public class AnimationClipFrameSyncer : MonoBehaviour
    {
        public int frame = 0;
        public Animator animator;
        public string fullStateName;

#if UNITY_EDITOR
        private void Reset()
        {
             animator = GetComponent<Animator>();
        }
#endif
    }
}

使用方式

  1. 将脚本内容拷贝到工程,命名为:AnimationClipFrameSyncer.cs
  2. 挂载在有 Animator 的脚本上
  3. 选择指定的状态机 State
  4. 指定帧为 0
  5. 点击按钮“同步姿态 (Frame: 0)” 即可完成首帧数据应用到游戏对象


Tips: 可以指定任意帧实现将任意帧的数据应用到游戏对象!

写到最后

如有更优解,欢迎讨论!

版权所有,转载请注明出处~

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容