using System.IO; using UnityEditor; using UnityEngine; using System.Collections.Generic; public class AnimationEditor { [MenuItem("Assets/Scale Animation Transforms...", true)] private static bool ValidateScaleMenu() { // enable only when an AnimationClip is selected return Selection.objects != null && Selection.objects.Length > 0 && System.Array.Exists(Selection.objects, o => o is AnimationClip); } [MenuItem("Assets/Scale Animation Transforms...")] private static void ScaleSelectedClips() { // default scale - you can change or prompt float scale = 2.0f; var clips = new List(); foreach (var o in Selection.objects) if (o is AnimationClip clip) clips.Add(clip); if (clips.Count == 0) { EditorUtility.DisplayDialog("Scale Animation", "No AnimationClip selected.", "OK"); return; } if (!EditorUtility.DisplayDialog("Scale Animation", $"Scale translations in {clips.Count} clip(s) by {scale}?", "Yes", "No")) return; foreach (AnimationClip clip in clips) { ScaleClip(clip, scale); } EditorUtility.DisplayDialog("Scale Animation", "Done scaling selected clips.", "OK"); } public static void ScaleClip(AnimationClip clip, float scale, string saveFolder) { string origPath = AssetDatabase.GetAssetPath(clip); string ext = Path.GetExtension(origPath); string nameNoExt = Path.GetFileNameWithoutExtension(origPath); string fileName = $"{nameNoExt}_scaled{ext}"; string savePath = Path.Combine(saveFolder, fileName).Replace("\\", "/"); if (AssetDatabase.LoadAssetAtPath(savePath) != null) { if (!AssetDatabase.DeleteAsset(savePath)) { Debug.LogError($"Failed to delete existing asset at {savePath}"); return; } Debug.Log($"Deleted old {savePath}"); AssetDatabase.Refresh(); } if (!AssetDatabase.CopyAsset(origPath, savePath)) { Debug.LogError($"Failed to duplicate asset: {origPath} -> {savePath}"); return; } AssetDatabase.ImportAsset(savePath); AnimationClip newClip = AssetDatabase.LoadAssetAtPath(savePath); if (newClip == null) { Debug.LogError($"Failed to load duplicated clip at: {savePath}"); return; } ScaleClip(newClip, scale); } public static void ScaleClip(AnimationClip clip, float scale) { // make editable copy if needed (if clip is in model import, make sure to duplicate or modify import settings) Undo.RegisterCompleteObjectUndo(clip, "Scale Animation Translations"); // Collect curve bindings for position-like properties. EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(clip); foreach (EditorCurveBinding binding in bindings) { string prop = binding.propertyName; if (string.IsNullOrEmpty(prop)) continue; // Identify position/localPosition/property patterns to scale: // common property names: // "m_LocalPosition.x", "m_LocalPosition.y", "m_LocalPosition.z" // "localPosition.x", etc. or custom paths that animate transform.localPosition // We'll scale any curve with "position" or "LocalPosition" in propertyName (case-insensitive). string lower = prop.ToLowerInvariant(); bool isPos = lower.Contains("m_localposition") || lower.Contains("localposition") || lower.Contains(".position") || lower == "position.x" || lower == "position.y" || lower == "position.z"; if (!isPos) continue; AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding); if (curve == null) continue; // scale each keyframe value Keyframe[] keys = curve.keys; for (int i = 0; i < keys.Length; i++) { Keyframe k = keys[i]; k.value *= scale; k.inTangent *= scale; k.outTangent *= scale; keys[i] = k; } curve.keys = keys; AnimationUtility.SetEditorCurve(clip, binding, curve); } EditorUtility.SetDirty(clip); AssetDatabase.SaveAssets(); } }