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)); } ///