feat: 스킬 애니메이션 N클립 순차 재생 및 이름 기반 자동 매칭 시스템
- SkillData: skillClip/endClip 단일 필드를 animationClips 리스트로 통합
- Data_Skill_ 접두사 애셋 이름과 Anim_{key}_{순서} 클립을 자동 매칭
- 레거시 skillClip/endClip 데이터 자동 마이그레이션
- SkillController: 클립 시퀀스 내 순차 재생 로직 (TryPlayNextClipInSequence)
- baseSkillClip을 컨트롤러 Skill state에서 OnValidate로 자동 발견
- waitingForEndAnimation / IsInEndAnimation 제거
- BuildSimulationEngine: 전체 클립 duration 합산 및 모든 클립 OnEffect 이벤트 파싱
- PlayerAbnormalityVerificationRunner: GetSkillDuration 전체 클립 길이 합산으로 변경
- EnemyBase: IsInEndAnimation 참조 제거
- AnimationClipExtractor: animationClips 리스트 기반 relink/collect로 변경
- AnimationClipSkillDataMatcher: 클립 변경 시 관련 SkillData 자동 갱신 (AssetPostprocessor)
- BaseSkillClipAssigner: 모든 컨트롤러의 Skill state에 base clip 일괄 할당 에디터 메뉴
- pre-commit hook: Anim_ 네이밍 규칙에 {순서} 패턴 추가 및 Anim_↔Data_Skill_ 매칭 검증
This commit is contained in:
@@ -345,13 +345,28 @@ public static class AnimationClipExtractor
|
||||
SerializedObject so = new SerializedObject(skillData);
|
||||
bool modified = false;
|
||||
|
||||
if (TryRemapClip(so.FindProperty("skillClip"), fbxToAnimMap, path, "skillClip"))
|
||||
// animationClips 리스트의 각 요소를 remap합니다.
|
||||
SerializedProperty clipsProp = so.FindProperty("animationClips");
|
||||
if (clipsProp != null)
|
||||
{
|
||||
for (int i = 0; i < clipsProp.arraySize; i++)
|
||||
{
|
||||
if (TryRemapClip(clipsProp.GetArrayElementAtIndex(i), fbxToAnimMap, path, $"animationClips[{i}]"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 레거시 필드도 remap (마이그레이션 전 애셋 보호)
|
||||
if (TryRemapClip(so.FindProperty("legacySkillClip"), fbxToAnimMap, path, "legacySkillClip"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
}
|
||||
|
||||
if (TryRemapClip(so.FindProperty("endClip"), fbxToAnimMap, path, "endClip"))
|
||||
if (TryRemapClip(so.FindProperty("legacyEndClip"), fbxToAnimMap, path, "legacyEndClip"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
@@ -567,8 +582,11 @@ public static class AnimationClipExtractor
|
||||
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);
|
||||
IReadOnlyList<AnimationClip> skillClips = skillData.AnimationClips;
|
||||
for (int i = 0; i < skillClips.Count; i++)
|
||||
{
|
||||
if (skillClips[i] != null) clips.Add(skillClips[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── BossPhaseData ──
|
||||
|
||||
101
Assets/_Game/Scripts/Editor/AnimationClipSkillDataMatcher.cs
Normal file
101
Assets/_Game/Scripts/Editor/AnimationClipSkillDataMatcher.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Animations 폴더 내 애니메이션 클립이 변경(생성/수정/삭제/이동)되면
|
||||
/// 관련 SkillData의 애니메이션 클립 목록을 자동 갱신합니다.
|
||||
/// </summary>
|
||||
public class AnimationClipSkillDataMatcher : AssetPostprocessor
|
||||
{
|
||||
private const string AnimationsFolderPath = "Assets/_Game/Animations";
|
||||
|
||||
private static void OnPostprocessAllAssets(
|
||||
string[] importedAssets,
|
||||
string[] deletedAssets,
|
||||
string[] movedAssets,
|
||||
string[] movedFromAssetPaths)
|
||||
{
|
||||
bool hasAnimationChange = false;
|
||||
|
||||
for (int i = 0; i < importedAssets.Length; i++)
|
||||
{
|
||||
if (IsAnimationClipPath(importedAssets[i]))
|
||||
{
|
||||
hasAnimationChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnimationChange)
|
||||
{
|
||||
for (int i = 0; i < deletedAssets.Length; i++)
|
||||
{
|
||||
if (IsAnimationClipPath(deletedAssets[i]))
|
||||
{
|
||||
hasAnimationChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnimationChange)
|
||||
{
|
||||
for (int i = 0; i < movedAssets.Length; i++)
|
||||
{
|
||||
if (IsAnimationClipPath(movedAssets[i]) || IsAnimationClipPath(movedFromAssetPaths[i]))
|
||||
{
|
||||
hasAnimationChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnimationChange)
|
||||
return;
|
||||
|
||||
RefreshAllSkillDataClips();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 경로가 Animations 폴더 내의 애니메이션 클립인지 확인합니다.
|
||||
/// </summary>
|
||||
private static bool IsAnimationClipPath(string assetPath)
|
||||
{
|
||||
return assetPath != null
|
||||
&& assetPath.StartsWith(AnimationsFolderPath)
|
||||
&& assetPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 SkillData의 애니메이션 클립 목록을 갱신합니다.
|
||||
/// </summary>
|
||||
private static void RefreshAllSkillDataClips()
|
||||
{
|
||||
string[] skillGuids = AssetDatabase.FindAssets("t:SkillData");
|
||||
int refreshedCount = 0;
|
||||
|
||||
for (int i = 0; i < skillGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(skillGuids[i]);
|
||||
SkillData skillData = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
if (skillData == null) continue;
|
||||
|
||||
int clipCountBefore = skillData.AnimationClips.Count;
|
||||
skillData.RefreshAnimationClips();
|
||||
|
||||
if (skillData.AnimationClips.Count != clipCountBefore)
|
||||
refreshedCount++;
|
||||
}
|
||||
|
||||
if (refreshedCount > 0)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4524bdb16f89724b96d588200b8a3e8
|
||||
103
Assets/_Game/Scripts/Editor/BaseSkillClipAssigner.cs
Normal file
103
Assets/_Game/Scripts/Editor/BaseSkillClipAssigner.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// AnimatorController의 Skill 상태에 base clip을 일괄 할당하는 에디터 도구입니다.
|
||||
/// </summary>
|
||||
public static class BaseSkillClipAssigner
|
||||
{
|
||||
private const string SkillStateName = "Skill";
|
||||
|
||||
/// <summary>
|
||||
/// 선택한 애니메이션 클립을 모든 AnimatorController의 Skill 상태에 할당합니다.
|
||||
/// </summary>
|
||||
[MenuItem("Tools/Animation/Assign Base Skill Clip to All Controllers")]
|
||||
private static void AssignBaseClipToAllControllers()
|
||||
{
|
||||
AnimationClip selectedClip = Selection.activeObject as AnimationClip;
|
||||
if (selectedClip == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Base Skill Clip 할당",
|
||||
"Animations 폴더에서 할당할 AnimationClip을 선택한 후 다시 실행하세요.",
|
||||
"확인");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] controllerGuids = AssetDatabase.FindAssets("t:AnimatorController", new[] { "Assets/_Game" });
|
||||
int assignedCount = 0;
|
||||
|
||||
for (int i = 0; i < controllerGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(controllerGuids[i]);
|
||||
AnimatorController ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);
|
||||
if (ac == null) continue;
|
||||
|
||||
if (TryAssignClipToState(ac, SkillStateName, selectedClip))
|
||||
assignedCount++;
|
||||
}
|
||||
|
||||
if (assignedCount > 0)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog(
|
||||
"Base Skill Clip 할당 완료",
|
||||
$"{selectedClip.name}을(를) {assignedCount}개 컨트롤러의 Skill 상태에 할당했습니다.",
|
||||
"확인");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Base Skill Clip 할당",
|
||||
"Skill 상태를 가진 컨트롤러를 찾지 못했습니다.",
|
||||
"확인");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimatorController의 지정한 상태에 클립을 할당합니다.
|
||||
/// </summary>
|
||||
private static bool TryAssignClipToState(AnimatorController ac, string stateName, AnimationClip clip)
|
||||
{
|
||||
for (int i = 0; i < ac.layers.Length; i++)
|
||||
{
|
||||
if (TryAssignClipInStateMachine(ac.layers[i].stateMachine, stateName, clip))
|
||||
{
|
||||
EditorUtility.SetDirty(ac);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StateMachine을 재귀적으로 탐색하여 지정한 이름의 상태에 클립을 할당합니다.
|
||||
/// </summary>
|
||||
private static bool TryAssignClipInStateMachine(AnimatorStateMachine sm, string stateName, AnimationClip clip)
|
||||
{
|
||||
for (int i = 0; i < sm.states.Length; i++)
|
||||
{
|
||||
if (sm.states[i].state.name == stateName)
|
||||
{
|
||||
sm.states[i].state.motion = clip;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < sm.stateMachines.Length; i++)
|
||||
{
|
||||
if (TryAssignClipInStateMachine(sm.stateMachines[i].stateMachine, stateName, clip))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a6c131eb50620a47ab0250c9d46d438
|
||||
Reference in New Issue
Block a user