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:
@@ -56,6 +56,120 @@ namespace Colosseum.Skills
|
||||
[CreateAssetMenu(fileName = "NewSkill", menuName = "Colosseum/Skill")]
|
||||
public class SkillData : ScriptableObject
|
||||
{
|
||||
private const string SkillAssetPrefix = "Data_Skill_";
|
||||
private const string ClipAssetPrefix = "Anim_";
|
||||
private const string AnimationsSearchPath = "Assets/_Game/Animations";
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// 레거시 마이그레이션 및 애니메이션 클립 자동 매칭.
|
||||
/// 에디터에서 애셋 로드/수정 시 자동 실행됩니다.
|
||||
/// </summary>
|
||||
private void OnValidate()
|
||||
{
|
||||
MigrateLegacyClips();
|
||||
RefreshAnimationClips();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 skillClip/endClip 데이터를 animationClips 리스트로 이관합니다.
|
||||
/// </summary>
|
||||
private void MigrateLegacyClips()
|
||||
{
|
||||
if (legacySkillClip == null && legacyEndClip == null)
|
||||
return;
|
||||
|
||||
if (animationClips.Count > 0)
|
||||
return;
|
||||
|
||||
if (legacySkillClip != null)
|
||||
{
|
||||
animationClips.Add(legacySkillClip);
|
||||
legacySkillClip = null;
|
||||
}
|
||||
|
||||
if (legacyEndClip != null)
|
||||
{
|
||||
animationClips.Add(legacyEndClip);
|
||||
legacyEndClip = null;
|
||||
}
|
||||
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
Debug.Log($"[SkillData] 레거시 클립을 animationClips로 이관 완료: {SkillName ?? name}", this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애셋 이름 기반으로 매칭되는 애니메이션 클립을 자동 수집합니다.
|
||||
/// SkillData 이름이 'Data_Skill_'으로 시작하면 'Anim_{key}_{순서}' 클립을 찾아 animationClips에 채웁니다.
|
||||
/// </summary>
|
||||
public void RefreshAnimationClips()
|
||||
{
|
||||
if (!name.StartsWith(SkillAssetPrefix))
|
||||
return;
|
||||
|
||||
string key = name.Substring(SkillAssetPrefix.Length);
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return;
|
||||
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets("t:AnimationClip", new[] { AnimationsSearchPath });
|
||||
var matchedClips = new List<(AnimationClip clip, int order)>();
|
||||
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
AnimationClip clip = UnityEditor.AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
|
||||
if (clip == null) continue;
|
||||
|
||||
string clipName = clip.name;
|
||||
if (!clipName.StartsWith(ClipAssetPrefix))
|
||||
continue;
|
||||
|
||||
string remaining = clipName.Substring(ClipAssetPrefix.Length);
|
||||
|
||||
if (remaining == key)
|
||||
{
|
||||
// 정확 매칭 (순서 번호 없음) → 최우선
|
||||
matchedClips.Add((clip, -1));
|
||||
}
|
||||
else if (remaining.StartsWith(key + "_"))
|
||||
{
|
||||
string suffix = remaining.Substring(key.Length + 1);
|
||||
if (int.TryParse(suffix, out int order))
|
||||
{
|
||||
matchedClips.Add((clip, order));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedClips.Count == 0)
|
||||
return;
|
||||
|
||||
// 정렬: 순서 번호 없음(-1) → 순서 번호 오름차순
|
||||
matchedClips.Sort((a, b) => a.order.CompareTo(b.order));
|
||||
|
||||
// 변경이 있는 경우만 갱신 (무한 루프 방지)
|
||||
bool changed = matchedClips.Count != animationClips.Count;
|
||||
if (!changed)
|
||||
{
|
||||
for (int i = 0; i < matchedClips.Count; i++)
|
||||
{
|
||||
if (matchedClips[i].clip != animationClips[i])
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
animationClips.Clear();
|
||||
for (int i = 0; i < matchedClips.Count; i++)
|
||||
animationClips.Add(matchedClips[i].clip);
|
||||
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
}
|
||||
#endif
|
||||
[Header("기본 정보")]
|
||||
[SerializeField] private string skillName;
|
||||
[TextArea(2, 4)]
|
||||
@@ -73,13 +187,15 @@ namespace Colosseum.Skills
|
||||
[SerializeField] private SkillBaseType baseTypes = SkillBaseType.None;
|
||||
|
||||
[Header("애니메이션")]
|
||||
[Tooltip("기본 Animator Controller의 'Skill' 상태에 덮어씌워질 클립")]
|
||||
[SerializeField] private AnimationClip skillClip;
|
||||
[Tooltip("종료 애니메이션 (선택)")]
|
||||
[SerializeField] private AnimationClip endClip;
|
||||
[Tooltip("순차 재생할 클립 목록. 애셋 이름이 Data_Skill_ 접두사면 Anim_{이름}_{순서} 클립을 자동 수집합니다.")]
|
||||
[SerializeField] private List<AnimationClip> animationClips = new();
|
||||
[Tooltip("애니메이션 재생 속도 (1 = 기본, 2 = 2배속)")]
|
||||
[Min(0.1f)] [SerializeField] private float animationSpeed = 1f;
|
||||
|
||||
// 레거시 마이그레이션 (기존 skillClip/endClip 데이터 보존)
|
||||
[SerializeField, HideInInspector] private AnimationClip legacySkillClip;
|
||||
[SerializeField, HideInInspector] private AnimationClip legacyEndClip;
|
||||
|
||||
[Header("루트 모션")]
|
||||
[Tooltip("애니메이션의 이동/회전 데이터를 캐릭터에 적용")]
|
||||
[SerializeField] private bool useRootMotion = false;
|
||||
@@ -123,8 +239,15 @@ namespace Colosseum.Skills
|
||||
public SkillRoleType SkillRole => skillRole;
|
||||
public SkillActivationType ActivationType => activationType;
|
||||
public SkillBaseType BaseTypes => baseTypes;
|
||||
public AnimationClip SkillClip => skillClip;
|
||||
public AnimationClip EndClip => endClip;
|
||||
/// <summary>
|
||||
/// 순차 재생할 클립 목록입니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AnimationClip> AnimationClips => animationClips;
|
||||
|
||||
/// <summary>
|
||||
/// 첫 번째 클립 (레거시 호환성). 기존 코드에서 SkillClip을 참조하는 곳에서 사용됩니다.
|
||||
/// </summary>
|
||||
public AnimationClip SkillClip => animationClips.Count > 0 ? animationClips[0] : null;
|
||||
public float AnimationSpeed => animationSpeed;
|
||||
public float Cooldown => cooldown;
|
||||
public float ManaCost => manaCost;
|
||||
|
||||
Reference in New Issue
Block a user