用途:列出動畫的第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; } }