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(true); // FogOfWar가 없으면 항상 보임
return;
}
// FogOfWar가 비활성화되어 있으면 항상 보임
if (!FogOfWarSystem.Instance.gameObject.activeInHierarchy)
{
if (_initializedVisibility) return;
_initializedVisibility = true;
SetRenderersEnabled(true);
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
}
}
}