fix: 플레이어 접촉 이동과 타깃 표면 추적 보정

- TargetSurfaceUtility를 추가해 플레이어와 적의 실제 충돌 표면 기준으로 거리, 방향, 목적지를 계산

- 플레이어 이동과 적 루트모션, 추적 로직에서 접촉 시 수평 이동을 제한해 겹침과 밀어내기 문제를 완화

- 드로그 AI 거리 판정 노드들이 표면 거리 기준을 사용하도록 맞춰 사거리 분기 오차를 줄임
This commit is contained in:
2026-04-09 23:22:28 +09:00
parent 0fa23d4389
commit abfc43ae76
20 changed files with 514 additions and 92 deletions

View File

@@ -249,29 +249,10 @@ namespace Colosseum.Player
private void UpdateBlockedDirection()
{
blockedDirection = Vector3.zero;
if (characterController == null)
return;
Vector3 center = transform.position + characterController.center;
float radius = characterController.radius + 0.15f;
float halfHeight = Mathf.Max(0f, characterController.height * 0.5f - characterController.radius);
int count = Physics.OverlapCapsuleNonAlloc(
center + Vector3.up * halfHeight,
center - Vector3.up * halfHeight,
radius, overlapBuffer);
for (int i = 0; i < count; i++)
{
if (overlapBuffer[i].gameObject == gameObject) continue;
if (!overlapBuffer[i].TryGetComponent<UnityEngine.AI.NavMeshAgent>(out _)) continue;
Vector3 toEnemy = overlapBuffer[i].transform.position - transform.position;
toEnemy.y = 0f;
if (toEnemy.sqrMagnitude > 0.0001f)
{
blockedDirection = toEnemy.normalized;
break;
}
}
TryGetEnemyContactBlockDirection(out blockedDirection);
}
private void ApplyGravity()
@@ -303,9 +284,10 @@ namespace Colosseum.Player
if (actionState != null && !actionState.CanMove)
moveDirection = Vector3.zero;
if (blockedDirection != Vector3.zero && Vector3.Dot(moveDirection, blockedDirection) > 0f)
if (IsBlockedByEnemyContact(moveDirection))
moveDirection = Vector3.zero;
forcedDelta = LimitHorizontalDeltaAgainstEnemyContact(forcedDelta);
float actualMoveSpeed = moveSpeed * GetMoveSpeedMultiplier();
characterController.Move((moveDirection * actualMoveSpeed + velocity) * Time.deltaTime + forcedDelta);
@@ -400,16 +382,8 @@ namespace Colosseum.Player
Vector3 deltaPosition = animator.deltaPosition;
Vector3 forcedDelta = ConsumeForcedMovementDelta(Time.deltaTime);
if (blockedDirection != Vector3.zero)
{
Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
if (Vector3.Dot(deltaXZ, blockedDirection) > 0f)
{
deltaPosition.x = 0f;
deltaPosition.z = 0f;
}
}
deltaPosition = LimitHorizontalDeltaAgainstEnemyContact(deltaPosition);
forcedDelta = LimitHorizontalDeltaAgainstEnemyContact(forcedDelta);
if (skillController.IgnoreRootMotionY)
{
@@ -446,5 +420,100 @@ namespace Colosseum.Player
return delta;
}
private Vector3 LimitHorizontalDeltaAgainstEnemyContact(Vector3 delta)
{
Vector3 horizontalDelta = new Vector3(delta.x, 0f, delta.z);
if (!IsBlockedByEnemyContact(horizontalDelta))
return delta;
delta.x = 0f;
delta.z = 0f;
return delta;
}
private bool IsBlockedByEnemyContact(Vector3 horizontalDirection)
{
Vector3 horizontal = new Vector3(horizontalDirection.x, 0f, horizontalDirection.z);
if (horizontal.sqrMagnitude <= 0.000001f || blockedDirection.sqrMagnitude <= 0.0001f)
return false;
return Vector3.Dot(horizontal, blockedDirection) >= 0f;
}
private bool TryGetEnemyContactBlockDirection(out Vector3 blockDirection)
{
blockDirection = Vector3.zero;
if (characterController == null)
return false;
Vector3 center = transform.position + characterController.center;
float radius = characterController.radius + 0.15f;
float halfHeight = Mathf.Max(0f, characterController.height * 0.5f - characterController.radius);
int count = Physics.OverlapCapsuleNonAlloc(
center + Vector3.up * halfHeight,
center - Vector3.up * halfHeight,
radius,
overlapBuffer);
int overlapCount = 0;
for (int i = 0; i < count; i++)
{
if (!TryGetEnemyCollider(overlapBuffer[i], out Collider enemyCollider))
continue;
if (!Physics.ComputePenetration(
characterController, transform.position, transform.rotation,
enemyCollider, enemyCollider.transform.position, enemyCollider.transform.rotation,
out Vector3 separationDirection, out float separationDistance) ||
separationDistance <= 0.0001f)
{
continue;
}
Vector3 towardEnemy = enemyCollider.bounds.center - center;
towardEnemy.y = 0f;
if (towardEnemy.sqrMagnitude <= 0.0001f)
{
towardEnemy = -separationDirection;
towardEnemy.y = 0f;
if (towardEnemy.sqrMagnitude <= 0.0001f)
continue;
}
blockDirection += towardEnemy.normalized;
overlapCount++;
}
if (overlapCount <= 0 || blockDirection.sqrMagnitude <= 0.0001f)
{
blockDirection = Vector3.zero;
return false;
}
blockDirection.Normalize();
return true;
}
private bool TryGetEnemyCollider(Collider overlapCollider, out Collider enemyCollider)
{
enemyCollider = null;
if (overlapCollider == null)
return false;
if (overlapCollider.gameObject == gameObject || overlapCollider.transform.IsChildOf(transform))
return false;
Colosseum.Enemy.EnemyBase enemy = overlapCollider.GetComponent<Colosseum.Enemy.EnemyBase>();
if (enemy == null)
enemy = overlapCollider.GetComponentInParent<Colosseum.Enemy.EnemyBase>();
if (enemy == null)
return false;
enemyCollider = overlapCollider;
return enemyCollider != null;
}
}
}