feat: 기본기3 브릿지 클립 및 애니메이션 편집 유틸 추가
- 기본기3 1타 브릿지 클립과 패턴/스킬 자산을 정리해 1타-2타 연결 흐름을 보강 - 애니메이션 브릿지 클립 생성 에디터 창을 추가해 앞뒤 프레임을 잘라 새 클립을 만들 수 있게 지원 - 선택한 애니메이션 클립의 마지막 프레임에 누락 키를 일괄 추가하는 에디터 메뉴를 추가
This commit is contained in:
165
Assets/_Game/Scripts/Editor/AnimationClipEndKeyBaker.cs
Normal file
165
Assets/_Game/Scripts/Editor/AnimationClipEndKeyBaker.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
/// <summary>
|
||||
/// 선택한 .anim 클립의 마지막 프레임에 모든 커브 키를 보강합니다.
|
||||
/// 애니메이션을 잘라서 연결할 때 마지막 포즈가 정확히 유지되도록 돕는 도구입니다.
|
||||
/// </summary>
|
||||
public static class AnimationClipEndKeyBaker
|
||||
{
|
||||
private const string MenuPath = "Tools/Animation/Bake End Frame Keys";
|
||||
|
||||
[MenuItem(MenuPath, false, 23)]
|
||||
public static void BakeSelectedClipEndKeys()
|
||||
{
|
||||
AnimationClip[] clips = Selection.GetFiltered<AnimationClip>(SelectionMode.Assets);
|
||||
if (clips == null || clips.Length == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Bake End Frame Keys",
|
||||
"마지막 프레임 키를 보강할 AnimationClip을 1개 이상 선택해 주세요.",
|
||||
"확인");
|
||||
return;
|
||||
}
|
||||
|
||||
int processedClipCount = 0;
|
||||
int addedFloatKeyCount = 0;
|
||||
int addedObjectKeyCount = 0;
|
||||
List<string> skippedClips = new List<string>();
|
||||
|
||||
for (int i = 0; i < clips.Length; i++)
|
||||
{
|
||||
AnimationClip clip = clips[i];
|
||||
string clipPath = AssetDatabase.GetAssetPath(clip);
|
||||
if (clip == null || string.IsNullOrEmpty(clipPath) || !clipPath.EndsWith(".anim"))
|
||||
{
|
||||
skippedClips.Add(clip != null ? clip.name : "(null)");
|
||||
continue;
|
||||
}
|
||||
|
||||
processedClipCount++;
|
||||
float lastFrameTime = clip.length;
|
||||
|
||||
EditorCurveBinding[] floatBindings = AnimationUtility.GetCurveBindings(clip);
|
||||
for (int bindingIndex = 0; bindingIndex < floatBindings.Length; bindingIndex++)
|
||||
{
|
||||
EditorCurveBinding binding = floatBindings[bindingIndex];
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(clip, binding);
|
||||
if (curve == null)
|
||||
continue;
|
||||
|
||||
if (HasFloatKeyAtTime(curve, lastFrameTime))
|
||||
continue;
|
||||
|
||||
float value = curve.Evaluate(lastFrameTime);
|
||||
int keyIndex = curve.AddKey(new Keyframe(lastFrameTime, value));
|
||||
if (keyIndex >= 0)
|
||||
{
|
||||
AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
|
||||
AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
|
||||
}
|
||||
|
||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||
addedFloatKeyCount++;
|
||||
}
|
||||
|
||||
EditorCurveBinding[] objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
|
||||
for (int bindingIndex = 0; bindingIndex < objectBindings.Length; bindingIndex++)
|
||||
{
|
||||
EditorCurveBinding binding = objectBindings[bindingIndex];
|
||||
ObjectReferenceKeyframe[] keys = AnimationUtility.GetObjectReferenceCurve(clip, binding);
|
||||
if (keys == null || keys.Length == 0)
|
||||
continue;
|
||||
|
||||
if (HasObjectKeyAtTime(keys, lastFrameTime))
|
||||
continue;
|
||||
|
||||
Object sampledValue = SampleObjectReferenceValue(keys, lastFrameTime);
|
||||
List<ObjectReferenceKeyframe> bakedKeys = new List<ObjectReferenceKeyframe>(keys)
|
||||
{
|
||||
new ObjectReferenceKeyframe
|
||||
{
|
||||
time = lastFrameTime,
|
||||
value = sampledValue
|
||||
}
|
||||
};
|
||||
|
||||
bakedKeys.Sort((a, b) => a.time.CompareTo(b.time));
|
||||
AnimationUtility.SetObjectReferenceCurve(clip, binding, bakedKeys.ToArray());
|
||||
addedObjectKeyCount++;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.ImportAsset(clipPath, ImportAssetOptions.ForceUpdate);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
string message =
|
||||
$"처리한 클립: {processedClipCount}개\n" +
|
||||
$"추가한 Float 키: {addedFloatKeyCount}개\n" +
|
||||
$"추가한 Object 키: {addedObjectKeyCount}개";
|
||||
|
||||
if (skippedClips.Count > 0)
|
||||
message += $"\n\n건너뛴 클립: {string.Join(", ", skippedClips)}";
|
||||
|
||||
Debug.Log($"[AnimationClipEndKeyBaker] {message.Replace('\n', ' ')}");
|
||||
EditorUtility.DisplayDialog("Bake End Frame Keys", message, "확인");
|
||||
}
|
||||
|
||||
[MenuItem(MenuPath, true)]
|
||||
public static bool ValidateBakeSelectedClipEndKeys()
|
||||
{
|
||||
return Selection.GetFiltered<AnimationClip>(SelectionMode.Assets).Length > 0;
|
||||
}
|
||||
|
||||
private static bool HasFloatKeyAtTime(AnimationCurve curve, float time)
|
||||
{
|
||||
if (curve == null)
|
||||
return false;
|
||||
|
||||
Keyframe[] keys = curve.keys;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (Mathf.Abs(keys[i].time - time) <= 0.0001f)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasObjectKeyAtTime(ObjectReferenceKeyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (Mathf.Abs(keys[i].time - time) <= 0.0001f)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Object SampleObjectReferenceValue(ObjectReferenceKeyframe[] keys, float time)
|
||||
{
|
||||
if (keys == null || keys.Length == 0)
|
||||
return null;
|
||||
|
||||
Object value = keys[0].value;
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
if (keys[i].time > time)
|
||||
break;
|
||||
|
||||
value = keys[i].value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user