fix: 드로그 기본기3 시작 루트모션과 추적 보정 정리
- 기본기3 시작 프레임의 수평 루트모션 스냅을 차단하고 정면 투영/접촉 정지 거리 보정을 정리 - 스킬 완료 판정과 시작 타깃 정렬 로직을 보강하고 드로그/플레이어 정면 및 시작점 디버그 gizmo를 추가 - 드로그 기본기3 관련 애니메이션·패턴·스킬 자산을 재정리하고 Blender 보조 스크립트를 추가
This commit is contained in:
8
Assets/_Game/Scripts/Debug.meta
Normal file
8
Assets/_Game/Scripts/Debug.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70661168ff52fffe7b990c89bbe7b0dc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
44
Assets/_Game/Scripts/Debug/FacingDirectionGizmoUtility.cs
Normal file
44
Assets/_Game/Scripts/Debug/FacingDirectionGizmoUtility.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Debugging
|
||||
{
|
||||
/// <summary>
|
||||
/// 씬 뷰에서 캐릭터의 정면 방향을 화살표로 표시하는 Gizmo 유틸리티입니다.
|
||||
/// </summary>
|
||||
public static class FacingDirectionGizmoUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 지정한 Transform의 정면 방향 화살표를 그립니다.
|
||||
/// </summary>
|
||||
public static void DrawFacingArrow(Transform targetTransform, Color color, float length = 1.6f, float headLength = 0.35f, float headWidth = 0.22f, float heightOffset = 0.1f, float shaftThickness = 0.08f)
|
||||
{
|
||||
if (targetTransform == null)
|
||||
return;
|
||||
|
||||
Vector3 origin = targetTransform.position + Vector3.up * heightOffset;
|
||||
Vector3 forward = targetTransform.forward.normalized;
|
||||
if (forward.sqrMagnitude <= 0.0001f)
|
||||
return;
|
||||
|
||||
Vector3 tip = origin + forward * length;
|
||||
Vector3 right = targetTransform.right.normalized;
|
||||
Vector3 up = targetTransform.up.normalized;
|
||||
Vector3 headBase = tip - forward * headLength;
|
||||
Vector3 rightOffset = right * shaftThickness;
|
||||
Vector3 upOffset = up * shaftThickness;
|
||||
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawLine(origin, tip);
|
||||
Gizmos.DrawLine(origin + rightOffset, tip + rightOffset);
|
||||
Gizmos.DrawLine(origin - rightOffset, tip - rightOffset);
|
||||
Gizmos.DrawLine(origin + upOffset, tip + upOffset);
|
||||
Gizmos.DrawLine(origin - upOffset, tip - upOffset);
|
||||
Gizmos.DrawLine(tip, headBase + right * headWidth);
|
||||
Gizmos.DrawLine(tip, headBase - right * headWidth);
|
||||
Gizmos.DrawLine(tip, headBase + up * headWidth * 0.7f);
|
||||
Gizmos.DrawLine(tip, headBase - up * headWidth * 0.7f);
|
||||
Gizmos.DrawSphere(origin, shaftThickness * 0.55f);
|
||||
Gizmos.DrawSphere(tip, shaftThickness * 0.65f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3481720c56d01e6dfa9327a4c386a1cd
|
||||
@@ -2,6 +2,7 @@ using Unity.Behavior;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Debugging;
|
||||
using Colosseum.Stats;
|
||||
|
||||
namespace Colosseum.Enemy
|
||||
@@ -11,6 +12,8 @@ namespace Colosseum.Enemy
|
||||
/// </summary>
|
||||
public class BossEnemy : EnemyBase
|
||||
{
|
||||
private static readonly Color FacingGizmoColor = new Color(1f, 0.25f, 0.2f, 1f);
|
||||
|
||||
[Header("Boss Settings")]
|
||||
[Tooltip("초기 Behavior Graph")]
|
||||
[SerializeField] private BehaviorGraph initialBehaviorGraph;
|
||||
@@ -76,5 +79,10 @@ namespace Colosseum.Enemy
|
||||
|
||||
base.HandleDeath();
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
FacingDirectionGizmoUtility.DrawFacingArrow(transform, FacingGizmoColor, length: 2.2f, headLength: 0.45f, headWidth: 0.28f, heightOffset: 0.15f, shaftThickness: 0.14f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ namespace Colosseum.Enemy
|
||||
[Header("Player Separation")]
|
||||
[Tooltip("적과 플레이어 사이에 추가로 유지할 수평 간격")]
|
||||
[Min(0f)] [SerializeField] private float playerSeparationPadding = 0.1f;
|
||||
[Tooltip("실제 겹치기 직전에도 근접 기본기 루트모션을 미리 차단할 추가 수평 여유 거리")]
|
||||
[Min(0f)] [SerializeField] private float playerSurfaceLockPadding = 0.75f;
|
||||
[Tooltip("플레이어와 닿아 있을 때 적의 수평 이동을 멈출지 여부")]
|
||||
[SerializeField] private bool freezeHorizontalMotionOnPlayerContact = true;
|
||||
|
||||
@@ -75,6 +77,10 @@ namespace Colosseum.Enemy
|
||||
private bool hasJumpTarget = false;
|
||||
private Vector3 jumpStartXZ;
|
||||
private Vector3 jumpTargetXZ;
|
||||
private bool isHoldingSkillTransitionPosition;
|
||||
private bool hasLastStableSkillPosition;
|
||||
private Vector3 heldSkillTransitionPosition;
|
||||
private Vector3 lastStableSkillPosition;
|
||||
// 이벤트
|
||||
public event Action<float, float> OnHealthChanged; // currentHealth, maxHealth
|
||||
public event Action<float, float> OnShieldChanged; // oldShield, newShield
|
||||
@@ -145,7 +151,15 @@ namespace Colosseum.Enemy
|
||||
{
|
||||
if (!IsServer || IsDead) return;
|
||||
if (freezeHorizontalMotionOnPlayerContact)
|
||||
{
|
||||
ApplySkillTransitionContactPositionHold();
|
||||
if (!isHoldingSkillTransitionPosition)
|
||||
{
|
||||
lastStableSkillPosition = transform.position;
|
||||
hasLastStableSkillPosition = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 separationOffset = ComputePlayerSeparationOffset();
|
||||
if (separationOffset.sqrMagnitude <= 0.000001f)
|
||||
@@ -189,6 +203,31 @@ namespace Colosseum.Enemy
|
||||
&& !skillCtrl.IgnoreRootMotionY;
|
||||
|
||||
Vector3 deltaPosition = animator.deltaPosition;
|
||||
bool shouldApplyRootPosition = skillCtrl == null || skillCtrl.ShouldApplyRootMotionPosition;
|
||||
if (!shouldApplyRootPosition)
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
}
|
||||
|
||||
if (skillCtrl != null && skillCtrl.ConsumeInitialSkillStartHorizontalRootMotionSuppression())
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
}
|
||||
|
||||
bool shouldProjectRootMotionToFacing = skillCtrl != null && skillCtrl.ShouldProjectHorizontalRootMotionToFacing;
|
||||
if (shouldProjectRootMotionToFacing)
|
||||
deltaPosition = ProjectHorizontalDeltaOntoFacing(deltaPosition);
|
||||
|
||||
if (skillCtrl != null)
|
||||
deltaPosition = ClampRootMotionAgainstTargetStopDistance(skillCtrl, deltaPosition);
|
||||
|
||||
bool isTouchingPlayerContact = freezeHorizontalMotionOnPlayerContact && IsTouchingPlayer();
|
||||
bool shouldFreezeSkillHorizontalMotion =
|
||||
isTouchingPlayerContact &&
|
||||
skillCtrl != null &&
|
||||
skillCtrl.IsPlayingAnimation;
|
||||
|
||||
if (needsYMotion)
|
||||
{
|
||||
@@ -209,16 +248,41 @@ namespace Colosseum.Enemy
|
||||
newXZ.x - transform.position.x,
|
||||
deltaPosition.y,
|
||||
newXZ.z - transform.position.z);
|
||||
if (!shouldApplyRootPosition)
|
||||
{
|
||||
desiredDelta.x = 0f;
|
||||
desiredDelta.z = 0f;
|
||||
}
|
||||
if (shouldProjectRootMotionToFacing)
|
||||
desiredDelta = ProjectHorizontalDeltaOntoFacing(desiredDelta);
|
||||
if (shouldFreezeSkillHorizontalMotion)
|
||||
{
|
||||
desiredDelta.x = 0f;
|
||||
desiredDelta.z = 0f;
|
||||
}
|
||||
if (freezeHorizontalMotionOnPlayerContact)
|
||||
{
|
||||
if (isTouchingPlayerContact && !shouldFreezeSkillHorizontalMotion)
|
||||
desiredDelta = ProjectHorizontalDeltaOntoFacing(desiredDelta);
|
||||
desiredDelta = LimitHorizontalDeltaAgainstPlayerContact(desiredDelta);
|
||||
}
|
||||
|
||||
transform.position += desiredDelta;
|
||||
}
|
||||
else
|
||||
{
|
||||
// jumpToTarget 없으면 기존처럼 애니메이션 루트모션 그대로 적용
|
||||
if (shouldFreezeSkillHorizontalMotion)
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
}
|
||||
if (freezeHorizontalMotionOnPlayerContact)
|
||||
{
|
||||
if (isTouchingPlayerContact && !shouldFreezeSkillHorizontalMotion)
|
||||
deltaPosition = ProjectHorizontalDeltaOntoFacing(deltaPosition);
|
||||
deltaPosition = LimitHorizontalDeltaAgainstPlayerContact(deltaPosition);
|
||||
}
|
||||
|
||||
transform.position += deltaPosition;
|
||||
}
|
||||
@@ -249,13 +313,21 @@ namespace Colosseum.Enemy
|
||||
if (freezeHorizontalMotionOnPlayerContact)
|
||||
{
|
||||
ApplyPlayerContactMovementLock();
|
||||
if (shouldFreezeSkillHorizontalMotion)
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
}
|
||||
else if (isTouchingPlayerContact)
|
||||
deltaPosition = ProjectHorizontalDeltaOntoFacing(deltaPosition);
|
||||
deltaPosition = LimitHorizontalDeltaAgainstPlayerContact(deltaPosition);
|
||||
}
|
||||
|
||||
navMeshAgent.Move(deltaPosition);
|
||||
}
|
||||
|
||||
if (animator.deltaRotation != Quaternion.identity)
|
||||
bool shouldApplyRootRotation = skillCtrl == null || skillCtrl.ShouldApplyRootMotionRotation;
|
||||
if (shouldApplyRootRotation && animator.deltaRotation != Quaternion.identity)
|
||||
transform.rotation *= animator.deltaRotation;
|
||||
}
|
||||
|
||||
@@ -389,6 +461,42 @@ namespace Colosseum.Enemy
|
||||
return separationOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 시작 직후 아직 Animator가 Skill 상태로 전이되지 않은 접촉 프레임에서는
|
||||
/// 수평 좌표를 직전 안정 위치에 고정해 전환 구간 드리프트를 막습니다.
|
||||
/// </summary>
|
||||
private void ApplySkillTransitionContactPositionHold()
|
||||
{
|
||||
var skillCtrl = GetComponent<SkillController>();
|
||||
bool shouldHoldPosition =
|
||||
skillCtrl != null &&
|
||||
skillCtrl.IsPlayingAnimation &&
|
||||
!skillCtrl.ShouldApplyRootMotionPosition &&
|
||||
IsTouchingPlayer();
|
||||
|
||||
if (!shouldHoldPosition)
|
||||
{
|
||||
isHoldingSkillTransitionPosition = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isHoldingSkillTransitionPosition)
|
||||
{
|
||||
heldSkillTransitionPosition = hasLastStableSkillPosition
|
||||
? lastStableSkillPosition
|
||||
: transform.position;
|
||||
isHoldingSkillTransitionPosition = true;
|
||||
}
|
||||
|
||||
Vector3 lockedPosition = transform.position;
|
||||
lockedPosition.x = heldSkillTransitionPosition.x;
|
||||
lockedPosition.z = heldSkillTransitionPosition.z;
|
||||
transform.position = lockedPosition;
|
||||
|
||||
if (navMeshAgent != null && navMeshAgent.enabled)
|
||||
navMeshAgent.Warp(transform.position);
|
||||
}
|
||||
|
||||
private bool IsTouchingPlayer()
|
||||
{
|
||||
if (bodyCollider == null)
|
||||
@@ -421,6 +529,14 @@ namespace Colosseum.Enemy
|
||||
if (horizontalDelta.sqrMagnitude <= 0.000001f)
|
||||
return deltaPosition;
|
||||
|
||||
if (TryGetTouchingPlayerDirection(out Vector3 touchingPlayerDirection) &&
|
||||
IsBlockedByContactForwardHemisphere(horizontalDelta, touchingPlayerDirection))
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
return deltaPosition;
|
||||
}
|
||||
|
||||
horizontalDelta = ClampHorizontalDeltaToPlayerSurface(horizontalDelta);
|
||||
if (horizontalDelta.sqrMagnitude <= 0.000001f)
|
||||
{
|
||||
@@ -447,6 +563,78 @@ namespace Colosseum.Enemy
|
||||
return deltaPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 접촉 중인 상태에서는 루트모션의 수평 이동을 적의 정면축으로만 투영합니다.
|
||||
/// 플레이어 표면에 걸리면서 측면으로 미끄러지는 현상을 줄이기 위한 보정입니다.
|
||||
/// </summary>
|
||||
private Vector3 ProjectHorizontalDeltaOntoFacing(Vector3 deltaPosition)
|
||||
{
|
||||
Vector3 horizontalDelta = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
|
||||
if (horizontalDelta.sqrMagnitude <= 0.000001f)
|
||||
return deltaPosition;
|
||||
|
||||
Vector3 forward = transform.forward;
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude <= 0.000001f)
|
||||
return deltaPosition;
|
||||
|
||||
forward.Normalize();
|
||||
float forwardDistance = Vector3.Dot(horizontalDelta, forward);
|
||||
Vector3 projected = forward * forwardDistance;
|
||||
|
||||
deltaPosition.x = projected.x;
|
||||
deltaPosition.z = projected.z;
|
||||
return deltaPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 실제로 겹치고 있는 플레이어 중심 방향을 수평 기준으로 구합니다.
|
||||
/// 접촉 중 전방 진입을 즉시 차단하기 위한 단순화된 방향 계산입니다.
|
||||
/// </summary>
|
||||
private bool TryGetTouchingPlayerDirection(out Vector3 touchingPlayerDirection)
|
||||
{
|
||||
touchingPlayerDirection = Vector3.zero;
|
||||
if (bodyCollider == null)
|
||||
return false;
|
||||
|
||||
float scanRadius = GetPlayerDetectionRadius();
|
||||
Vector3 scanCenter = bodyCollider.bounds.center;
|
||||
int count = Physics.OverlapSphereNonAlloc(scanCenter, scanRadius, overlapBuffer);
|
||||
int overlapCount = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
|
||||
continue;
|
||||
|
||||
if (!Physics.ComputePenetration(
|
||||
bodyCollider, bodyCollider.transform.position, bodyCollider.transform.rotation,
|
||||
playerController, playerController.transform.position, playerController.transform.rotation,
|
||||
out _, out float separationDistance) ||
|
||||
separationDistance <= 0.0001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 towardPlayer = playerController.bounds.center - scanCenter;
|
||||
towardPlayer.y = 0f;
|
||||
if (towardPlayer.sqrMagnitude <= 0.0001f)
|
||||
continue;
|
||||
|
||||
touchingPlayerDirection += towardPlayer.normalized;
|
||||
overlapCount++;
|
||||
}
|
||||
|
||||
if (overlapCount <= 0 || touchingPlayerDirection.sqrMagnitude <= 0.0001f)
|
||||
{
|
||||
touchingPlayerDirection = Vector3.zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
touchingPlayerDirection.Normalize();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector3 ClampHorizontalDeltaToPlayerSurface(Vector3 horizontalDelta)
|
||||
{
|
||||
if (bodyCollider == null || horizontalDelta.sqrMagnitude <= 0.000001f)
|
||||
@@ -606,13 +794,64 @@ namespace Colosseum.Enemy
|
||||
return horizontalDelta * min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 루트모션 근접 스킬이 대상 표면 정지 거리 안으로 파고들지 않도록 수평 이동을 제한합니다.
|
||||
/// 현재는 MoveTowardTarget에만 쓰이던 stopDistance를 루트모션 근접기에도 동일하게 적용합니다.
|
||||
/// </summary>
|
||||
private Vector3 ClampRootMotionAgainstTargetStopDistance(SkillController skillCtrl, Vector3 deltaPosition)
|
||||
{
|
||||
if (skillCtrl == null || !skillCtrl.UsesRootMotion)
|
||||
return deltaPosition;
|
||||
|
||||
SkillData currentSkill = skillCtrl.CurrentSkill;
|
||||
GameObject currentTarget = skillCtrl.CurrentTargetOverride;
|
||||
if (currentSkill == null || currentTarget == null || !currentTarget.activeInHierarchy)
|
||||
return deltaPosition;
|
||||
|
||||
float stopDistance = Mathf.Max(0f, currentSkill.CastTargetStopDistance);
|
||||
if (stopDistance <= 0f)
|
||||
return deltaPosition;
|
||||
|
||||
Vector3 horizontalDelta = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
|
||||
if (horizontalDelta.sqrMagnitude <= 0.000001f)
|
||||
return deltaPosition;
|
||||
|
||||
Vector3 towardTarget = TargetSurfaceUtility.GetHorizontalDirectionToSurface(transform.position, currentTarget);
|
||||
if (towardTarget.sqrMagnitude <= 0.0001f)
|
||||
return deltaPosition;
|
||||
|
||||
towardTarget.Normalize();
|
||||
|
||||
float currentSurfaceDistance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(transform.position, currentTarget);
|
||||
if (currentSurfaceDistance <= stopDistance)
|
||||
{
|
||||
deltaPosition.x = 0f;
|
||||
deltaPosition.z = 0f;
|
||||
return deltaPosition;
|
||||
}
|
||||
|
||||
float towardDistance = Vector3.Dot(horizontalDelta, towardTarget);
|
||||
if (towardDistance <= 0f)
|
||||
return deltaPosition;
|
||||
|
||||
float allowedTowardDistance = Mathf.Max(0f, currentSurfaceDistance - stopDistance);
|
||||
if (towardDistance <= allowedTowardDistance)
|
||||
return deltaPosition;
|
||||
|
||||
Vector3 lateralDelta = horizontalDelta - towardTarget * towardDistance;
|
||||
Vector3 clampedHorizontalDelta = lateralDelta + towardTarget * allowedTowardDistance;
|
||||
deltaPosition.x = clampedHorizontalDelta.x;
|
||||
deltaPosition.z = clampedHorizontalDelta.z;
|
||||
return deltaPosition;
|
||||
}
|
||||
|
||||
private bool WouldCrossPlayerSurface(Vector3 horizontalOffset)
|
||||
{
|
||||
if (bodyCollider == null)
|
||||
return false;
|
||||
|
||||
float enemyRadius = GetBodyHorizontalRadius();
|
||||
float minimumSurfaceDistance = enemyRadius + playerSeparationPadding;
|
||||
float minimumSurfaceDistance = enemyRadius + playerSeparationPadding + playerSurfaceLockPadding;
|
||||
float scanRadius = GetPlayerDetectionRadius() + horizontalOffset.magnitude;
|
||||
Vector3 scanCenter = bodyCollider.bounds.center + horizontalOffset;
|
||||
int count = Physics.OverlapSphereNonAlloc(scanCenter, scanRadius, overlapBuffer);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Unity.Netcode;
|
||||
|
||||
using Colosseum.Abnormalities;
|
||||
using Colosseum.Combat;
|
||||
using Colosseum.Debugging;
|
||||
using Colosseum.Passives;
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Stats;
|
||||
@@ -19,6 +20,8 @@ namespace Colosseum.Player
|
||||
/// </summary>
|
||||
public class PlayerNetworkController : NetworkBehaviour, IDamageable
|
||||
{
|
||||
private static readonly Color FacingGizmoColor = new Color(0.2f, 0.85f, 1f, 1f);
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")]
|
||||
[SerializeField] private CharacterStats characterStats;
|
||||
@@ -219,6 +222,11 @@ namespace Colosseum.Player
|
||||
SpendMana(amount);
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
FacingDirectionGizmoUtility.DrawFacingArrow(transform, FacingGizmoColor, length: 1.7f, headLength: 0.35f, headWidth: 0.22f, heightOffset: 0.12f, shaftThickness: 0.11f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복 (서버에서만 실행)
|
||||
/// </summary>
|
||||
|
||||
@@ -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