fix: 드로그 기본기3 시작 루트모션과 추적 보정 정리

- 기본기3 시작 프레임의 수평 루트모션 스냅을 차단하고 정면 투영/접촉 정지 거리 보정을 정리
- 스킬 완료 판정과 시작 타깃 정렬 로직을 보강하고 드로그/플레이어 정면 및 시작점 디버그 gizmo를 추가
- 드로그 기본기3 관련 애니메이션·패턴·스킬 자산을 재정리하고 Blender 보조 스크립트를 추가
This commit is contained in:
2026-04-17 09:56:40 +09:00
parent 5ba543ed8c
commit 8c08e63c81
20 changed files with 173170 additions and 162503 deletions

View File

@@ -55,6 +55,8 @@ namespace Colosseum.Skills
private const string IdleStateName = "Idle";
private const string BossIdlePhase1StateName = "Idle_Phase1";
private const string BossIdlePhase3StateName = "Idle_Phase3";
private const int MaxDebugSkillStartPoints = 16;
private static readonly Color SkillStartPointGizmoColor = new Color(1f, 0.92f, 0.2f, 0.95f);
[Header("애니메이션")]
[SerializeField] private Animator animator;
@@ -122,6 +124,8 @@ namespace Colosseum.Skills
private bool shouldBlendIntoCurrentSkill = true;
private bool shouldRestoreToIdleAfterCurrentSkill = true;
private bool isBossPatternBoundarySkill = false;
private bool hasAppliedStartTargetFacing = false;
private bool shouldSuppressInitialSkillStartHorizontalRootMotion = false;
// 쿨타임 추적
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
@@ -131,12 +135,64 @@ namespace Colosseum.Skills
private float currentClipExpectedDuration = 0f;
private string currentClipDebugName = string.Empty;
private int currentClipStartFrame = -1;
private readonly List<Vector3> debugSkillStartPoints = new();
public bool IsExecutingSkill => currentSkill != null;
public bool IsPlayingAnimation => currentSkill != null;
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
/// <summary>
/// 현재 스킬의 루트 위치를 실제 이동에 반영해도 되는지 반환합니다.
/// 스킬 시작 직후 아직 Animator가 Skill 상태로 전이되지 않은 프레임에서는
/// 이전 상태의 루트 XZ가 누적되지 않도록 위치 적용을 잠시 차단합니다.
/// </summary>
public bool ShouldApplyRootMotionPosition =>
currentSkill == null
|| !currentSkill.UseRootMotion
|| animator == null
|| IsCurrentStateSkillState(animator.GetCurrentAnimatorStateInfo(0));
/// <summary>
/// 현재 스킬의 루트 회전을 실제 Transform 회전에 반영해야 하는지 반환합니다.
/// 대상 추적이 코드 회전으로 이미 해결되는 FaceTarget 스킬은
/// 잘라 쓴 클립의 종료 루트 회전이 다음 스킬로 누적되지 않도록 애니메이션 루트 회전을 차단합니다.
/// </summary>
public bool ShouldApplyRootMotionRotation =>
currentSkill == null
|| !currentSkill.UseRootMotion
|| (currentSkill.CastTargetTrackingMode != SkillCastTargetTrackingMode.FaceTarget
&& currentSkill.CastTargetTrackingMode != SkillCastTargetTrackingMode.FaceTargetOnStart);
/// <summary>
/// 현재 스킬의 수평 루트모션을 항상 정면축 기준으로 투영해야 하는지 반환합니다.
/// 근접 추적형 보스 스킬은 클립 내부의 비스듬한 XZ 이동 대신 현재 정면 기준 전진만 사용합니다.
/// </summary>
public bool ShouldProjectHorizontalRootMotionToFacing =>
currentSkill != null
&& currentSkill.UseRootMotion
&& (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget
|| currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTargetOnStart);
/// <summary>
/// 스킬 진입 첫 프레임의 비정상적인 수평 루트 스냅을 1회 차단해야 하는지 반환합니다.
/// Animator가 Skill 상태 normalizedTime 0으로 막 진입한 프레임에서만 true를 반환하고 곧바로 소비합니다.
/// </summary>
public bool ConsumeInitialSkillStartHorizontalRootMotionSuppression()
{
if (!shouldSuppressInitialSkillStartHorizontalRootMotion || currentSkill == null || !currentSkill.UseRootMotion || animator == null)
return false;
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (!IsCurrentStateSkillState(stateInfo))
return false;
if (stateInfo.normalizedTime <= 0.001f)
{
shouldSuppressInitialSkillStartHorizontalRootMotion = false;
return true;
}
shouldSuppressInitialSkillStartHorizontalRootMotion = false;
return false;
}
public SkillData CurrentSkill => currentSkill;
public SkillLoadoutEntry CurrentLoadoutEntry => currentLoadoutEntry;
public Animator Animator => animator;
@@ -293,6 +349,9 @@ namespace Colosseum.Skills
var stateInfo = animator.GetCurrentAnimatorStateInfo(0);
if (!IsCurrentStateSkillState(stateInfo))
return;
// 애니메이션 종료 시 처리
if (stateInfo.normalizedTime >= 1f)
{
@@ -419,6 +478,7 @@ namespace Colosseum.Skills
currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount();
currentIterationIndex = 0;
loopHoldRequested = skill.RequiresLoopHold;
RecordSkillStartPoint();
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
@@ -485,6 +545,34 @@ namespace Colosseum.Skills
}
}
/// <summary>
/// 씬 뷰에서 스킬 시작 위치를 추적하기 위해 현재 루트 위치를 기록합니다.
/// </summary>
private void RecordSkillStartPoint()
{
debugSkillStartPoints.Add(transform.position);
if (debugSkillStartPoints.Count <= MaxDebugSkillStartPoints)
return;
debugSkillStartPoints.RemoveAt(0);
}
private void OnDrawGizmos()
{
if (debugSkillStartPoints.Count <= 0)
return;
Gizmos.color = SkillStartPointGizmoColor;
for (int i = 0; i < debugSkillStartPoints.Count; i++)
{
Vector3 point = debugSkillStartPoints[i] + Vector3.up * 0.06f;
float radius = i == debugSkillStartPoints.Count - 1 ? 0.14f : 0.1f;
Gizmos.DrawSphere(point, radius);
}
}
/// <summary>
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
@@ -561,6 +649,8 @@ namespace Colosseum.Skills
currentIterationIndex++;
currentClipSequenceIndex = 0;
hasAppliedStartTargetFacing = false;
shouldSuppressInitialSkillStartHorizontalRootMotion = currentSkill.UseRootMotion;
isPlayingReleasePhase = false;
currentPhaseAnimationClips = currentSkill.AnimationClips;
@@ -570,6 +660,7 @@ namespace Colosseum.Skills
}
TriggerCastStartEffects();
ApplyImmediateCastTargetFacingBeforeSkillStart();
if (currentPhaseAnimationClips.Count > 0 && animator != null)
{
@@ -584,6 +675,37 @@ namespace Colosseum.Skills
TriggerImmediateSelfEffectsIfNeeded();
}
/// <summary>
/// 루트모션 근접 스킬은 첫 프레임 이동이 현재 정면 기준으로 바로 적용되므로
/// 실제 클립 재생 전에 대상 방향으로 한 번 즉시 정렬해 시작 방향이 틀어지지 않게 합니다.
/// </summary>
private void ApplyImmediateCastTargetFacingBeforeSkillStart()
{
if (currentSkill == null || currentTargetOverride == null || !currentTargetOverride.activeInHierarchy)
return;
if (IsSpawned && !IsServer)
return;
bool shouldSnapFacing =
currentSkill.UseRootMotion &&
(currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget
|| currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTargetOnStart
|| currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.MoveTowardTarget);
if (!shouldSnapFacing)
return;
Vector3 direction = TargetSurfaceUtility.GetHorizontalDirectionToSurface(transform.position, currentTargetOverride);
if (direction.sqrMagnitude < 0.0001f)
return;
transform.rotation = Quaternion.LookRotation(direction.normalized);
if (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTargetOnStart)
hasAppliedStartTargetFacing = true;
}
/// <summary>
/// 시퀀스 내 다음 클립이 있으면 재생합니다.
/// </summary>
@@ -1297,6 +1419,8 @@ namespace Colosseum.Skills
currentClipStartFrame = -1;
shouldBlendIntoCurrentSkill = true;
shouldRestoreToIdleAfterCurrentSkill = true;
hasAppliedStartTargetFacing = false;
shouldSuppressInitialSkillStartHorizontalRootMotion = false;
}
/// <summary>
@@ -1373,7 +1497,15 @@ namespace Colosseum.Skills
&& currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget
&& enemyBase.IsTouchingPlayerContact;
if (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget ||
if (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTargetOnStart)
{
if (!hasAppliedStartTargetFacing)
{
transform.rotation = Quaternion.LookRotation(direction.normalized);
hasAppliedStartTargetFacing = true;
}
}
else if (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget ||
currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.MoveTowardTarget)
{
if (!suppressRotationWhileContactingPlayer)
@@ -1598,6 +1730,15 @@ namespace Colosseum.Skills
return Animator.StringToHash($"{BaseLayerName}.{SKILL_STATE_NAME}");
}
/// <summary>
/// 현재 레이어가 실제 스킬 상태를 재생 중인지 확인합니다.
/// 스킬 시작 직후 한 프레임 동안 이전 상태 정보가 남아 조기 종료되는 것을 방지합니다.
/// </summary>
private static bool IsCurrentStateSkillState(AnimatorStateInfo stateInfo)
{
return stateInfo.fullPathHash == GetSkillStateHash();
}
/// <summary>
/// 스킬 종료 후 복귀할 상태 해시를 결정합니다.
/// </summary>

View File

@@ -58,6 +58,7 @@ namespace Colosseum.Skills
None,
FaceTarget,
MoveTowardTarget,
FaceTargetOnStart,
}
/// <summary>