diff --git a/Assets/Scripts/EnemyAIController.cs b/Assets/Scripts/EnemyAIController.cs index baa2eb8..08df0c6 100644 --- a/Assets/Scripts/EnemyAIController.cs +++ b/Assets/Scripts/EnemyAIController.cs @@ -90,7 +90,7 @@ namespace Northbound private bool _isAttacking = false; private GameObject _pendingAttackTarget; private float _attackStartTime; - private const float ATTACK_TIMEOUT = 3f; // 애니메이션 이벤트 미발생 시 타임아웃 + private const float ATTACK_TIMEOUT = 1f; // 애니메이션 이벤트 미발생 시 타임아웃 public override void OnNetworkSpawn() { @@ -106,8 +106,8 @@ namespace Northbound { // NavMeshAgent 초기 설정 _agent.speed = moveSpeed; - _agent.acceleration = 8f; - _agent.angularSpeed = 120f; + _agent.acceleration = 100f; // 높은 가속도로 즉시 정지 가능 + _agent.angularSpeed = 360f; // 빠른 회전 _agent.stoppingDistance = attackRange * 0.9f; // 공격 범위까지 더 가까이 이동 _agent.autoBraking = true; _agent.updateRotation = true; @@ -173,19 +173,15 @@ namespace Northbound if (_isRecalculatingPath) return; // 코루틴 대기 중이면 중단 if (_coreTransform == null) { FindCore(); return; } - // 0. 경로 계산 중이거나 이동 중이 아니면 아무것도 하지 않음 - if (_agent.pathPending) return; // 경로 계산 중 + // 0. 경로 계산 중이면 대기 + if (_agent.pathPending) return; - // 0.5. 실제로 이동 중인지 확인 (velocity로 판단) - if (_agent.hasPath && _agent.velocity.sqrMagnitude > 0.01f) + // 0.5. 코어 콜라이더 표면까지의 거리로 도달 확인 (이동 중 여부와 관계없이 항상 체크) + if (GetDistanceToCoreSurface() <= attackRange) { - // 이동 중이면 코어 콜라이더 표면까지의 거리로 도달 확인 - if (GetDistanceToCoreSurface() <= attackRange) - { - SetTargetPlayer(_coreTransform.gameObject); - TransitionToState(EnemyAIState.Attack); - return; - } + SetTargetPlayer(_coreTransform.gameObject); + TransitionToState(EnemyAIState.Attack); + return; } // 1. Player 감지 (코어로 가는 도중에도 Player/건물을 타겟팅) @@ -231,7 +227,20 @@ namespace Northbound return; } - float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.transform.position); + // 코어 타겟인지 확인 + bool isCoreTarget = (targetPlayer == _coreTransform?.gameObject); + + // 거리 계산 - 코어는 콜라이더 표면까지의 거리 사용 + float distanceToPlayer; + if (isCoreTarget) + { + distanceToPlayer = GetDistanceToCoreSurface(); + } + else + { + distanceToPlayer = GetDistanceToTarget(targetPlayer); + } + Vector3 chaseReferencePoint = (aiType == TeamType.Monster) ? _chaseStartPosition : _originPosition; float distanceFromReference = Vector3.Distance(transform.position, chaseReferencePoint); @@ -258,6 +267,12 @@ namespace Northbound // 공격 타임아웃 체크 (애니메이션 이벤트가 발생하지 않은 경우) if (_isAttacking && Time.time - _attackStartTime > ATTACK_TIMEOUT) { + if (showDebugInfo) + { + Debug.LogWarning($"[EnemyAIController] 공격 타임아웃 - 애니메이션 이벤트 미발생: {gameObject.name}"); + } + // 타임아웃 시 공격 실행 후 상태 리셋 + PerformAttack(); _isAttacking = false; _pendingAttackTarget = null; } @@ -475,6 +490,9 @@ namespace Northbound else if (_animator != null) { _animator.SetTrigger("Attack"); + // 애니메이션은 있지만 NetworkAnimator가 없으면 즉시 공격 (이벤트 미지원) + PerformAttack(); + _isAttacking = false; } else { @@ -487,6 +505,10 @@ namespace Northbound else { // 공격할 수 없는 대상이면 상태를 해제합니다. + if (showDebugInfo) + { + Debug.LogWarning($"[EnemyAIController] IDamageable을 찾을 수 없음: {target.name}"); + } OnLostTarget(); } } @@ -513,7 +535,14 @@ namespace Northbound /// private void PerformAttack() { - if (_pendingAttackTarget == null) return; + if (_pendingAttackTarget == null) + { + if (showDebugInfo) + { + Debug.LogWarning("[EnemyAIController] PerformAttack: 타겟이 없음"); + } + return; + } IDamageable damageable = _pendingAttackTarget.GetComponentInChildren(); if (damageable == null) damageable = _pendingAttackTarget.GetComponent(); @@ -523,6 +552,18 @@ namespace Northbound { damageable.TakeDamage(attackDamage, NetworkObjectId); OnAttackPerformed?.Invoke(_pendingAttackTarget); + + if (showDebugInfo) + { + Debug.Log($"[EnemyAIController] {gameObject.name}이(가) {_pendingAttackTarget.name}에게 {attackDamage} 데미지 적용"); + } + } + else + { + if (showDebugInfo) + { + Debug.LogWarning($"[EnemyAIController] PerformAttack: 대상이 유효하지 않음 - {_pendingAttackTarget.name}"); + } } } @@ -569,6 +610,8 @@ namespace Northbound case EnemyAIState.Attack: _agent.isStopped = true; _agent.ResetPath(); + // 즉시 정지를 위해 velocity 초기화 + _agent.velocity = Vector3.zero; break; case EnemyAIState.MoveToCore: _agent.isStopped = false; @@ -699,6 +742,57 @@ namespace Northbound Gizmos.DrawLine(transform.position, transform.position + right); } } + + private void OnDrawGizmosSelected() + { + #if UNITY_EDITOR + // 상태 및 타겟 정보 표시 + string stateInfo = $"State: {_currentState.Value}"; + string targetInfo = "Target: None"; + + if (_cachedTargetPlayer != null) + { + targetInfo = $"Target: {_cachedTargetPlayer.name}"; + } + else if (_targetPlayerId.Value != 0) + { + targetInfo = $"Target ID: {_targetPlayerId.Value}"; + } + else if (_coreTransform != null && _currentState.Value == EnemyAIState.MoveToCore) + { + targetInfo = "Target: Core (moving)"; + } + + string attackInfo = _isAttacking ? " [ATTACKING]" : ""; + string distanceInfo = ""; + + if (_coreTransform != null) + { + float distToCore = GetDistanceToCoreSurface(); + distanceInfo = $"\nDistToCore: {distToCore:F1} (Range: {attackRange})"; + } + + string fullInfo = $"{gameObject.name}\n{stateInfo}\n{targetInfo}{attackInfo}{distanceInfo}"; + + UnityEditor.Handles.Label( + transform.position + Vector3.up * 3f, + fullInfo, + new GUIStyle(GUI.skin.label) { fontSize = 12, normal = { textColor = Color.white } } + ); + + // 타겟으로 선 그리기 + if (_cachedTargetPlayer != null) + { + Gizmos.color = Color.magenta; + Gizmos.DrawLine(transform.position + Vector3.up, _cachedTargetPlayer.transform.position + Vector3.up); + } + else if (_coreTransform != null && _currentState.Value == EnemyAIState.MoveToCore) + { + Gizmos.color = Color.cyan; + Gizmos.DrawLine(transform.position + Vector3.up, _coreTransform.position + Vector3.up); + } + #endif + } #endregion } } \ No newline at end of file