fix: 드로그 기본기3 시작 루트모션과 추적 보정 정리
- 기본기3 시작 프레임의 수평 루트모션 스냅을 차단하고 정면 투영/접촉 정지 거리 보정을 정리 - 스킬 완료 판정과 시작 타깃 정렬 로직을 보강하고 드로그/플레이어 정면 및 시작점 디버그 gizmo를 추가 - 드로그 기본기3 관련 애니메이션·패턴·스킬 자산을 재정리하고 Blender 보조 스크립트를 추가
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace Colosseum.Skills
|
||||
None,
|
||||
FaceTarget,
|
||||
MoveTowardTarget,
|
||||
FaceTargetOnStart,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user