using Unity.Netcode; using UnityEngine; namespace Northbound { /// /// 적대 유닛 (적대세력 또는 몬스터) /// [RequireComponent(typeof(Collider))] public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember, IHealthProvider { [Header("Fog of War Visibility")] [Tooltip("전장의 안개에 의한 가시성 제어 활성화")] public bool enableFogVisibility = true; [Tooltip("가시성 체크 주기 (초)")] public float visibilityCheckInterval = 0.1f; [Header("Team Settings")] [Tooltip("이 유닛의 팀 (Hostile = 적대세력, Monster = 몬스터)")] public TeamType enemyTeam = TeamType.Hostile; [Header("Combat")] public int maxHealth = 100; [Header("Visual")] public GameObject damageEffectPrefab; public GameObject destroyEffectPrefab; [Header("Health Bar")] public bool showHealthBar = true; public GameObject healthBarPrefab; private NetworkVariable _currentHealth = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _team = new NetworkVariable( TeamType.Neutral, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _isInvulnerable = new NetworkVariable( false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); /// /// 사망 시 발생하는 이벤트 (매개변수: killerId) /// public event System.Action OnDeath; /// /// 데미지를 받았을 때 발생하는 이벤트 (매개변수: attackerId, damage) /// public event System.Action OnDamageTaken; private UnitHealthBar _healthBar; // 전장의 안개 가시성 private Renderer[] _renderers; private float _visibilityTimer; private bool _lastVisibleState = true; private bool _initializedVisibility = false; private ulong _localPlayerId = ulong.MaxValue; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); // 렌더러 캐시 (가시성 제어용) _renderers = GetComponentsInChildren(); if (IsServer) { _currentHealth.Value = maxHealth; _team.Value = enemyTeam; } // 체력 변경 이벤트 구독 _currentHealth.OnValueChanged += OnHealthChanged; // 체력바 생성 if (showHealthBar && healthBarPrefab != null) { CreateHealthBar(); } // 클라이언트에서는 기본적으로 렌더러 비활성화 if (enableFogVisibility && IsClient) { SetRenderersEnabled(false); } } public override void OnNetworkDespawn() { _currentHealth.OnValueChanged -= OnHealthChanged; base.OnNetworkDespawn(); } private void Update() { // 클라이언트에서만 가시성 체크 (호스트 포함) if (enableFogVisibility && IsClient) { UpdateFogVisibility(); } } /// /// 전장의 안개에 따른 가시성 업데이트 (클라이언트 전용) /// private void UpdateFogVisibility() { _visibilityTimer += Time.deltaTime; if (_visibilityTimer < visibilityCheckInterval) return; _visibilityTimer = 0f; // 로컬 플레이어 ID 캐시 if (_localPlayerId == ulong.MaxValue) { _localPlayerId = GetLocalPlayerId(); if (_localPlayerId == ulong.MaxValue) { // 로컬 플레이어를 찾지 못함 - 기본적으로 숨김 if (_initializedVisibility) return; _initializedVisibility = true; SetRenderersEnabled(false); return; } } // FogOfWarSystem이 없으면 가시성 체크 안함 if (FogOfWarSystem.Instance == null) { if (_initializedVisibility) return; _initializedVisibility = true; SetRenderersEnabled(false); return; } // 현재 위치의 가시성 확인 FogOfWarState state = FogOfWarSystem.Instance.GetVisibilityState(_localPlayerId, transform.position); bool shouldBeVisible = (state == FogOfWarState.Visible); // 초기화되지 않은 경우 강제로 설정 if (!_initializedVisibility) { _initializedVisibility = true; _lastVisibleState = shouldBeVisible; SetRenderersEnabled(shouldBeVisible); return; } // 상태가 변경된 경우에만 렌더러 업데이트 if (shouldBeVisible != _lastVisibleState) { _lastVisibleState = shouldBeVisible; SetRenderersEnabled(shouldBeVisible); } } /// /// 로컬 플레이어의 실제 ID 가져오기 /// private ulong GetLocalPlayerId() { if (NetworkManager.Singleton == null) { return ulong.MaxValue; } // 방법 1: SpawnManager에서 찾기 var localPlayer = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject(); // 방법 2: LocalClient에서 찾기 if (localPlayer == null && NetworkManager.Singleton.LocalClient != null) { localPlayer = NetworkManager.Singleton.LocalClient.PlayerObject; } // 방법 3: 직접 검색 (IsLocalPlayer인 플레이어 찾기) if (localPlayer == null) { var allPlayers = FindObjectsByType(FindObjectsSortMode.None); foreach (var player in allPlayers) { if (player.IsLocalPlayer) { localPlayer = player.GetComponent(); break; } } } if (localPlayer == null) { return ulong.MaxValue; } var playerController = localPlayer.GetComponent(); if (playerController == null) { return ulong.MaxValue; } return playerController.OwnerPlayerId; } /// /// 모든 렌더러 활성화/비활성화 /// private void SetRenderersEnabled(bool enabled) { if (_renderers == null) return; foreach (var renderer in _renderers) { if (renderer != null) { renderer.enabled = enabled; } } // 체력바도 함께 처리 if (_healthBar != null) { _healthBar.gameObject.SetActive(enabled); } } private void OnHealthChanged(int previousValue, int newValue) { if (_healthBar != null) { _healthBar.UpdateHealth(); } } private void CreateHealthBar() { if (_healthBar != null) return; if (healthBarPrefab == null) return; GameObject healthBarObj = Instantiate(healthBarPrefab, transform); _healthBar = healthBarObj.GetComponent(); if (_healthBar != null) { _healthBar.Initialize(this); } } #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) { if (!IsServer) return; if (_currentHealth.Value <= 0) return; // 무적 상태면 데미지 무시 if (_isInvulnerable.Value) return; // 공격자의 팀 확인 if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj)) { var attackerTeamMember = attackerObj.GetComponent(); if (attackerTeamMember != null) { if (!TeamManager.CanAttack(attackerTeamMember, this)) { return; } } } int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; // 데미지 이펙트 ShowDamageEffectClientRpc(); // 데미지 받음 이벤트 발생 (AI 어그로 시스템용) OnDamageTaken?.Invoke(attackerId, actualDamage); // 체력이 0이 되면 파괴 if (_currentHealth.Value <= 0) { DestroyUnit(attackerId); } } private void DestroyUnit(ulong attackerId) { if (!IsServer) return; // 사망 이벤트 발생 (애니메이션 등) OnDeath?.Invoke(attackerId); // 파괴 이펙트 ShowDestroyEffectClientRpc(); // 네트워크 오브젝트 파괴 Invoke(nameof(DespawnUnit), 3.0f); } private void DespawnUnit() { if (IsServer && NetworkObject != null) { NetworkObject.Despawn(true); } } [Rpc(SendTo.ClientsAndHost)] private void ShowDamageEffectClientRpc() { if (damageEffectPrefab != null) { GameObject effect = Instantiate(damageEffectPrefab, transform.position, Quaternion.identity); Destroy(effect, 2f); } } [Rpc(SendTo.ClientsAndHost)] private void ShowDestroyEffectClientRpc() { if (destroyEffectPrefab != null) { GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity); Destroy(effect, 3f); } } #endregion #region ITeamMember Implementation public bool IsDead() => _currentHealth.Value <= 0; public TeamType GetTeam() => _team.Value; public void SetTeam(TeamType team) { if (!IsServer) return; _team.Value = team; } #endregion #region IHealthProvider Implementation public int GetCurrentHealth() => _currentHealth.Value; public int GetMaxHealth() => maxHealth; public float GetHealthPercentage() { int max = GetMaxHealth(); return max > 0 ? (float)_currentHealth.Value / max : 0f; } #endregion #region Invulnerability & Healing public bool IsInvulnerable() => _isInvulnerable.Value; public void SetInvulnerable(bool value) { if (!IsServer) return; _isInvulnerable.Value = value; } public void HealToFull() { if (!IsServer) return; _currentHealth.Value = maxHealth; } #endregion private void OnDrawGizmosSelected() { #if UNITY_EDITOR if (Application.isPlaying) { UnityEditor.Handles.Label(transform.position + Vector3.up * 2f, $"Team: {TeamManager.GetTeamName(_team.Value)}\nHP: {_currentHealth.Value}/{maxHealth}"); } else { UnityEditor.Handles.Label(transform.position + Vector3.up * 2f, $"Team: {TeamManager.GetTeamName(enemyTeam)}"); } #endif } } }