using Unity.Netcode; using UnityEngine; using UnityEngine.AI; using System.Collections; using System.Collections.Generic; namespace Northbound { /// /// 몬스터와 적대 세력의 AI 컨트롤러 /// [RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(EnemyUnit))] public class EnemyAIController : NetworkBehaviour { [Header("AI Type")] [Tooltip("Monster: 코어로 이동, Hostile: 제자리에서 대기")] public TeamType aiType = TeamType.Monster; [Header("Detection")] [Tooltip("플레이어 감지 범위")] public float detectionRange = 15f; [Tooltip("시야 각도 (0-360, 360=전방향)")] [Range(0, 360)] public float detectionAngle = 120f; [Tooltip("탐지할 레이어")] public LayerMask playerLayer = ~0; [Tooltip("시야 체크 장애물 레이어")] public LayerMask obstacleLayer = ~0; [Header("Chase Settings")] [Tooltip("추적 최대 거리 (이 거리 이상 추적하면 중단)")] public float maxChaseDistance = 30f; [Tooltip("추적 포기 거리 (플레이어와 이 거리 이상 멀어지면 추적 중단)")] public float chaseGiveUpDistance = 25f; [Header("Combat")] [Tooltip("공격 범위")] public float attackRange = 2f; [Tooltip("공격 간격 (초)")] public float attackInterval = 1.5f; [Tooltip("공격 데미지")] public int attackDamage = 10; [Header("Movement")] [Tooltip("이동 속도")] public float moveSpeed = 3.5f; [Tooltip("추적 중 속도 배율")] public float chaseSpeedMultiplier = 1.5f; [Header("Debug")] [Tooltip("디버그 정보 표시")] public bool showDebugInfo = true; [Header("Events")] public System.Action OnAttackPerformed; [Header("Camp Alert System")] [Tooltip("이 크립이 속한 캠프 (null이면 독립 크립)")] public CreepCamp creepCamp; [Header("Aggro System")] [Tooltip("어그로(위협도) 시스템 활성화")] public bool enableAggroSystem = true; [Tooltip("데미지당 어그로 증가량")] public float damageAggroMultiplier = 2f; [Tooltip("어그로 감쇠율 (초당)")] public float aggroDecayRate = 5f; [Tooltip("새 타겟 전환에 필요한 최소 어그로 차이")] public float aggroSwitchThreshold = 20f; [Tooltip("타겟 재평가 주기 (초)")] public float targetReevaluateInterval = 1f; [Tooltip("거리 기반 타겟 전환 허용 거리차")] public float distanceSwitchThreshold = 5f; private NavMeshAgent _agent; private EnemyUnit _enemyUnit; private Core _core; private Transform _coreTransform; private Collider _coreCollider; private Vector3 _originPosition; private Vector3 _chaseStartPosition; private float _lastAttackTime; private bool _hasSetCoreDestination; private bool _isRecalculatingPath = false; // NavMesh 갱신 대기 플래그 private NetworkVariable _currentState = new NetworkVariable( EnemyAIState.Idle, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _targetPlayerId = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private GameObject _cachedTargetPlayer; private Animator _animator; private Unity.Netcode.Components.NetworkAnimator _networkAnimator; private bool _isAttacking = false; private GameObject _pendingAttackTarget; private float _attackStartTime; private const float ATTACK_TIMEOUT = 1f; // 애니메이션 이벤트 미발생 시 타임아웃 // 어그로 시스템 private Dictionary _aggroTable = new Dictionary(); private float _lastTargetReevaluateTime; private System.Action _damageTakenHandler; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); _agent = GetComponent(); _enemyUnit = GetComponent(); _animator = GetComponent(); _networkAnimator = GetComponent(); _originPosition = transform.position; if (IsServer) { // NavMeshAgent 초기 설정 _agent.speed = moveSpeed; _agent.acceleration = 100f; // 높은 가속도로 즉시 정지 가능 _agent.angularSpeed = 360f; // 빠른 회전 _agent.stoppingDistance = attackRange * 0.9f; // 공격 범위까지 더 가까이 이동 _agent.autoBraking = true; _agent.updateRotation = true; _agent.updateUpAxis = false; if (aiType == TeamType.Monster) { FindCore(); TransitionToState(EnemyAIState.MoveToCore); } else if (aiType == TeamType.Hostile) { TransitionToState(EnemyAIState.Idle); } // 사망 이벤트 구독 if (_enemyUnit != null) { _enemyUnit.OnDeath += HandleDeath; // 데미지 받음 이벤트 구독 (어그로 시스템) if (enableAggroSystem) { _damageTakenHandler = (attackerId, damage) => HandleDamageTaken(attackerId, damage); _enemyUnit.OnDamageTaken += _damageTakenHandler; } } } } public override void OnNetworkDespawn() { if (_enemyUnit != null) { _enemyUnit.OnDeath -= HandleDeath; if (_damageTakenHandler != null) { _enemyUnit.OnDamageTaken -= _damageTakenHandler; } } base.OnNetworkDespawn(); } private void Update() { if (!IsServer) return; if (!_agent.isOnNavMesh) return; // 어그로 시스템 업데이트 if (enableAggroSystem) { ReevaluateTarget(); } switch (_currentState.Value) { case EnemyAIState.Idle: UpdateIdle(); break; case EnemyAIState.MoveToCore: UpdateMoveToCore(); break; case EnemyAIState.ChasePlayer: UpdateChasePlayer(); break; case EnemyAIState.Attack: UpdateAttack(); break; case EnemyAIState.ReturnToOrigin: UpdateReturnToOrigin(); break; case EnemyAIState.Dead: break; // 사망 상태에서는 아무것도 하지 않음 } } #region State Updates private void UpdateIdle() { GameObject player = DetectTarget(); if (player != null) { SetTargetPlayer(player); // 캠프 내 모든 크립에게 경고 전파 if (creepCamp != null) { creepCamp.AlertAllCreeps(player, this); } TransitionToState(EnemyAIState.ChasePlayer); } } private void UpdateMoveToCore() { if (_isRecalculatingPath) return; // 코루틴 대기 중이면 중단 if (_coreTransform == null) { FindCore(); return; } // 0. 경로 계산 중이면 대기 if (_agent.pathPending) return; // 0.5. 코어 콜라이더 표면까지의 거리로 도달 확인 (이동 중 여부와 관계없이 항상 체크) if (GetDistanceToCoreSurface() <= attackRange) { SetTargetPlayer(_coreTransform.gameObject); TransitionToState(EnemyAIState.Attack); return; } // 1. Player 감지 (코어로 가는 도중에도 Player/건물을 타겟팅) GameObject detectedPlayer = DetectTarget(); if (detectedPlayer != null) { SetTargetPlayer(detectedPlayer); // 캠프 내 모든 크립에게 경고 전파 if (creepCamp != null) { creepCamp.AlertAllCreeps(detectedPlayer, this); } TransitionToState(EnemyAIState.ChasePlayer); return; } // 2. 경로 상의 장애물(건물) 감지 - 경로 상태와 관계없이 확인 GameObject obstacle = DetectObstacle(); if (obstacle != null) { SetTargetPlayer(obstacle); TransitionToState(EnemyAIState.Attack); return; } // 3. 코어로 가는 경로 설정 if (!_hasSetCoreDestination || !_agent.hasPath || _agent.pathStatus == NavMeshPathStatus.PathInvalid) { _agent.SetDestination(_core.GetNavMeshPosition()); _hasSetCoreDestination = true; } } private void UpdateChasePlayer() { GameObject targetPlayer = GetTargetPlayer(); if (targetPlayer == null) { OnLostTarget(); return; } // 타겟이 사망했는지 확인 IDamageable damageable = targetPlayer.GetComponentInParent(); if (damageable != null && damageable.IsDead()) { OnLostTarget(); return; } // 코어 타겟인지 확인 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); if (distanceToPlayer > chaseGiveUpDistance || distanceFromReference > maxChaseDistance) { OnLostTarget(); return; } if (distanceToPlayer <= attackRange) { TransitionToState(EnemyAIState.Attack); return; } if (_agent.isOnNavMesh && !_agent.isStopped) { _agent.SetDestination(targetPlayer.transform.position); } } private void UpdateAttack() { // 공격 타임아웃 체크 (애니메이션 이벤트가 발생하지 않은 경우) if (_isAttacking && Time.time - _attackStartTime > ATTACK_TIMEOUT) { if (showDebugInfo) { Debug.LogWarning($"[EnemyAIController] 공격 타임아웃 - 애니메이션 이벤트 미발생: {gameObject.name}"); } // 타임아웃 시 공격 실행 후 상태 리셋 PerformAttack(); _isAttacking = false; _pendingAttackTarget = null; } GameObject target = GetTargetPlayer(); if (target == null) { if (showDebugInfo) {} OnLostTarget(); return; } // 타겟이 사망했는지 확인 IDamageable damageable = target.GetComponentInParent(); if (damageable != null && damageable.IsDead()) { OnLostTarget(); return; } // 코어 공격 시에는 도달 시점부터 항상 공격 가능하다고 가정 (거리 체크 생략) bool isCoreTarget = (target == _coreTransform?.gameObject); if (!isCoreTarget) { // 일반 타겟(플레이어, 장애물)은 거리 체크 필요 float distance = GetDistanceToTarget(target); // 공격 범위 체크 (공격 시 약간의 거리 여유 1.2f 부여) if (distance > attackRange * 1.2f) { // 거리가 멀어지면 추적 상태로 전환 TransitionToState(EnemyAIState.ChasePlayer); return; } } // 타겟 바라보기 Vector3 direction = (target.transform.position - transform.position).normalized; direction.y = 0; if (direction != Vector3.zero) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 5f); } // 공격 실행 if (Time.time - _lastAttackTime >= attackInterval) { AttackTarget(target); } } private void UpdateReturnToOrigin() { // 복귀 중에는 플레이어 감지하지 않음 (무한 루프 방지) // Idle 상태에 도달하면 다시 감지 시작 if (!_agent.pathPending && _agent.remainingDistance <= _agent.stoppingDistance) { if (!_agent.hasPath || _agent.velocity.sqrMagnitude == 0f) { TransitionToState(EnemyAIState.Idle); } } } #endregion #region Detection & Target Management private GameObject DetectTarget() { LayerMask targetMask = playerLayer | obstacleLayer; Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, targetMask); GameObject closestTarget = null; float closestDistance = float.MaxValue; foreach (Collider col in colliders) { if (col.transform.root == transform.root) continue; IDamageable damageable = col.GetComponentInParent(); if (damageable == null) continue; // 이미 사망한 타겟은 제외 if (damageable.IsDead()) continue; ITeamMember teamMember = col.GetComponentInParent(); bool isAttackable = (teamMember != null && teamMember.GetTeam() == TeamType.Player) || (teamMember == null); if (!isAttackable) continue; float distance = Vector3.Distance(transform.position, col.transform.position); if (detectionAngle < 360f) { Vector3 directionToTarget = (col.transform.position - transform.position).normalized; if (Vector3.Angle(transform.forward, directionToTarget) > detectionAngle / 2f) continue; } if (distance < closestDistance) { closestDistance = distance; // 부모(Walls)가 아닌 실제 콜라이더가 있는 자식(wall)을 잡음 closestTarget = col.gameObject; } } return closestTarget; } public GameObject DetectObstacle() { RaycastHit hit; Vector3 origin = transform.position + Vector3.up * 1.0f; Vector3 direction = transform.forward; float castDistance = attackRange * 1.5f; if (Physics.SphereCast(origin, 0.5f, direction, out hit, castDistance, obstacleLayer)) { if (hit.collider.GetComponentInParent() != null) { // hit.collider.gameObject를 반환함으로써 자식 오브젝트를 직접 타겟팅함 return hit.collider.gameObject; } } return null; } private void SetTargetPlayer(GameObject target) { if (target == null) return; _cachedTargetPlayer = target; var networkObject = target.GetComponentInParent(); if (networkObject != null) { _targetPlayerId.Value = networkObject.NetworkObjectId; // 타겟 설정 시 기본 어그로 부여 (최초 감지) if (enableAggroSystem && !_aggroTable.ContainsKey(networkObject.NetworkObjectId)) { _aggroTable[networkObject.NetworkObjectId] = 10f; // 기본 어그로 } } else { _targetPlayerId.Value = 0; } } private void ClearTargetPlayer() { _targetPlayerId.Value = 0; _cachedTargetPlayer = null; } private GameObject GetTargetPlayer() { // 1순위: 물리적 참조 (장애물/서버 전용) if (_cachedTargetPlayer != null && _cachedTargetPlayer.activeInHierarchy) { // 사망 상태 체크 IDamageable damageable = _cachedTargetPlayer.GetComponentInParent(); if (damageable == null || damageable.IsDead()) { ClearTargetPlayer(); return null; } return _cachedTargetPlayer; } // 2순위: 네트워크 ID 검색 if (_targetPlayerId.Value != 0) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(_targetPlayerId.Value, out NetworkObject networkObject)) { _cachedTargetPlayer = networkObject.gameObject; // 사망 상태 체크 IDamageable damageable = _cachedTargetPlayer.GetComponentInParent(); if (damageable != null && damageable.IsDead()) { ClearTargetPlayer(); return null; } return _cachedTargetPlayer; } } // 3순위: 코어 확인 if (_currentState.Value == EnemyAIState.Attack && _coreTransform != null) { if (GetDistanceToCoreSurface() <= attackRange * 1.5f) return _coreTransform.gameObject; } return null; } #endregion #region Combat Logic private void AttackTarget(GameObject target) { // 이미 공격 중이면 대기 if (_isAttacking) return; // 1. 타겟의 자식, 본인, 부모 순으로 샅샛이 뒤져서 IDamageable을 찾습니다. IDamageable damageable = target.GetComponentInChildren(); if (damageable == null) damageable = target.GetComponent(); if (damageable == null) damageable = target.GetComponentInParent(); if (damageable != null) { // 2. 공격 쿨타임 체크 if (Time.time - _lastAttackTime >= attackInterval) { _lastAttackTime = Time.time; _isAttacking = true; _pendingAttackTarget = target; _attackStartTime = Time.time; // 애니메이션 재생 (서버에서 NetworkAnimator로 동기화) if (_networkAnimator != null) { _networkAnimator.SetTrigger("Attack"); } else if (_animator != null) { _animator.SetTrigger("Attack"); // 애니메이션은 있지만 NetworkAnimator가 없으면 즉시 공격 (이벤트 미지원) PerformAttack(); _isAttacking = false; } else { // 애니메이션이 없으면 즉시 공격 실행 PerformAttack(); _isAttacking = false; } } } else { // 공격할 수 없는 대상이면 상태를 해제합니다. if (showDebugInfo) { Debug.LogWarning($"[EnemyAIController] IDamageable을 찾을 수 없음: {target.name}"); } OnLostTarget(); } } /// /// 애니메이션 이벤트: 공격 타격 시점에 호출 /// public void OnAttackHit() { PerformAttack(); } /// /// 애니메이션 이벤트: 공격 완료 시 호출 /// public void OnAttackComplete() { _isAttacking = false; _pendingAttackTarget = null; } /// /// 실제 공격 수행 (데미지 적용) /// private void PerformAttack() { if (_pendingAttackTarget == null) { if (showDebugInfo) { Debug.LogWarning("[EnemyAIController] PerformAttack: 타겟이 없음"); } return; } IDamageable damageable = _pendingAttackTarget.GetComponentInChildren(); if (damageable == null) damageable = _pendingAttackTarget.GetComponent(); if (damageable == null) damageable = _pendingAttackTarget.GetComponentInParent(); if (damageable != null && !damageable.IsDead()) { 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}"); } } } #endregion #region Aggro System private void HandleDamageTaken(ulong attackerId, int damage) { if (!IsServer) return; if (!enableAggroSystem) return; // 공격자에게 어그로 추가 AddAggro(attackerId, damage * damageAggroMultiplier); // 즉시 타겟 전환 고려 ConsiderTargetSwitch(attackerId); } private void AddAggro(ulong attackerId, float amount) { if (_aggroTable.ContainsKey(attackerId)) { _aggroTable[attackerId] += amount; } else { _aggroTable[attackerId] = amount; } } private void DecayAggro() { List keysToRemove = new List(); List> updates = new List>(); foreach (var kvp in _aggroTable) { float newAggro = kvp.Value - aggroDecayRate * Time.deltaTime; if (newAggro <= 0) { keysToRemove.Add(kvp.Key); } else { updates.Add(new KeyValuePair(kvp.Key, newAggro)); } } // 순회 후 수정 적용 foreach (var update in updates) { _aggroTable[update.Key] = update.Value; } // 어그로가 0 이하인 항목 제거 foreach (var key in keysToRemove) { _aggroTable.Remove(key); } } private void ConsiderTargetSwitch(ulong attackerId) { // 현재 ChasePlayer 또는 Attack 상태가 아니면 무시 if (_currentState.Value != EnemyAIState.ChasePlayer && _currentState.Value != EnemyAIState.Attack) { return; } // 현재 타겟의 어그로 확인 ulong currentTargetId = _targetPlayerId.Value; float currentAggro = 0f; if (currentTargetId != 0 && _aggroTable.TryGetValue(currentTargetId, out float currAgg)) { currentAggro = currAgg; } // 공격자의 어그로 확인 if (!_aggroTable.TryGetValue(attackerId, out float attackerAggro)) { return; } // 어그로 차이가 임계값 이상이면 타겟 전환 if (attackerAggro >= currentAggro + aggroSwitchThreshold) { // 공격자를 NetworkObject로 찾기 if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj)) { GameObject attacker = attackerObj.gameObject; // 유효한 타겟인지 확인 (사망하지 않음, 거리 내에 있음) IDamageable damageable = attacker.GetComponentInParent(); if (damageable != null && !damageable.IsDead()) { float distance = GetDistanceToTarget(attacker); if (distance <= maxChaseDistance) { SetTargetPlayer(attacker); // 캠프에 알림 if (creepCamp != null) { creepCamp.AlertAllCreeps(attacker, this); } if (_currentState.Value != EnemyAIState.ChasePlayer) { TransitionToState(EnemyAIState.ChasePlayer); } } } } } } private void ReevaluateTarget() { if (!enableAggroSystem) return; if (Time.time - _lastTargetReevaluateTime < targetReevaluateInterval) return; _lastTargetReevaluateTime = Time.time; // 어그로 감쇠 DecayAggro(); // 현재 추적 중이 아니면 감지 로직에 맡김 if (_currentState.Value != EnemyAIState.ChasePlayer && _currentState.Value != EnemyAIState.Attack) { return; } GameObject currentTarget = GetTargetPlayer(); if (currentTarget == null) return; // 최고 어그로 타겟 찾기 ulong highestAggroId = 0; float highestAggro = 0f; foreach (var kvp in _aggroTable) { if (kvp.Value > highestAggro) { // 유효한 타겟인지 확인 if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(kvp.Key, out NetworkObject netObj)) { IDamageable damageable = netObj.GetComponentInParent(); if (damageable != null && !damageable.IsDead()) { float distance = GetDistanceToTarget(netObj.gameObject); if (distance <= maxChaseDistance) { highestAggroId = kvp.Key; highestAggro = kvp.Value; } } } } } // 현재 타겟이 최고 어그로가 아니면 전환 고려 ulong currentTargetNetId = 0; var currentNetObj = currentTarget.GetComponentInParent(); if (currentNetObj != null) { currentTargetNetId = currentNetObj.NetworkObjectId; } if (highestAggroId != 0 && highestAggroId != currentTargetNetId) { float currentAggro = 0f; _aggroTable.TryGetValue(currentTargetNetId, out currentAggro); if (highestAggro >= currentAggro + aggroSwitchThreshold) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(highestAggroId, out NetworkObject newTargetObj)) { SetTargetPlayer(newTargetObj.gameObject); } } } // 거리 기반 타겟 전환 (어그로가 비슷하면 더 가까운 타겟으로) ConsiderDistanceBasedSwitch(); } private void ConsiderDistanceBasedSwitch() { GameObject currentTarget = GetTargetPlayer(); if (currentTarget == null) return; float currentDistance = GetDistanceToTarget(currentTarget); // 주변 플레이어 검색 Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, playerLayer); foreach (Collider col in colliders) { if (col.transform.root == transform.root) continue; if (col.gameObject == currentTarget) continue; IDamageable damageable = col.GetComponentInParent(); if (damageable == null || damageable.IsDead()) continue; ITeamMember teamMember = col.GetComponentInParent(); if (teamMember != null && teamMember.GetTeam() != TeamType.Player) continue; float newDistance = GetDistanceToTarget(col.gameObject); // 현재 타겟보다 충분히 가까우면 전환 고려 if (currentDistance - newDistance >= distanceSwitchThreshold) { // 현재 타겟의 어그로 가져오기 ulong currentTargetId = _targetPlayerId.Value; float currentAggro = 0f; if (currentTargetId != 0) { _aggroTable.TryGetValue(currentTargetId, out currentAggro); } // 새 타겟이 어그로가 비슷하거나 높으면 전환 var newNetObj = col.GetComponentInParent(); if (newNetObj != null) { float newAggro = 0f; _aggroTable.TryGetValue(newNetObj.NetworkObjectId, out newAggro); // 거리 우선: 어그로가 크게 차이나지 않으면 더 가까운 타겟으로 if (newAggro >= currentAggro - aggroSwitchThreshold * 0.5f) { SetTargetPlayer(col.gameObject); if (creepCamp != null) { creepCamp.AlertAllCreeps(col.gameObject, this); } return; // 한 번에 하나만 전환 } } } } } #endregion #region Utilities & Distance private float GetDistanceToTarget(GameObject target) { if (target == _coreTransform?.gameObject) return GetDistanceToCoreSurface(); Collider col = target.GetComponent(); if (col != null) { return Vector3.Distance(transform.position, col.ClosestPoint(transform.position)); } return Vector3.Distance(transform.position, target.transform.position); } private float GetDistanceToCoreSurface() { if (_coreTransform == null) return float.MaxValue; if (_coreCollider != null) { Vector3 closestPoint = _coreCollider.ClosestPoint(transform.position); return Vector3.Distance(transform.position, closestPoint); } return Vector3.Distance(transform.position, _coreTransform.position); } private void TransitionToState(EnemyAIState newState) { if (_currentState.Value == newState) return; OnExitState(_currentState.Value); _currentState.Value = newState; OnEnterState(newState); } private void OnEnterState(EnemyAIState state) { switch (state) { case EnemyAIState.Idle: _agent.isStopped = true; _agent.ResetPath(); // 즉시 정지를 위해 velocity 초기화 _agent.velocity = Vector3.zero; // 복귀 완료 - 무적 해제 if (_enemyUnit != null) { _enemyUnit.SetInvulnerable(false); } break; case EnemyAIState.Attack: _agent.isStopped = true; _agent.ResetPath(); // 즉시 정지를 위해 velocity 초기화 _agent.velocity = Vector3.zero; break; case EnemyAIState.MoveToCore: _agent.isStopped = false; _agent.speed = moveSpeed; if (_core != null) { _agent.SetDestination(_core.GetNavMeshPosition()); _hasSetCoreDestination = true; } break; case EnemyAIState.ChasePlayer: _agent.isStopped = false; _agent.speed = moveSpeed * chaseSpeedMultiplier; _chaseStartPosition = transform.position; break; case EnemyAIState.ReturnToOrigin: _agent.isStopped = false; _agent.speed = moveSpeed; _agent.SetDestination(_originPosition); // 복귀 시작 - 체력 회복 및 무적 if (_enemyUnit != null) { _enemyUnit.HealToFull(); _enemyUnit.SetInvulnerable(true); } break; case EnemyAIState.Dead: _agent.isStopped = true; _agent.ResetPath(); _agent.enabled = false; // NavMeshAgent 비활성화 break; } } private void OnExitState(EnemyAIState state) { } private void HandleDeath(ulong killerId) { if (!IsServer) return; // 사망 상태로 전환 TransitionToState(EnemyAIState.Dead); ClearTargetPlayer(); if (showDebugInfo) {} } private void OnLostTarget() { ClearTargetPlayer(); if (IsServer && _agent.isOnNavMesh) { // 1. 현재 가던 길을 즉시 멈춤 _agent.ResetPath(); _hasSetCoreDestination = false; // 2. NavMesh가 채워질 시간을 벌기 위한 코루틴 시작 StartCoroutine(WaitAndRepathRoutine()); } } private IEnumerator WaitAndRepathRoutine() { _isRecalculatingPath = true; // NavMesh 바닥이 채워지는 시간을 기다림 (0.15~0.2초 권장) yield return new WaitForSeconds(0.2f); _isRecalculatingPath = false; // 타입에 따라 적절한 상태로 전환 // Monster: 코어로 이동, Hostile: 원래 위치로 복귀 if (aiType == TeamType.Monster) { TransitionToState(EnemyAIState.MoveToCore); } else { TransitionToState(EnemyAIState.ReturnToOrigin); } } private void FindCore() { _core = FindFirstObjectByType(); if (_core != null) { _coreTransform = _core.transform; // 물리적 충돌을 막는 MeshCollider를 우선 찾기 (Trigger 콜라이더 무시) Collider[] allColliders = _core.GetComponentsInChildren(); foreach (Collider col in allColliders) { if (!col.isTrigger) { _coreCollider = col; break; } } // Fallback: 모든 콜라이더가 Trigger이면 첫 번째 콜라이더 사용 if (_coreCollider == null && allColliders.Length > 0) { _coreCollider = allColliders[0]; } } } #endregion #region Public API public EnemyAIState GetCurrentState() { return _currentState.Value; } /// /// 캠프 내 다른 크립으로부터 경고 전파 받음 /// public void ReceiveAlert(GameObject target) { if (!IsServer) return; if (target == null) return; // 이미 해당 타겟을 추적 중이면 무시 if (_currentState.Value == EnemyAIState.ChasePlayer || _currentState.Value == EnemyAIState.Attack) { GameObject currentTarget = GetTargetPlayer(); if (currentTarget == target) return; } // 이미 사망한 타겟은 무시 IDamageable damageable = target.GetComponentInParent(); if (damageable != null && damageable.IsDead()) return; // 타겟 설정 및 추적 시작 SetTargetPlayer(target); if (_currentState.Value == EnemyAIState.Idle || _currentState.Value == EnemyAIState.MoveToCore) { TransitionToState(EnemyAIState.ChasePlayer); } } #endregion #region Gizmos (기존 코드 유지) private void OnDrawGizmos() { if (!showDebugInfo) return; Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, detectionRange); Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); if (detectionAngle < 360f) { Vector3 forward = transform.forward * detectionRange; Vector3 left = Quaternion.Euler(0, -detectionAngle / 2f, 0) * forward; Vector3 right = Quaternion.Euler(0, detectionAngle / 2f, 0) * forward; Gizmos.color = Color.blue; Gizmos.DrawLine(transform.position, transform.position + left); 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 aggroInfo = ""; if (enableAggroSystem && _aggroTable.Count > 0) { float currentAggro = 0f; if (_targetPlayerId.Value != 0 && _aggroTable.TryGetValue(_targetPlayerId.Value, out float agg)) { currentAggro = agg; } aggroInfo = $"\nAggro: {currentAggro:F0} (Targets: {_aggroTable.Count})"; } string fullInfo = $"{gameObject.name}\n{stateInfo}\n{targetInfo}{attackInfo}{distanceInfo}{aggroInfo}"; 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 } }