AnimationClip同步工具
用途:列出动画的第1帧与预制体GameObject当前值不同的,需要同步的可以手动同步
效果图
public struct ValueNotSameItem { public EditorCurveBinding curveBinding; //关联参数 public AnimationCurve animCurve; //动画曲线 public float kfValue; //动画曲线上第1帧的值 public float objValue; //预制体中GameObject上的值 }
public class AnimClipSyncToolWnd : EditorWindow { [MenuItem("MyTools/AnimClipSyncToolWnd", false, 1)] static void ShowWindow() { var win = GetWindow(typeof(AnimClipSyncToolWnd), false, "AnimClipSyncToolWnd"); win.minSize = new Vector2(500, 300); } private Vector2 m_ScrollPos; private GameObject m_PrefabAsset; //预制体资源 private Animator m_Animator; //预制体根节点上的Animator组件 private AnimationClip[] m_AnimClips; //Animator状态机下的所有动画文件 private string[] m_AnimClipOptions; private int m_AnimClipOptionIndex; private List<string> m_NodePathList = new List<string>(); //值不同的节点路径 private Dictionary<string, List<ValueNotSameItem>> m_ValueNotSameItemsDict = new Dictionary<string, List<ValueNotSameItem>>(); private bool m_PrefabRefreshFlag = false; private void OnEnable() { m_PrefabRefreshFlag = true; Undo.undoRedoPerformed += On_UndoRedoPerformed; } private void OnDisable() { Undo.undoRedoPerformed -= On_UndoRedoPerformed; } private void On_UndoRedoPerformed() { var grpName = Undo.GetCurrentGroupName(); Debug.Log($"undoRedo: {grpName}"); m_PrefabRefreshFlag = true; } private void OnGUI() { m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos); { OnGUI_ScrollView(); } EditorGUILayout.EndScrollView(); } private void OnGUI_ScrollView() { var prefabAsset = (GameObject)EditorGUILayout.ObjectField("Prefab", m_PrefabAsset, typeof(GameObject), false); bool isPrefabChange = m_PrefabAsset != prefabAsset; if (null != m_PrefabAsset) { if (GUILayout.Button($"Refresh")) isPrefabChange = true; } if (m_PrefabRefreshFlag || isPrefabChange) { m_PrefabRefreshFlag = false; m_PrefabAsset = prefabAsset; m_Animator = null; m_AnimClips = null; m_AnimClipOptions = null; m_AnimClipOptionIndex = 0; m_NodePathList.Clear(); m_ValueNotSameItemsDict.Clear(); if (null != m_PrefabAsset) { m_Animator = m_PrefabAsset.GetComponent<Animator>(); if (null != m_Animator) RefreshAnimClips(m_Animator); } } if (null == m_PrefabAsset) return; if (null == m_Animator) { EditorGUILayout.LabelField("No Animator !!!"); return; } foreach (var path in m_NodePathList) { var list = m_ValueNotSameItemsDict[path]; EditorGUILayout.LabelField(path); EditorGUI.indentLevel = 1; for (int j = 0; j < list.Count; ++j) { var item = list[j]; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"{item.curveBinding.propertyName}: obj: {item.objValue}, kf: {item.kfValue}"); if (GUILayout.Button("set as obj", GUILayout.Width(70))) { float delta = item.objValue - item.kfValue; item.kfValue = item.objValue; list[j] = item; //所有帧+delta值 var keyFrames = item.animCurve.keys; for (int i = 0; i < keyFrames.Length; ++i) { var kf = keyFrames[i]; kf.value = TrimFloat(kf.value + delta); item.animCurve.MoveKey(i, kf); } //保存 var clip = m_AnimClips[m_AnimClipOptionIndex]; Undo.RecordObject(clip, "all keyFrames+delta"); AnimationUtility.SetEditorCurve(clip, item.curveBinding, item.animCurve); EditorUtility.SetDirty(clip); AssetDatabase.SaveAssets(); Repaint(); } if (GUILayout.Button("set as kf", GUILayout.Width(70))) { item.objValue = item.kfValue; list[j] = item; OnClick_SetAsKf(item); } EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel = 0; EditorGUILayout.Space(5); //条目间隔 } } //刷新Animator状态机的所有AnimClip private void RefreshAnimClips(Animator animat) { m_AnimClips = animat.runtimeAnimatorController.animationClips; if (null != m_AnimClips && m_AnimClips.Length > 0) { m_AnimClipOptions = new string[m_AnimClips.Length]; for (int i = 0; i < m_AnimClips.Length; ++i) { var clip = m_AnimClips[i]; m_AnimClipOptions[i] = clip.name; } var curClip = m_AnimClips[m_AnimClipOptionIndex]; RefreshAnimCurves(curClip); } } //刷新AnimClip的所有动画曲线 private void RefreshAnimCurves(AnimationClip clip) { EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip); //一个property关联一个动画曲线 var prefabTf = m_PrefabAsset.transform; foreach (var b in bindings) { var curve = AnimationUtility.GetEditorCurve(clip, b); var kframes = curve.keys; if (kframes.Length <= 0) continue; var tf = prefabTf.Find(b.path); if (null == tf) continue; var kf0 = kframes[0]; CheckFirstKeyFrame(b, curve, tf, kf0); } } private void OnClick_SetAsKf(ValueNotSameItem item) { var tf = m_PrefabAsset.transform.Find(item.curveBinding.path); Undo.RecordObject(tf, "Set As kf"); var rtf = tf as RectTransform; switch (item.curveBinding.propertyName) { case "m_LocalPosition.x": { var pos = tf.localPosition; pos.x = item.kfValue; tf.localPosition = pos; break; } case "m_LocalPosition.y": { var pos = tf.localPosition; pos.y = item.kfValue; tf.localPosition = pos; break; } case "m_LocalPosition.z": { var pos = tf.localPosition; pos.z = item.kfValue; tf.localPosition = pos; break; } case "m_LocalScale.x": { var s = tf.localScale; s.x = item.kfValue; tf.localScale = s; break; } case "m_LocalScale.y": { var s = tf.localScale; s.y = item.kfValue; tf.localScale = s; break; } case "localEulerAnglesRaw.x": { var euler = tf.localEulerAngles; euler.x = item.kfValue; tf.localEulerAngles = euler; break; } case "localEulerAnglesRaw.y": { var euler = tf.localEulerAngles; euler.y = item.kfValue; tf.localEulerAngles = euler; break; } case "localEulerAnglesRaw.z": { var euler = tf.localEulerAngles; euler.z = item.kfValue; tf.localEulerAngles = euler; break; } case "m_AnchoredPosition.x": { var anPos = rtf.anchoredPosition; anPos.x = item.kfValue; rtf.anchoredPosition = anPos; break; } case "m_AnchoredPosition.y": { var anPos = rtf.anchoredPosition; anPos.y = item.kfValue; rtf.anchoredPosition = anPos; break; } case "m_SizeDelta.x": { var size = rtf.sizeDelta; size.x = item.kfValue; rtf.sizeDelta = size; break; } case "m_SizeDelta.y": { var size = rtf.sizeDelta; size.y = item.kfValue; rtf.sizeDelta = size; break; } } EditorUtility.SetDirty(m_PrefabAsset); //PrefabUtility.SavePrefabAsset(prefabAsset); AssetDatabase.SaveAssets(); Repaint(); } //检查第1帧的值, 与预制体中GameObject当前的值是否相同 /// <param name="animCurve">property关联的动画曲线</param> private void CheckFirstKeyFrame(EditorCurveBinding b, AnimationCurve animCurve, Transform tf, Keyframe kf0) { float objValue = 0; switch (b.propertyName) { case "m_LocalPosition.x": { objValue = tf.localPosition.x; break; } case "m_LocalPosition.y": { objValue = tf.localPosition.y; break; } case "m_LocalPosition.z": { objValue = tf.localPosition.z; break; } case "m_LocalScale.x": { objValue = tf.localScale.x; break; } case "m_LocalScale.y": { objValue = tf.localScale.y; break; } case "localEulerAnglesRaw.x": { objValue = tf.localEulerAngles.x; break; } case "localEulerAnglesRaw.y": { objValue = tf.localEulerAngles.y; break; } case "localEulerAnglesRaw.z": { objValue = tf.localEulerAngles.z; break; } case "m_AnchoredPosition.x": { var rtf = (RectTransform)tf; objValue = rtf.anchoredPosition.x; break; } case "m_AnchoredPosition.y": { var rtf = (RectTransform)tf; objValue = rtf.anchoredPosition.y; break; } case "m_SizeDelta.x": { var rtf = (RectTransform)tf; objValue = rtf.sizeDelta.x; break; } case "m_SizeDelta.y": { var rtf = (RectTransform)tf; objValue = rtf.sizeDelta.y; break; } default: return; } float delta = objValue - kf0.value; if (Mathf.Abs(delta) >= 0.001f) { if (!m_ValueNotSameItemsDict.TryGetValue(b.path, out var list)) { list = new List<ValueNotSameItem>(); m_ValueNotSameItemsDict.Add(b.path, list); m_NodePathList.Add(b.path); } var cframe = new ValueNotSameItem(); cframe.animCurve = animCurve; cframe.curveBinding = b; cframe.objValue = objValue; cframe.kfValue = kf0.value; list.Add(cframe); } } public static float TrimFloat(float f) { int i = (int)(f * 1000); float result = i / 1000.0f; return result; } }