chore: Assets 디렉토리 구조 정리 및 네이밍 컨벤션 적용
- Assets/_Game/ 하위로 게임 에셋 통합 - External/ 패키지 벤더별 분류 (Synty, Animations, UI) - 에셋 네이밍 컨벤션 확립 및 적용 (Data_Skill_, Data_SkillEffect_, Prefab_, Anim_, Model_, BT_ 등) - pre-commit hook으로 네이밍 컨벤션 자동 검사 추가 - RESTRUCTURE_CHECKLIST.md 작성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
304
Assets/_Game/Scripts/Skills/SkillController.cs
Normal file
304
Assets/_Game/Scripts/Skills/SkillController.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Colosseum.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 실행을 관리하는 컴포넌트.
|
||||
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
|
||||
/// </summary>
|
||||
public class SkillController : MonoBehaviour
|
||||
{
|
||||
private const string SKILL_STATE_NAME = "Skill";
|
||||
private const string END_STATE_NAME = "SkillEnd";
|
||||
|
||||
[Header("애니메이션")]
|
||||
[SerializeField] private Animator animator;
|
||||
[Tooltip("기본 Animator Controller (스킬 종료 후 복원용)")]
|
||||
[SerializeField] private RuntimeAnimatorController baseController;
|
||||
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용)")]
|
||||
[SerializeField] private AnimationClip baseSkillClip;
|
||||
|
||||
[Header("설정")]
|
||||
[SerializeField] private bool debugMode = false;
|
||||
[Tooltip("공격 범위 시각화 (Scene 뷰에서 확인)")]
|
||||
[SerializeField] private bool showAreaDebug = true;
|
||||
[Tooltip("범위 표시 지속 시간")]
|
||||
[Min(0.1f)] [SerializeField] private float debugDrawDuration = 1f;
|
||||
|
||||
// 현재 실행 중인 스킬
|
||||
private SkillData currentSkill;
|
||||
private bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부
|
||||
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
|
||||
|
||||
// 쿨타임 추적
|
||||
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
||||
|
||||
public bool IsExecutingSkill => currentSkill != null && !skillEndRequested;
|
||||
public bool IsPlayingAnimation => currentSkill != null;
|
||||
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
|
||||
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
|
||||
public SkillData CurrentSkill => currentSkill;
|
||||
public Animator Animator => animator;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (animator == null)
|
||||
{
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
}
|
||||
|
||||
// 기본 컨트롤러 저장
|
||||
if (baseController == null && animator != null)
|
||||
{
|
||||
baseController = animator.runtimeAnimatorController;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (currentSkill == null || animator == null) return;
|
||||
|
||||
var stateInfo = animator.GetCurrentAnimatorStateInfo(0);
|
||||
|
||||
// EndAnimation 종료 감지
|
||||
if (waitingForEndAnimation)
|
||||
{
|
||||
if (stateInfo.normalizedTime >= 1f)
|
||||
{
|
||||
if (debugMode) Debug.Log($"[Skill] EndAnimation complete: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
currentSkill = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 애니메이션 종료 시 처리 (OnSkillEnd 여부와 관계없이 애니메이션 끝까지 재생)
|
||||
if (stateInfo.normalizedTime >= 1f)
|
||||
{
|
||||
if (currentSkill.EndClip != null)
|
||||
{
|
||||
// EndAnimation 재생 후 종료 대기
|
||||
if (debugMode) Debug.Log($"[Skill] SkillAnimation done, playing EndAnimation: {currentSkill.SkillName}");
|
||||
PlayEndClip(currentSkill.EndClip);
|
||||
waitingForEndAnimation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// EndAnimation 없으면 바로 종료
|
||||
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
currentSkill = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 시전
|
||||
/// </summary>
|
||||
public bool ExecuteSkill(SkillData skill)
|
||||
{
|
||||
if (skill == null)
|
||||
{
|
||||
Debug.LogWarning("Skill is null!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 사망 상태면 스킬 사용 불가
|
||||
var damageable = GetComponent<Colosseum.Combat.IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
{
|
||||
if (debugMode) Debug.Log($"[Skill] Cannot execute skill - owner is dead");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsExecutingSkill)
|
||||
{
|
||||
if (debugMode) Debug.Log($"Already executing skill: {currentSkill.SkillName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsOnCooldown(skill))
|
||||
{
|
||||
if (debugMode) Debug.Log($"Skill {skill.SkillName} is on cooldown");
|
||||
return false;
|
||||
}
|
||||
|
||||
currentSkill = skill;
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
|
||||
|
||||
// 쿨타임 시작
|
||||
StartCooldown(skill);
|
||||
|
||||
// 스킬 애니메이션 재생
|
||||
if (skill.SkillClip != null && animator != null)
|
||||
{
|
||||
PlaySkillClip(skill.SkillClip);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 클립으로 Override Controller 생성 후 재생
|
||||
/// </summary>
|
||||
private void PlaySkillClip(AnimationClip clip)
|
||||
{
|
||||
if (baseSkillClip == null)
|
||||
{
|
||||
Debug.LogError("[SkillController] Base Skill Clip is not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}");
|
||||
}
|
||||
|
||||
var overrideController = new AnimatorOverrideController(baseController);
|
||||
overrideController[baseSkillClip] = clip;
|
||||
animator.runtimeAnimatorController = overrideController;
|
||||
|
||||
// 애니메이터 완전 리셋 후 재생
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 종료 클립 재생
|
||||
/// </summary>
|
||||
private void PlayEndClip(AnimationClip clip)
|
||||
{
|
||||
if (baseSkillClip == null)
|
||||
{
|
||||
Debug.LogError("[SkillController] Base Skill Clip is not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
var overrideController = new AnimatorOverrideController(baseController);
|
||||
overrideController[baseSkillClip] = clip;
|
||||
animator.runtimeAnimatorController = overrideController;
|
||||
|
||||
// 애니메이터 완전 리셋 후 재생
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 컨트롤러로 복원
|
||||
/// </summary>
|
||||
private void RestoreBaseController()
|
||||
{
|
||||
if (animator != null && baseController != null)
|
||||
{
|
||||
animator.runtimeAnimatorController = baseController;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 이벤트에서 호출. Effect 리스트의 index번째 효과를 발동합니다.
|
||||
/// Animation Event: Function = OnEffect, Int Parameter = effect index (0-based)
|
||||
/// </summary>
|
||||
public void OnEffect(int index)
|
||||
{
|
||||
if (currentSkill == null)
|
||||
{
|
||||
if (debugMode) Debug.LogWarning("[Effect] No skill executing");
|
||||
return;
|
||||
}
|
||||
|
||||
// 사망 상태면 효과 발동 중단
|
||||
var damageable = GetComponent<Colosseum.Combat.IDamageable>();
|
||||
if (damageable != null && damageable.IsDead)
|
||||
{
|
||||
if (debugMode) Debug.Log($"[Effect] Cancelled - owner is dead");
|
||||
return;
|
||||
}
|
||||
|
||||
var effects = currentSkill.Effects;
|
||||
if (index < 0 || index >= effects.Count)
|
||||
{
|
||||
if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}");
|
||||
return;
|
||||
}
|
||||
|
||||
var effect = effects[index];
|
||||
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
|
||||
|
||||
// 공격 범위 시각화
|
||||
if (showAreaDebug)
|
||||
{
|
||||
effect.DrawDebugRange(gameObject, debugDrawDuration);
|
||||
}
|
||||
|
||||
effect.ExecuteOnCast(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 이벤트에서 호출. 스킬 종료를 요청합니다.
|
||||
/// 애니메이션은 끝까지 재생된 후 종료됩니다.
|
||||
/// Animation Event: Function = OnSkillEnd
|
||||
/// </summary>
|
||||
public void OnSkillEnd()
|
||||
{
|
||||
if (currentSkill == null)
|
||||
{
|
||||
if (debugMode) Debug.LogWarning("[SkillEnd] No skill executing");
|
||||
return;
|
||||
}
|
||||
|
||||
skillEndRequested = true;
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] End requested: {currentSkill.SkillName} (will complete after animation)");
|
||||
}
|
||||
|
||||
public void CancelSkill()
|
||||
{
|
||||
if (currentSkill != null)
|
||||
{
|
||||
if (debugMode) Debug.Log($"Skill cancelled: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
currentSkill = null;
|
||||
skillEndRequested = false;
|
||||
waitingForEndAnimation = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOnCooldown(SkillData skill)
|
||||
{
|
||||
if (!cooldownTracker.ContainsKey(skill))
|
||||
return false;
|
||||
|
||||
return Time.time < cooldownTracker[skill];
|
||||
}
|
||||
|
||||
public float GetRemainingCooldown(SkillData skill)
|
||||
{
|
||||
if (!cooldownTracker.ContainsKey(skill))
|
||||
return 0f;
|
||||
|
||||
float remaining = cooldownTracker[skill] - Time.time;
|
||||
return Mathf.Max(0f, remaining);
|
||||
}
|
||||
|
||||
private void StartCooldown(SkillData skill)
|
||||
{
|
||||
cooldownTracker[skill] = Time.time + skill.Cooldown;
|
||||
}
|
||||
|
||||
public void ResetCooldown(SkillData skill)
|
||||
{
|
||||
cooldownTracker.Remove(skill);
|
||||
}
|
||||
|
||||
public void ResetAllCooldowns()
|
||||
{
|
||||
cooldownTracker.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user