refactor: FBX 애니메이션 클립 독립 .anim 추출 및 자동 등록 시스템 구축
- FBX 내장 AnimationClip을 개별 .anim 파일로 추출하는 에디터 툴 추가 (AnimationClipExtractor) - 스킬/보스/AnimatorController에서 참조 중인 클립만 선택적 추출 - 추출 후 모든 참조(SkillData, BossPhaseData, AnimatorController)를 .anim으로 자동 relink - 추출 완료된 FBX 자동 삭제 (참조 안전성 검증 포함) - SkillController: registeredClips를 OnValidate에서 _Player_ 이름 기반 자동 등록 - PlayerSkillInput: skillSlots를 OnValidate에서 _Skill_Player_ 이름 기반 자동 등록 - 38개 FBX 삭제, 40+개 .anim 파일로 교체 완료
This commit is contained in:
680
Assets/_Game/Scripts/Editor/AnimationClipExtractor.cs
Normal file
680
Assets/_Game/Scripts/Editor/AnimationClipExtractor.cs
Normal file
@@ -0,0 +1,680 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
using UnityEditor.Animations;
|
||||
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
/// <summary>
|
||||
/// 스킬/보스/애니메이션 컨트롤러에서 참조 중인 FBX 내장 AnimationClip을 개별 .anim으로 추출하고,
|
||||
/// 모든 참조를 갱신한 뒤 원본 FBX를 삭제하는 에디터 도구입니다.
|
||||
/// </summary>
|
||||
public static class AnimationClipExtractor
|
||||
{
|
||||
private const string MenuPathExtract = "Tools/Animation/Extract Clips from FBX";
|
||||
private const string MenuPathRelink = "Tools/Animation/Relink Clips to Extracted .anim";
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Extract + Relink + Delete FBX (메인 진입점)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
[MenuItem(MenuPathExtract)]
|
||||
public static void ExtractAndRelink()
|
||||
{
|
||||
// ── 1. 참조 중인 클립 수집 (SkillData + BossPhaseData + AnimatorController) ──
|
||||
HashSet<AnimationClip> referencedClips = CollectReferencedClips();
|
||||
|
||||
if (referencedClips.Count == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Animation Clip Extractor",
|
||||
"스킬/보스/컨트롤러에서 참조 중인 AnimationClip이 없습니다.",
|
||||
"확인");
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 2. FBX 서브에셋인 클립만 필터링 ──
|
||||
var extractPlan = new List<(string fbxPath, AnimationClip clip, string clipName, string outputPath)>();
|
||||
int skipStandalone = 0;
|
||||
int skipExisting = 0;
|
||||
|
||||
foreach (AnimationClip clip in referencedClips)
|
||||
{
|
||||
if (clip == null) continue;
|
||||
|
||||
string assetPath = AssetDatabase.GetAssetPath(clip);
|
||||
|
||||
if (assetPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
skipStandalone++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!assetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
skipStandalone++;
|
||||
continue;
|
||||
}
|
||||
|
||||
string fbxDir = Path.GetDirectoryName(assetPath).Replace("\\", "/");
|
||||
string fbxName = Path.GetFileNameWithoutExtension(assetPath);
|
||||
string outputPath = $"{fbxDir}/{fbxName}.anim";
|
||||
|
||||
if (AssetDatabase.AssetPathToGUID(outputPath) != string.Empty)
|
||||
{
|
||||
skipExisting++;
|
||||
continue;
|
||||
}
|
||||
|
||||
extractPlan.Add((assetPath, clip, fbxName, outputPath));
|
||||
}
|
||||
|
||||
// ── 3. 요약 ──
|
||||
string summary = $"참조 중인 클립: {referencedClips.Count}개\n"
|
||||
+ $"추출 대상: {extractPlan.Count}개\n";
|
||||
|
||||
if (skipStandalone > 0)
|
||||
summary += $"이미 독립 파일: {skipStandalone}개 (건너뜀)\n";
|
||||
if (skipExisting > 0)
|
||||
summary += $"이미 추출됨: {skipExisting}개 (건너뜀)\n";
|
||||
|
||||
if (extractPlan.Count == 0)
|
||||
{
|
||||
bool doRelink = EditorUtility.DisplayDialog(
|
||||
"Animation Clip Extractor",
|
||||
$"{summary}\n추출할 클립이 없습니다.\n\n기존 추출된 .anim으로 참조를 갱신하시겠습니까?",
|
||||
"Relink", "취소");
|
||||
|
||||
if (doRelink)
|
||||
{
|
||||
int relinked = PerformRelink();
|
||||
PerformDeleteFbx();
|
||||
EditorUtility.DisplayDialog("Animation Clip Extractor",
|
||||
$"Relink: {relinked}개 참조 갱신", "확인");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
summary += "\n── 추출 대상 ──\n";
|
||||
foreach (var (fbxPath, clip, clipName, _) in extractPlan)
|
||||
summary += $" {clip.name} → {clipName}.anim\n";
|
||||
|
||||
// 삭제될 FBX 목록 (중복 제거)
|
||||
var fbxsToDelete = new HashSet<string>(extractPlan.Select(e => e.fbxPath));
|
||||
summary += $"\n── 삭제될 FBX ({fbxsToDelete.Count}개) ──\n";
|
||||
foreach (string fbx in fbxsToDelete.OrderBy(p => p))
|
||||
summary += $" {Path.GetFileName(fbx)}\n";
|
||||
|
||||
summary += "\n추출 → 참조 갱신 → FBX 삭제 순서로 진행합니다.";
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Animation Clip Extractor",
|
||||
$"{summary}\n진행하시겠습니까?",
|
||||
"진행", "취소"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 4. 클립 추출 ──
|
||||
AssetDatabase.StartAssetEditing();
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var (fbxPath, sourceClip, clipName, outputPath) in extractPlan)
|
||||
{
|
||||
try
|
||||
{
|
||||
AnimationClip extractedClip = ExtractClip(sourceClip, clipName);
|
||||
string savePath = AssetDatabase.GenerateUniqueAssetPath(outputPath);
|
||||
|
||||
AssetDatabase.CreateAsset(extractedClip, savePath);
|
||||
Debug.Log($"[AnimationClipExtractor] 추출: {sourceClip.name} → {savePath}");
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[AnimationClipExtractor] 추출 실패: {sourceClip.name} ({fbxPath})\n{e}");
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AssetDatabase.StopAssetEditing();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
// ── 5. Relink (모든 참조 소스) ──
|
||||
int relinkCount = PerformRelink();
|
||||
|
||||
// ── 6. FBX 삭제 ──
|
||||
int deletedCount = PerformDeleteFbx();
|
||||
|
||||
// ── 7. 결과 ──
|
||||
string resultMsg = $"추출: {successCount}개 성공";
|
||||
if (errorCount > 0) resultMsg += $", {errorCount}개 실패";
|
||||
resultMsg += $"\nRelink: {relinkCount}개 참조 갱신";
|
||||
resultMsg += $"\nFBX 삭제: {deletedCount}개";
|
||||
|
||||
Debug.Log($"[AnimationClipExtractor] {resultMsg}");
|
||||
EditorUtility.DisplayDialog("Animation Clip Extractor", resultMsg, "확인");
|
||||
}
|
||||
|
||||
[MenuItem(MenuPathExtract, true)]
|
||||
public static bool ValidateExtract()
|
||||
{
|
||||
return !EditorApplication.isPlayingOrWillChangePlaymode;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Relink Only (별도 메뉴)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
[MenuItem(MenuPathRelink)]
|
||||
public static void RelinkOnly()
|
||||
{
|
||||
int count = PerformRelink();
|
||||
PerformDeleteFbx();
|
||||
|
||||
EditorUtility.DisplayDialog(
|
||||
"Animation Clip Relink",
|
||||
$"{count}개 참조를 갱신했습니다.",
|
||||
"확인");
|
||||
}
|
||||
|
||||
[MenuItem(MenuPathRelink, true)]
|
||||
public static bool ValidateRelink()
|
||||
{
|
||||
return !EditorApplication.isPlayingOrWillChangePlaymode;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// FBX 삭제
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 추출 완료된 FBX를 삭제합니다.
|
||||
/// {FBX이름}.anim이 동일 폴더에 존재하면, 해당 FBX의 모든 서브에셋 클립이
|
||||
/// 추출된 것으로 간주하고 삭제합니다.
|
||||
/// </summary>
|
||||
/// <returns>삭제된 FBX 수</returns>
|
||||
private static int PerformDeleteFbx()
|
||||
{
|
||||
var fbxToAnimMap = BuildFbxToAnimMap();
|
||||
int deletedCount = 0;
|
||||
|
||||
AssetDatabase.StartAssetEditing();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var (fbxPath, _) in fbxToAnimMap)
|
||||
{
|
||||
// FBX 내의 서브에셋 클립이 모두 .anim으로 교체되었는지 확인
|
||||
UnityEngine.Object[] subAssets = AssetDatabase.LoadAllAssetsAtPath(fbxPath);
|
||||
AnimationClip[] clips = subAssets.OfType<AnimationClip>().ToArray();
|
||||
|
||||
if (clips.Length == 0)
|
||||
continue;
|
||||
|
||||
// 모든 클립이 추출된 .anim으로 relink되었는지 확인
|
||||
bool allRelinked = true;
|
||||
foreach (AnimationClip clip in clips)
|
||||
{
|
||||
string clipAssetPath = AssetDatabase.GetAssetPath(clip);
|
||||
|
||||
// FBX 서브에셋 클립이 여전히 FBX에만 있는지 확인
|
||||
if (clipAssetPath == fbxPath)
|
||||
{
|
||||
// 이 클립이 누군가 참조하고 있는지 확인
|
||||
if (IsClipReferencedByAnyAsset(clip))
|
||||
{
|
||||
allRelinked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allRelinked)
|
||||
{
|
||||
Debug.LogWarning($"[AnimationClipExtractor] FBX 삭제 스킵 (아직 참조 중인 클립이 있음): {fbxPath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
AssetDatabase.DeleteAsset(fbxPath);
|
||||
Debug.Log($"[AnimationClipExtractor] FBX 삭제: {fbxPath}");
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AssetDatabase.StopAssetEditing();
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 클립이 프로젝트 내 어떤 에셋에서 참조되고 있는지 확인합니다.
|
||||
/// </summary>
|
||||
private static bool IsClipReferencedByAnyAsset(AnimationClip clip)
|
||||
{
|
||||
if (clip == null) return false;
|
||||
|
||||
string clipPath = AssetDatabase.GetAssetPath(clip);
|
||||
int clipInstanceId = clip.GetInstanceID();
|
||||
|
||||
// SkillData에서 참조 확인
|
||||
string[] skillGuids = AssetDatabase.FindAssets("t:SkillData");
|
||||
foreach (string guid in skillGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string[] deps = AssetDatabase.GetDependencies(path, false);
|
||||
if (deps.Contains(clipPath))
|
||||
return true;
|
||||
}
|
||||
|
||||
// BossPhaseData에서 참조 확인
|
||||
string[] phaseGuids = AssetDatabase.FindAssets("t:BossPhaseData");
|
||||
foreach (string guid in phaseGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string[] deps = AssetDatabase.GetDependencies(path, false);
|
||||
if (deps.Contains(clipPath))
|
||||
return true;
|
||||
}
|
||||
|
||||
// AnimatorController에서 참조 확인
|
||||
string[] controllerGuids = AssetDatabase.FindAssets("t:AnimatorController", new[] { "Assets/_Game" });
|
||||
foreach (string guid in controllerGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string[] deps = AssetDatabase.GetDependencies(path, false);
|
||||
if (deps.Contains(clipPath))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Relink 핵심 로직
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// SkillData / BossPhaseData / AnimatorController에서 FBX 서브에셋 클립 참조를
|
||||
/// 추출된 .anim 파일로 교체합니다.
|
||||
/// </summary>
|
||||
/// <returns>갱신된 참조 수</returns>
|
||||
private static int PerformRelink()
|
||||
{
|
||||
var fbxToAnimMap = BuildFbxToAnimMap();
|
||||
|
||||
if (fbxToAnimMap.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[AnimationClipExtractor] 매핑 가능한 FBX→.anim 쌍이 없습니다.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int relinkCount = 0;
|
||||
|
||||
AssetDatabase.StartAssetEditing();
|
||||
|
||||
try
|
||||
{
|
||||
// ── SkillData relink ──
|
||||
string[] skillGuids = AssetDatabase.FindAssets("t:SkillData");
|
||||
foreach (string guid in skillGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
SkillData skillData = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
if (skillData == null) continue;
|
||||
|
||||
SerializedObject so = new SerializedObject(skillData);
|
||||
bool modified = false;
|
||||
|
||||
if (TryRemapClip(so.FindProperty("skillClip"), fbxToAnimMap, path, "skillClip"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
}
|
||||
|
||||
if (TryRemapClip(so.FindProperty("endClip"), fbxToAnimMap, path, "endClip"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
}
|
||||
|
||||
if (modified)
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
// ── BossPhaseData relink ──
|
||||
string[] phaseGuids = AssetDatabase.FindAssets("t:BossPhaseData");
|
||||
foreach (string guid in phaseGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
BossPhaseData phaseData = AssetDatabase.LoadAssetAtPath<BossPhaseData>(path);
|
||||
if (phaseData == null) continue;
|
||||
|
||||
SerializedObject so = new SerializedObject(phaseData);
|
||||
|
||||
if (TryRemapClip(so.FindProperty("phaseStartAnimation"), fbxToAnimMap, path, "phaseStartAnimation"))
|
||||
{
|
||||
so.ApplyModifiedProperties();
|
||||
relinkCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── AnimatorController relink ──
|
||||
string[] controllerGuids = AssetDatabase.FindAssets("t:AnimatorController", new[] { "Assets/_Game" });
|
||||
foreach (string guid in controllerGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);
|
||||
if (controller == null) continue;
|
||||
|
||||
int remapped = RemapControllerClips(controller, fbxToAnimMap, path);
|
||||
if (remapped > 0)
|
||||
{
|
||||
EditorUtility.SetDirty(controller);
|
||||
relinkCount += remapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AssetDatabase.StopAssetEditing();
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
if (relinkCount > 0)
|
||||
Debug.Log($"[AnimationClipExtractor] Relink 완료: {relinkCount}개 참조 갱신됨");
|
||||
|
||||
return relinkCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimatorController의 모든 상태에서 FBX 서브에셋 클립을 .anim으로 교체합니다.
|
||||
/// </summary>
|
||||
private static int RemapControllerClips(
|
||||
AnimatorController controller,
|
||||
Dictionary<string, AnimationClip> fbxToAnimMap,
|
||||
string controllerPath)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
// 컨트롤러의 모든 StateMachine과 State를 순회
|
||||
foreach (AnimatorControllerLayer layer in controller.layers)
|
||||
{
|
||||
if (layer.stateMachine == null) continue;
|
||||
count += RemapStateMachineClips(layer.stateMachine, fbxToAnimMap, controllerPath);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StateMachine 내의 모든 상태에서 클립 참조를 교체합니다.
|
||||
/// </summary>
|
||||
private static int RemapStateMachineClips(
|
||||
AnimatorStateMachine stateMachine,
|
||||
Dictionary<string, AnimationClip> fbxToAnimMap,
|
||||
string controllerPath)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
// 일반 State
|
||||
foreach (ChildAnimatorState childState in stateMachine.states)
|
||||
{
|
||||
count += TryRemapMotion(childState.state, fbxToAnimMap, controllerPath);
|
||||
}
|
||||
|
||||
// BlendTree (하위 상태 머신 포함)
|
||||
foreach (ChildAnimatorStateMachine childSm in stateMachine.stateMachines)
|
||||
{
|
||||
count += TryRemapMotion(childSm.stateMachine.states[0].state, fbxToAnimMap, controllerPath);
|
||||
|
||||
// 하위 StateMachine 재귀
|
||||
if (childSm.stateMachine != null)
|
||||
count += RemapStateMachineClips(childSm.stateMachine, fbxToAnimMap, controllerPath);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimatorState의 Motion(AnimationClip)을 .anim으로 교체합니다.
|
||||
/// </summary>
|
||||
private static int TryRemapMotion(
|
||||
AnimatorState state,
|
||||
Dictionary<string, AnimationClip> fbxToAnimMap,
|
||||
string controllerPath)
|
||||
{
|
||||
if (state == null || state.motion == null)
|
||||
return 0;
|
||||
|
||||
AnimationClip currentClip = state.motion as AnimationClip;
|
||||
if (currentClip == null)
|
||||
return 0;
|
||||
|
||||
string clipPath = AssetDatabase.GetAssetPath(currentClip);
|
||||
|
||||
// 이미 .anim이거나 FBX 서브에셋이 아니면 스킵
|
||||
if (clipPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
||||
return 0;
|
||||
if (!clipPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase))
|
||||
return 0;
|
||||
|
||||
if (!fbxToAnimMap.TryGetValue(clipPath, out AnimationClip replacementClip))
|
||||
return 0;
|
||||
|
||||
state.motion = replacementClip;
|
||||
Debug.Log($"[AnimationClipExtractor] Controller Relink: {Path.GetFileName(controllerPath)}"
|
||||
+ $" | {currentClip.name} → {replacementClip.name}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FBX 경로 → 추출된 .anim AnimationClip 매핑을 구축합니다.
|
||||
/// </summary>
|
||||
private static Dictionary<string, AnimationClip> BuildFbxToAnimMap()
|
||||
{
|
||||
var map = new Dictionary<string, AnimationClip>();
|
||||
|
||||
string[] animGuids = AssetDatabase.FindAssets("t:AnimationClip", new[] { "Assets/_Game" });
|
||||
|
||||
foreach (string guid in animGuids)
|
||||
{
|
||||
string animPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!animPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
string animName = Path.GetFileNameWithoutExtension(animPath);
|
||||
string fbxPath = $"{Path.GetDirectoryName(animPath).Replace("\\", "/")}/{animName}.fbx";
|
||||
|
||||
if (AssetDatabase.AssetPathToGUID(fbxPath) != string.Empty)
|
||||
{
|
||||
AnimationClip animClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(animPath);
|
||||
if (animClip != null)
|
||||
map[fbxPath] = animClip;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SerializedProperty의 AnimationClip 참조가 FBX 서브에셋이면 교체합니다.
|
||||
/// </summary>
|
||||
private static bool TryRemapClip(
|
||||
SerializedProperty clipProperty,
|
||||
Dictionary<string, AnimationClip> fbxToAnimMap,
|
||||
string assetPath,
|
||||
string fieldName)
|
||||
{
|
||||
if (clipProperty == null || clipProperty.propertyType != SerializedPropertyType.ObjectReference)
|
||||
return false;
|
||||
|
||||
AnimationClip currentClip = clipProperty.objectReferenceValue as AnimationClip;
|
||||
if (currentClip == null) return false;
|
||||
|
||||
string clipAssetPath = AssetDatabase.GetAssetPath(currentClip);
|
||||
|
||||
if (clipAssetPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (!clipAssetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
if (!fbxToAnimMap.TryGetValue(clipAssetPath, out AnimationClip replacementClip))
|
||||
return false;
|
||||
|
||||
clipProperty.objectReferenceValue = replacementClip;
|
||||
Debug.Log($"[AnimationClipExtractor] Relink: {Path.GetFileName(assetPath)}.{fieldName}"
|
||||
+ $" | {currentClip.name} → {replacementClip.name}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 참조 클립 수집
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 프로젝트 내 SkillData, BossPhaseData, AnimatorController에서 참조 중인 AnimationClip을 수집합니다.
|
||||
/// </summary>
|
||||
private static HashSet<AnimationClip> CollectReferencedClips()
|
||||
{
|
||||
var clips = new HashSet<AnimationClip>();
|
||||
|
||||
// ── SkillData ──
|
||||
string[] skillGuids = AssetDatabase.FindAssets("t:SkillData");
|
||||
foreach (string guid in skillGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
SkillData skillData = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
if (skillData == null) continue;
|
||||
|
||||
if (skillData.SkillClip != null) clips.Add(skillData.SkillClip);
|
||||
if (skillData.EndClip != null) clips.Add(skillData.EndClip);
|
||||
}
|
||||
|
||||
// ── BossPhaseData ──
|
||||
string[] phaseGuids = AssetDatabase.FindAssets("t:BossPhaseData");
|
||||
foreach (string guid in phaseGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
BossPhaseData phaseData = AssetDatabase.LoadAssetAtPath<BossPhaseData>(path);
|
||||
if (phaseData == null) continue;
|
||||
|
||||
if (phaseData.PhaseStartAnimation != null) clips.Add(phaseData.PhaseStartAnimation);
|
||||
}
|
||||
|
||||
// ── AnimatorController ──
|
||||
string[] controllerGuids = AssetDatabase.FindAssets("t:AnimatorController", new[] { "Assets/_Game" });
|
||||
foreach (string guid in controllerGuids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
AnimatorController controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);
|
||||
if (controller == null) continue;
|
||||
|
||||
CollectClipsFromController(controller, clips);
|
||||
}
|
||||
|
||||
return clips;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimatorController의 모든 상태에서 AnimationClip을 수집합니다.
|
||||
/// </summary>
|
||||
private static void CollectClipsFromController(AnimatorController controller, HashSet<AnimationClip> clips)
|
||||
{
|
||||
foreach (AnimatorControllerLayer layer in controller.layers)
|
||||
{
|
||||
if (layer.stateMachine == null) continue;
|
||||
CollectClipsFromStateMachine(layer.stateMachine, clips);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StateMachine 내의 모든 상태에서 AnimationClip을 수집합니다.
|
||||
/// </summary>
|
||||
private static void CollectClipsFromStateMachine(AnimatorStateMachine stateMachine, HashSet<AnimationClip> clips)
|
||||
{
|
||||
foreach (ChildAnimatorState childState in stateMachine.states)
|
||||
{
|
||||
if (childState.state?.motion is AnimationClip clip)
|
||||
clips.Add(clip);
|
||||
}
|
||||
|
||||
foreach (ChildAnimatorStateMachine childSm in stateMachine.stateMachines)
|
||||
{
|
||||
if (childSm.stateMachine != null)
|
||||
CollectClipsFromStateMachine(childSm.stateMachine, clips);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 클립 추출
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 소스 클립의 모든 커브와 이벤트를 복사한 독립 AnimationClip을 생성합니다.
|
||||
/// </summary>
|
||||
private static AnimationClip ExtractClip(AnimationClip source, string clipName)
|
||||
{
|
||||
AnimationClip extracted = new AnimationClip
|
||||
{
|
||||
name = clipName,
|
||||
frameRate = source.frameRate,
|
||||
legacy = source.legacy,
|
||||
wrapMode = source.wrapMode,
|
||||
localBounds = source.localBounds
|
||||
};
|
||||
|
||||
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(source);
|
||||
foreach (EditorCurveBinding binding in bindings)
|
||||
{
|
||||
AnimationCurve curve = AnimationUtility.GetEditorCurve(source, binding);
|
||||
if (curve == null || curve.keys.Length == 0)
|
||||
continue;
|
||||
|
||||
extracted.SetCurve(binding.path, binding.type, binding.propertyName, curve);
|
||||
}
|
||||
|
||||
AnimationEvent[] events = AnimationUtility.GetAnimationEvents(source);
|
||||
if (events.Length > 0)
|
||||
{
|
||||
AnimationEvent[] copiedEvents = new AnimationEvent[events.Length];
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
{
|
||||
copiedEvents[i] = new AnimationEvent
|
||||
{
|
||||
time = events[i].time,
|
||||
functionName = events[i].functionName,
|
||||
floatParameter = events[i].floatParameter,
|
||||
intParameter = events[i].intParameter,
|
||||
stringParameter = events[i].stringParameter,
|
||||
objectReferenceParameter = events[i].objectReferenceParameter,
|
||||
messageOptions = events[i].messageOptions
|
||||
};
|
||||
}
|
||||
|
||||
AnimationUtility.SetAnimationEvents(extracted, copiedEvents);
|
||||
}
|
||||
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba07c71a5e8314d4bbcf8f57382e2a87
|
||||
Reference in New Issue
Block a user