406 lines
12 KiB
C#
406 lines
12 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace Northbound
|
|
{
|
|
/// <summary>
|
|
/// 적대 유닛 (적대세력 또는 몬스터)
|
|
/// </summary>
|
|
[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<int> _currentHealth = new NetworkVariable<int>(
|
|
0,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
|
|
TeamType.Neutral,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private NetworkVariable<bool> _isInvulnerable = new NetworkVariable<bool>(
|
|
false,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
/// <summary>
|
|
/// 사망 시 발생하는 이벤트 (매개변수: killerId)
|
|
/// </summary>
|
|
public event System.Action<ulong> OnDeath;
|
|
|
|
/// <summary>
|
|
/// 데미지를 받았을 때 발생하는 이벤트 (매개변수: attackerId, damage)
|
|
/// </summary>
|
|
public event System.Action<ulong, int> 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<Renderer>();
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 전장의 안개에 따른 가시성 업데이트 (클라이언트 전용)
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 로컬 플레이어의 실제 ID 가져오기
|
|
/// </summary>
|
|
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<NetworkPlayerController>(FindObjectsSortMode.None);
|
|
foreach (var player in allPlayers)
|
|
{
|
|
if (player.IsLocalPlayer)
|
|
{
|
|
localPlayer = player.GetComponent<NetworkObject>();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (localPlayer == null)
|
|
{
|
|
return ulong.MaxValue;
|
|
}
|
|
|
|
var playerController = localPlayer.GetComponent<NetworkPlayerController>();
|
|
if (playerController == null)
|
|
{
|
|
return ulong.MaxValue;
|
|
}
|
|
|
|
return playerController.OwnerPlayerId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 렌더러 활성화/비활성화
|
|
/// </summary>
|
|
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<UnitHealthBar>();
|
|
|
|
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<ITeamMember>();
|
|
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
|
|
}
|
|
}
|
|
} |