diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/BossPatternActionBase.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/BossPatternActionBase.cs
index ad5acfc4..02c8be3f 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/BossPatternActionBase.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/BossPatternActionBase.cs
@@ -176,7 +176,7 @@ public abstract partial class BossPatternActionBase : Action
GameObject candidate = player.gameObject;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance > maxDistance || distance >= nearestDistance)
continue;
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs
index be273ed9..a454483c 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/ChaseTargetAction.cs
@@ -1,7 +1,11 @@
using System;
+
+using Colosseum.Combat;
using Colosseum.Enemy;
+
using Unity.Behavior;
using UnityEngine;
+
using Action = Unity.Behavior.Action;
using Unity.Properties;
@@ -69,14 +73,14 @@ public partial class ChaseTargetAction : Action
}
// 이미 사거리 내에 있으면 성공
- float distance = Vector3.Distance(GameObject.transform.position, Target.Value.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, Target.Value);
if (distance <= StopDistance.Value)
{
agent.isStopped = true;
return Status.Success;
}
- agent.SetDestination(Target.Value.transform.position);
+ agent.SetDestination(TargetSurfaceUtility.GetClosestSurfacePoint(GameObject.transform.position, Target.Value));
return Status.Running;
}
@@ -88,4 +92,3 @@ public partial class ChaseTargetAction : Action
}
}
}
-
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RefreshPrimaryTargetAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RefreshPrimaryTargetAction.cs
index 1fd6e922..c0c4b84e 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RefreshPrimaryTargetAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RefreshPrimaryTargetAction.cs
@@ -1,6 +1,7 @@
using System;
using Colosseum;
+using Colosseum.Combat;
using Colosseum.Enemy;
using Colosseum.Player;
@@ -79,7 +80,7 @@ public partial class RefreshPrimaryTargetAction : Action
if (Team.IsSameTeam(GameObject, candidate))
continue;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance > searchRange || distance >= nearestDistance)
continue;
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RotateToTargetAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RotateToTargetAction.cs
index 2c646bd4..0876a203 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RotateToTargetAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/RotateToTargetAction.cs
@@ -1,6 +1,10 @@
using System;
+
+using Colosseum.Combat;
+
using Unity.Behavior;
using UnityEngine;
+
using Action = Unity.Behavior.Action;
using Unity.Properties;
@@ -24,8 +28,7 @@ public partial class RotateToTargetAction : Action
return Status.Failure;
}
- Vector3 direction = Target.Value.transform.position - GameObject.transform.position;
- direction.y = 0f;
+ Vector3 direction = TargetSurfaceUtility.GetHorizontalDirectionToSurface(GameObject.transform.position, Target.Value);
if (direction == Vector3.zero)
{
@@ -51,4 +54,3 @@ public partial class RotateToTargetAction : Action
return Status.Running;
}
}
-
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectAlternateTargetByDistanceAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectAlternateTargetByDistanceAction.cs
index a3eb3830..a13ad4c8 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectAlternateTargetByDistanceAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectAlternateTargetByDistanceAction.cs
@@ -75,7 +75,7 @@ public partial class SelectAlternateTargetByDistanceAction : Action
if (!IsValidHostileTarget(candidate))
continue;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance < minRange || distance > maxRange)
continue;
@@ -86,7 +86,7 @@ public partial class SelectAlternateTargetByDistanceAction : Action
{
if (primaryTarget != null && IsValidHostileTarget(primaryTarget))
{
- float primaryDistance = Vector3.Distance(GameObject.transform.position, primaryTarget.transform.position);
+ float primaryDistance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, primaryTarget);
if (primaryDistance >= minRange && primaryDistance <= maxRange)
return primaryTarget;
}
@@ -123,7 +123,7 @@ public partial class SelectAlternateTargetByDistanceAction : Action
if (!IsValidHostileTarget(candidate))
continue;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance > aggroRange || distance >= nearestDistance)
continue;
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectNearestDownedTargetAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectNearestDownedTargetAction.cs
index 98ef5891..8b687fe5 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectNearestDownedTargetAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectNearestDownedTargetAction.cs
@@ -53,7 +53,7 @@ public partial class SelectNearestDownedTargetAction : Action
if (damageable != null && damageable.IsDead)
continue;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance > searchRadius || distance >= nearestDistance)
continue;
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectTargetByDistanceAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectTargetByDistanceAction.cs
index 8f153289..b1d09c66 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectTargetByDistanceAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SelectTargetByDistanceAction.cs
@@ -162,7 +162,7 @@ public partial class SelectTargetByDistanceAction : Action
? enemyBase.Data.AggroRange
: maxRange;
- distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance < minRange || distance > maxRange || distance > sightRange)
return false;
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SetTargetInRangeAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SetTargetInRangeAction.cs
index 88dea309..006f238f 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SetTargetInRangeAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/SetTargetInRangeAction.cs
@@ -58,10 +58,9 @@ public partial class SetTargetInRangeAction : Action
continue;
}
- float distance = Vector3.Distance(
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(
GameObject.transform.position,
- potentialTarget.transform.position
- );
+ potentialTarget);
if (distance < nearestDistance)
{
@@ -79,4 +78,3 @@ public partial class SetTargetInRangeAction : Action
return Status.Success;
}
}
-
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/UsePatternAction.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/UsePatternAction.cs
index 180e895f..a2287901 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Actions/UsePatternAction.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Actions/UsePatternAction.cs
@@ -192,7 +192,7 @@ public partial class UsePatternAction : Action
if (damageable != null && damageable.IsDead)
return false;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
return distance <= maxDistance;
}
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsDownedTargetInRangeCondition.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsDownedTargetInRangeCondition.cs
index 118a9a93..b23bc509 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsDownedTargetInRangeCondition.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsDownedTargetInRangeCondition.cs
@@ -52,7 +52,7 @@ namespace Colosseum.AI.BehaviorActions.Conditions
if (damageable != null && damageable.IsDead)
continue;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance <= searchRadius)
return true;
}
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsHealthBelowCondition.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsHealthBelowCondition.cs
index 3afa6635..2ab70eb4 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsHealthBelowCondition.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsHealthBelowCondition.cs
@@ -11,6 +11,7 @@ namespace Colosseum.AI.BehaviorActions.Conditions
/// 체력이 지정된 비율 이하인지 확인합니다.
///
[Serializable, GeneratePropertyBag]
+ [Condition(name: "Is Health Below", story: "체력이 [HealthPercent]% 이하인가?", id: "7a4ce4b7-9344-4589-b744-11f5d846dcb2")]
[NodeDescription(name: "Is Health Below", story: "Check if health is below [HealthPercent] percent", category: "Combat")]
public partial class IsHealthBelowCondition : Condition
{
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInAttackRangeCondition.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInAttackRangeCondition.cs
index 718d0084..1644ff19 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInAttackRangeCondition.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInAttackRangeCondition.cs
@@ -1,4 +1,7 @@
using System;
+
+using Colosseum.Combat;
+
using UnityEngine;
using Unity.Behavior;
using Unity.Properties;
@@ -26,10 +29,9 @@ namespace Colosseum.AI.BehaviorActions.Conditions
return false;
}
- float distance = Vector3.Distance(
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(
GameObject.transform.position,
- Target.Value.transform.position
- );
+ Target.Value);
return distance <= Range.Value;
}
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInRangeCondition.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInRangeCondition.cs
index 1296adbe..9db6306a 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInRangeCondition.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsInRangeCondition.cs
@@ -1,4 +1,7 @@
using System;
+
+using Colosseum.Combat;
+
using UnityEngine;
using Unity.Behavior;
using Unity.Properties;
@@ -26,7 +29,7 @@ namespace Colosseum.AI.BehaviorActions.Conditions
return false;
}
- float distance = Vector3.Distance(GameObject.transform.position, Target.Value.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, Target.Value);
return distance <= Range.Value;
}
}
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetBeyondDistanceCondition.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetBeyondDistanceCondition.cs
index 3bcd2b6b..c88df00e 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetBeyondDistanceCondition.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetBeyondDistanceCondition.cs
@@ -54,7 +54,7 @@ namespace Colosseum.AI.BehaviorActions.Conditions
if (target.IsDead)
continue;
- float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, candidate);
if (distance >= minDistance)
return true;
}
diff --git a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetInAttackRangeCondition.cs b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetInAttackRangeCondition.cs
index 3f51f0b9..de49ad14 100644
--- a/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetInAttackRangeCondition.cs
+++ b/Assets/_Game/Scripts/AI/BehaviorActions/Conditions/IsTargetInAttackRangeCondition.cs
@@ -1,5 +1,6 @@
using System;
+using Colosseum.Combat;
using Colosseum.Enemy;
using Unity.Behavior;
@@ -33,7 +34,7 @@ namespace Colosseum.AI.BehaviorActions.Conditions
return false;
float attackRange = Mathf.Max(0f, AttackRange.Value);
- float distance = Vector3.Distance(GameObject.transform.position, Target.Value.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(GameObject.transform.position, Target.Value);
return distance <= attackRange + 0.25f;
}
}
diff --git a/Assets/_Game/Scripts/Combat/TargetSurfaceUtility.cs b/Assets/_Game/Scripts/Combat/TargetSurfaceUtility.cs
new file mode 100644
index 00000000..b7bc6426
--- /dev/null
+++ b/Assets/_Game/Scripts/Combat/TargetSurfaceUtility.cs
@@ -0,0 +1,73 @@
+using UnityEngine;
+
+namespace Colosseum.Combat
+{
+ ///
+ /// 대상 루트가 아니라 실제 충돌 표면 기준의 위치와 거리를 계산하는 유틸리티입니다.
+ ///
+ public static class TargetSurfaceUtility
+ {
+ ///
+ /// 기준 위치에서 대상의 가장 가까운 충돌 표면 지점을 반환합니다.
+ ///
+ public static Vector3 GetClosestSurfacePoint(Vector3 origin, GameObject target)
+ {
+ if (target == null)
+ return origin;
+
+ if (TryGetTargetCollider(target, out Collider targetCollider))
+ return targetCollider.ClosestPoint(origin);
+
+ return target.transform.position;
+ }
+
+ ///
+ /// 기준 위치에서 대상 충돌 표면까지의 수평 거리를 반환합니다.
+ ///
+ public static float GetHorizontalSurfaceDistance(Vector3 origin, GameObject target)
+ {
+ Vector3 closestPoint = GetClosestSurfacePoint(origin, target);
+ Vector3 horizontalDelta = closestPoint - origin;
+ horizontalDelta.y = 0f;
+ return horizontalDelta.magnitude;
+ }
+
+ ///
+ /// 기준 위치에서 대상 충돌 표면으로 향하는 수평 방향 벡터를 반환합니다.
+ ///
+ public static Vector3 GetHorizontalDirectionToSurface(Vector3 origin, GameObject target)
+ {
+ Vector3 direction = GetClosestSurfacePoint(origin, target) - origin;
+ direction.y = 0f;
+ return direction;
+ }
+
+ ///
+ /// 대상에서 표면 계산에 사용할 대표 콜라이더를 찾습니다.
+ ///
+ public static bool TryGetTargetCollider(GameObject target, out Collider targetCollider)
+ {
+ targetCollider = null;
+ if (target == null)
+ return false;
+
+ CharacterController characterController = target.GetComponent();
+ if (characterController == null)
+ characterController = target.GetComponentInParent();
+
+ if (characterController != null)
+ {
+ targetCollider = characterController;
+ return true;
+ }
+
+ targetCollider = target.GetComponent();
+ if (targetCollider == null)
+ targetCollider = target.GetComponentInChildren();
+ if (targetCollider == null)
+ targetCollider = target.GetComponentInParent();
+
+ return targetCollider != null;
+ }
+ }
+}
diff --git a/Assets/_Game/Scripts/Combat/TargetSurfaceUtility.cs.meta b/Assets/_Game/Scripts/Combat/TargetSurfaceUtility.cs.meta
new file mode 100644
index 00000000..64b9a45a
--- /dev/null
+++ b/Assets/_Game/Scripts/Combat/TargetSurfaceUtility.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 09a6561ba8b8e4325ad39f70740b32d4
\ No newline at end of file
diff --git a/Assets/_Game/Scripts/Enemy/EnemyBase.cs b/Assets/_Game/Scripts/Enemy/EnemyBase.cs
index 3d1a6695..2b84140d 100644
--- a/Assets/_Game/Scripts/Enemy/EnemyBase.cs
+++ b/Assets/_Game/Scripts/Enemy/EnemyBase.cs
@@ -92,6 +92,7 @@ namespace Colosseum.Enemy
public EnemyData Data => enemyData;
public Animator Animator => animator;
public bool UseThreatSystem => useThreatSystem;
+ public bool IsTouchingPlayerContact => IsTouchingPlayer();
public override void OnNetworkSpawn()
{
@@ -129,6 +130,7 @@ namespace Colosseum.Enemy
UpdateThreatState(Time.deltaTime);
OnServerUpdate();
+ ApplyPlayerContactMovementLock();
}
///
@@ -137,12 +139,13 @@ namespace Colosseum.Enemy
protected virtual void OnServerUpdate() { }
///
- /// 보스와 플레이어가 겹치면 적 자신을 살짝 밀어내 겹침을 해소합니다.
- /// 점프 착지 포함, 항상 실행됩니다.
+ /// 접촉 잠금 보정을 사용하지 않는 적만 플레이어 겹침 해소를 적용합니다.
///
private void LateUpdate()
{
if (!IsServer || IsDead) return;
+ if (freezeHorizontalMotionOnPlayerContact)
+ return;
Vector3 separationOffset = ComputePlayerSeparationOffset();
if (separationOffset.sqrMagnitude <= 0.000001f)
@@ -202,11 +205,21 @@ namespace Colosseum.Enemy
// XZ: 애니메이션 진행도에 따라 목표 위치로 lerp
float t = Mathf.Clamp01(animator.GetCurrentAnimatorStateInfo(0).normalizedTime);
Vector3 newXZ = Vector3.Lerp(jumpStartXZ, jumpTargetXZ, t);
- transform.position = new Vector3(newXZ.x, transform.position.y + deltaPosition.y, newXZ.z);
+ Vector3 desiredDelta = new Vector3(
+ newXZ.x - transform.position.x,
+ deltaPosition.y,
+ newXZ.z - transform.position.z);
+ if (freezeHorizontalMotionOnPlayerContact)
+ desiredDelta = LimitHorizontalDeltaAgainstPlayerContact(desiredDelta);
+
+ transform.position += desiredDelta;
}
else
{
// jumpToTarget 없으면 기존처럼 애니메이션 루트모션 그대로 적용
+ if (freezeHorizontalMotionOnPlayerContact)
+ deltaPosition = LimitHorizontalDeltaAgainstPlayerContact(deltaPosition);
+
transform.position += deltaPosition;
}
}
@@ -218,18 +231,25 @@ namespace Colosseum.Enemy
isAirborne = false;
if (hasJumpTarget)
{
- // lerp가 1.0에 못 미쳐도 착지 시 정확한 위치로 스냅
- transform.position = new Vector3(jumpTargetXZ.x, transform.position.y, jumpTargetXZ.z);
+ // lerp가 1.0에 못 미쳐도 착지 시 목표 지점으로 보정하되, 플레이어 표면을 넘지 않도록 제한합니다.
+ Vector3 landingDelta = new Vector3(
+ jumpTargetXZ.x - transform.position.x,
+ 0f,
+ jumpTargetXZ.z - transform.position.z);
+ if (freezeHorizontalMotionOnPlayerContact)
+ landingDelta = LimitHorizontalDeltaAgainstPlayerContact(landingDelta);
+
+ transform.position += landingDelta;
}
hasJumpTarget = false;
navMeshAgent.enabled = true;
navMeshAgent.Warp(transform.position);
}
- if (freezeHorizontalMotionOnPlayerContact && IsTouchingPlayer())
+ if (freezeHorizontalMotionOnPlayerContact)
{
- deltaPosition.x = 0f;
- deltaPosition.z = 0f;
+ ApplyPlayerContactMovementLock();
+ deltaPosition = LimitHorizontalDeltaAgainstPlayerContact(deltaPosition);
}
navMeshAgent.Move(deltaPosition);
@@ -337,7 +357,7 @@ namespace Colosseum.Enemy
return Vector3.zero;
float scanRadius = GetPlayerDetectionRadius();
- int count = Physics.OverlapSphereNonAlloc(transform.position, scanRadius, overlapBuffer);
+ int count = Physics.OverlapSphereNonAlloc(bodyCollider.bounds.center, scanRadius, overlapBuffer);
Vector3 separationOffset = Vector3.zero;
int overlapCount = 0;
@@ -346,7 +366,7 @@ namespace Colosseum.Enemy
if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
continue;
if (!Physics.ComputePenetration(
- bodyCollider, transform.position, transform.rotation,
+ bodyCollider, bodyCollider.transform.position, bodyCollider.transform.rotation,
playerController, playerController.transform.position, playerController.transform.rotation,
out Vector3 separationDirection, out float separationDistance))
{
@@ -371,34 +391,273 @@ namespace Colosseum.Enemy
private bool IsTouchingPlayer()
{
+ if (bodyCollider == null)
+ return false;
+
float scanRadius = GetPlayerDetectionRadius();
- int count = Physics.OverlapSphereNonAlloc(transform.position, scanRadius, overlapBuffer);
+ int count = Physics.OverlapSphereNonAlloc(bodyCollider.bounds.center, scanRadius, overlapBuffer);
for (int i = 0; i < count; i++)
{
if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
continue;
- Vector3 toPlayer = playerController.transform.position - transform.position;
- toPlayer.y = 0f;
- if (toPlayer.magnitude < GetRequiredSeparationDistance(playerController))
+ if (Physics.ComputePenetration(
+ bodyCollider, bodyCollider.transform.position, bodyCollider.transform.rotation,
+ playerController, playerController.transform.position, playerController.transform.rotation,
+ out _, out float separationDistance)
+ && separationDistance > 0.0001f)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private Vector3 LimitHorizontalDeltaAgainstPlayerContact(Vector3 deltaPosition)
+ {
+ Vector3 horizontalDelta = new Vector3(deltaPosition.x, 0f, deltaPosition.z);
+ if (horizontalDelta.sqrMagnitude <= 0.000001f)
+ return deltaPosition;
+
+ horizontalDelta = ClampHorizontalDeltaToPlayerSurface(horizontalDelta);
+ if (horizontalDelta.sqrMagnitude <= 0.000001f)
+ {
+ deltaPosition.x = 0f;
+ deltaPosition.z = 0f;
+ return deltaPosition;
+ }
+
+ if (TryGetPlayerContactBlockDirection(out Vector3 currentBlockDirection) &&
+ IsBlockedByContactForwardHemisphere(horizontalDelta, currentBlockDirection))
+ {
+ horizontalDelta = Vector3.zero;
+ }
+
+ if (horizontalDelta.sqrMagnitude > 0.000001f &&
+ TryGetProjectedPlayerContactBlockDirection(horizontalDelta, out Vector3 projectedBlockDirection) &&
+ IsBlockedByContactForwardHemisphere(horizontalDelta, projectedBlockDirection))
+ {
+ horizontalDelta = ClampHorizontalDeltaBeforePlayerOverlap(horizontalDelta);
+ }
+
+ deltaPosition.x = horizontalDelta.x;
+ deltaPosition.z = horizontalDelta.z;
+ return deltaPosition;
+ }
+
+ private Vector3 ClampHorizontalDeltaToPlayerSurface(Vector3 horizontalDelta)
+ {
+ if (bodyCollider == null || horizontalDelta.sqrMagnitude <= 0.000001f)
+ return horizontalDelta;
+
+ if (!WouldCrossPlayerSurface(horizontalDelta))
+ return horizontalDelta;
+
+ if (WouldCrossPlayerSurface(Vector3.zero))
+ return Vector3.zero;
+
+ float min = 0f;
+ float max = 1f;
+
+ for (int i = 0; i < 8; i++)
+ {
+ float mid = (min + max) * 0.5f;
+ if (WouldCrossPlayerSurface(horizontalDelta * mid))
+ max = mid;
+ else
+ min = mid;
+ }
+
+ return horizontalDelta * min;
+ }
+
+ private float GetPlayerDetectionRadius()
+ {
+ float enemyRadius = GetBodyHorizontalRadius();
+ return enemyRadius + 1f + playerSeparationPadding;
+ }
+
+ private float GetBodyHorizontalRadius()
+ {
+ if (bodyCollider != null)
+ {
+ Bounds bounds = bodyCollider.bounds;
+ return Mathf.Max(bounds.extents.x, bounds.extents.z);
+ }
+
+ return navMeshAgent != null ? navMeshAgent.radius : 0.5f;
+ }
+
+ private void ApplyPlayerContactMovementLock()
+ {
+ if (!freezeHorizontalMotionOnPlayerContact ||
+ navMeshAgent == null ||
+ !navMeshAgent.enabled ||
+ isAirborne)
+ {
+ return;
+ }
+
+ if (!IsTouchingPlayer())
+ return;
+
+ if (!navMeshAgent.isStopped)
+ navMeshAgent.isStopped = true;
+
+ if (navMeshAgent.hasPath)
+ navMeshAgent.ResetPath();
+ }
+
+ private bool TryGetPlayerContactBlockDirection(out Vector3 blockDirection)
+ {
+ return TryGetPlayerContactBlockDirection(Vector3.zero, out blockDirection);
+ }
+
+ private bool TryGetProjectedPlayerContactBlockDirection(Vector3 horizontalDelta, out Vector3 blockDirection)
+ {
+ return TryGetPlayerContactBlockDirection(horizontalDelta, out blockDirection);
+ }
+
+ private bool TryGetPlayerContactBlockDirection(Vector3 horizontalOffset, out Vector3 blockDirection)
+ {
+ blockDirection = Vector3.zero;
+ if (bodyCollider == null)
+ return false;
+
+ float scanRadius = GetPlayerDetectionRadius() + horizontalOffset.magnitude;
+ Vector3 scanCenter = bodyCollider.bounds.center + horizontalOffset;
+ Vector3 bodyPosition = bodyCollider.transform.position + horizontalOffset;
+ 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, bodyPosition, bodyCollider.transform.rotation,
+ playerController, playerController.transform.position, playerController.transform.rotation,
+ out Vector3 separationDirection, out float separationDistance) ||
+ separationDistance <= 0.0001f)
+ {
+ continue;
+ }
+
+ Vector3 playerCenter = playerController.bounds.center;
+ Vector3 towardPlayer = playerCenter - scanCenter;
+ towardPlayer.y = 0f;
+ if (towardPlayer.sqrMagnitude <= 0.0001f)
+ {
+ towardPlayer = -separationDirection;
+ towardPlayer.y = 0f;
+ if (towardPlayer.sqrMagnitude <= 0.0001f)
+ continue;
+ }
+
+ blockDirection += towardPlayer.normalized;
+ overlapCount++;
+ }
+
+ if (overlapCount <= 0 || blockDirection.sqrMagnitude <= 0.0001f)
+ {
+ blockDirection = Vector3.zero;
+ return false;
+ }
+
+ blockDirection.Normalize();
+ return true;
+ }
+
+ private static bool IsBlockedByContactForwardHemisphere(Vector3 horizontalDelta, Vector3 blockDirection)
+ {
+ if (horizontalDelta.sqrMagnitude <= 0.000001f || blockDirection.sqrMagnitude <= 0.0001f)
+ return false;
+
+ float blockedAmount = Vector3.Dot(horizontalDelta, blockDirection);
+ return blockedAmount >= 0f;
+ }
+
+ private Vector3 ClampHorizontalDeltaBeforePlayerOverlap(Vector3 horizontalDelta)
+ {
+ if (bodyCollider == null || horizontalDelta.sqrMagnitude <= 0.000001f)
+ return horizontalDelta;
+
+ if (!HasProjectedPlayerOverlap(horizontalDelta))
+ return horizontalDelta;
+
+ if (HasProjectedPlayerOverlap(Vector3.zero))
+ return Vector3.zero;
+
+ float min = 0f;
+ float max = 1f;
+
+ for (int i = 0; i < 8; i++)
+ {
+ float mid = (min + max) * 0.5f;
+ if (HasProjectedPlayerOverlap(horizontalDelta * mid))
+ max = mid;
+ else
+ min = mid;
+ }
+
+ return horizontalDelta * min;
+ }
+
+ private bool WouldCrossPlayerSurface(Vector3 horizontalOffset)
+ {
+ if (bodyCollider == null)
+ return false;
+
+ float enemyRadius = GetBodyHorizontalRadius();
+ float minimumSurfaceDistance = enemyRadius + playerSeparationPadding;
+ float scanRadius = GetPlayerDetectionRadius() + horizontalOffset.magnitude;
+ Vector3 scanCenter = bodyCollider.bounds.center + horizontalOffset;
+ int count = Physics.OverlapSphereNonAlloc(scanCenter, scanRadius, overlapBuffer);
+
+ for (int i = 0; i < count; i++)
+ {
+ if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
+ continue;
+
+ Vector3 closestPoint = playerController.ClosestPoint(scanCenter);
+ Vector3 horizontalToSurface = closestPoint - scanCenter;
+ horizontalToSurface.y = 0f;
+ if (horizontalToSurface.magnitude <= minimumSurfaceDistance + 0.001f)
return true;
}
return false;
}
- private float GetPlayerDetectionRadius()
+ private bool HasProjectedPlayerOverlap(Vector3 horizontalOffset)
{
- float enemyRadius = navMeshAgent != null ? navMeshAgent.radius : 0.5f;
- return enemyRadius + 1f + playerSeparationPadding;
- }
+ if (bodyCollider == null)
+ return false;
- private float GetRequiredSeparationDistance(CharacterController playerController)
- {
- float enemyRadius = navMeshAgent != null ? navMeshAgent.radius : 0.5f;
- float playerRadius = playerController != null ? playerController.radius : 0.5f;
- return enemyRadius + playerRadius + playerSeparationPadding;
+ float scanRadius = GetPlayerDetectionRadius() + horizontalOffset.magnitude;
+ Vector3 scanCenter = bodyCollider.bounds.center + horizontalOffset;
+ Vector3 bodyPosition = bodyCollider.transform.position + horizontalOffset;
+ int count = Physics.OverlapSphereNonAlloc(scanCenter, scanRadius, overlapBuffer);
+
+ for (int i = 0; i < count; i++)
+ {
+ if (!TryGetPlayerCharacterController(overlapBuffer[i], out CharacterController playerController))
+ continue;
+
+ if (Physics.ComputePenetration(
+ bodyCollider, bodyPosition, bodyCollider.transform.rotation,
+ playerController, playerController.transform.position, playerController.transform.rotation,
+ out _, out float separationDistance)
+ && separationDistance > 0.0001f)
+ {
+ return true;
+ }
+ }
+
+ return false;
}
private static bool TryGetPlayerCharacterController(Collider overlapCollider, out CharacterController playerController)
@@ -780,7 +1039,7 @@ namespace Colosseum.Enemy
if (!float.IsInfinity(maxDistance))
{
- float distance = Vector3.Distance(transform.position, target.transform.position);
+ float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(transform.position, target);
if (distance > maxDistance)
return false;
}
diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs
index a514287d..ed31d8bc 100644
--- a/Assets/_Game/Scripts/Player/PlayerMovement.cs
+++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs
@@ -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(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();
+ if (enemy == null)
+ enemy = overlapCollider.GetComponentInParent();
+
+ if (enemy == null)
+ return false;
+
+ enemyCollider = overlapCollider;
+ return enemyCollider != null;
+ }
}
}
diff --git a/Assets/_Game/Scripts/Skills/SkillController.cs b/Assets/_Game/Scripts/Skills/SkillController.cs
index 902854fb..f4b989df 100644
--- a/Assets/_Game/Scripts/Skills/SkillController.cs
+++ b/Assets/_Game/Scripts/Skills/SkillController.cs
@@ -6,6 +6,7 @@ using UnityEngine;
using Unity.Netcode;
using Colosseum.Abnormalities;
+using Colosseum.Combat;
using Colosseum.Player;
#if UNITY_EDITOR
@@ -1266,20 +1267,26 @@ namespace Colosseum.Skills
if (IsSpawned && !IsServer)
return;
- Vector3 direction = currentTargetOverride.transform.position - transform.position;
- direction.y = 0f;
+ Vector3 direction = TargetSurfaceUtility.GetHorizontalDirectionToSurface(transform.position, currentTargetOverride);
if (direction.sqrMagnitude < 0.0001f)
return;
+ bool suppressRotationWhileContactingPlayer = currentSkill.UseRootMotion
+ && currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget
+ && enemyBase.IsTouchingPlayerContact;
+
if (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget ||
currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.MoveTowardTarget)
{
- Quaternion targetRotation = Quaternion.LookRotation(direction.normalized);
- float rotationSpeed = Mathf.Max(0f, currentSkill.CastTargetRotationSpeed);
- if (rotationSpeed <= 0f)
- transform.rotation = targetRotation;
- else
- transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * 360f * Time.deltaTime);
+ if (!suppressRotationWhileContactingPlayer)
+ {
+ Quaternion targetRotation = Quaternion.LookRotation(direction.normalized);
+ float rotationSpeed = Mathf.Max(0f, currentSkill.CastTargetRotationSpeed);
+ if (rotationSpeed <= 0f)
+ transform.rotation = targetRotation;
+ else
+ transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * 360f * Time.deltaTime);
+ }
}
if (currentSkill.CastTargetTrackingMode != SkillCastTargetTrackingMode.MoveTowardTarget || currentSkill.UseRootMotion)
@@ -1290,7 +1297,8 @@ namespace Colosseum.Skills
return;
float stopDistance = Mathf.Max(0f, currentSkill.CastTargetStopDistance);
- if (direction.magnitude <= stopDistance)
+ float surfaceDistance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(transform.position, currentTargetOverride);
+ if (surfaceDistance <= stopDistance)
{
navMeshAgent.isStopped = true;
navMeshAgent.ResetPath();
@@ -1298,7 +1306,7 @@ namespace Colosseum.Skills
}
navMeshAgent.isStopped = false;
- navMeshAgent.SetDestination(currentTargetOverride.transform.position);
+ navMeshAgent.SetDestination(TargetSurfaceUtility.GetClosestSurfacePoint(transform.position, currentTargetOverride));
}
///