건물 체력 시스템 추가

건물 시야 시스템은 기본 건물데이터에 통합
This commit is contained in:
2026-01-27 13:14:29 +09:00
parent 67773e5864
commit 6c5a52e19b
11 changed files with 845 additions and 376 deletions

View File

@@ -1,9 +1,10 @@
using System;
using Unity.Netcode;
using UnityEngine;
namespace Northbound
{
public class Building : NetworkBehaviour
public class Building : NetworkBehaviour, IDamageable, IVisionProvider
{
[Header("References")]
public BuildingData buildingData;
@@ -12,17 +13,387 @@ namespace Northbound
public Vector3Int gridPosition;
public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°)
[Header("Ownership (for pre-placed buildings)")]
[Tooltip("씬에 미리 배치된 건물의 경우 여기서 소유자 설정 (0 = 중립, 1+ = 플레이어 ID)")]
public ulong initialOwnerId = 0;
[Tooltip("사전 배치 건물인가요? 체크하면 initialOwnerId를 사용합니다")]
public bool useInitialOwner = false;
[Header("Health")]
public bool showHealthBar = true;
public GameObject healthBarPrefab;
[Header("Visual Effects")]
public GameObject destroyEffectPrefab;
public GameObject damageEffectPrefab;
public Transform effectSpawnPoint;
[Header("Debug")]
public bool showGridBounds = true;
public Color gridBoundsColor = Color.cyan;
public void Initialize(BuildingData data, Vector3Int gridPos, int rot)
// 현재 체력
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
// 건물 소유자 (사전 배치 건물 또는 동적 건물 모두 지원)
private NetworkVariable<ulong> _ownerId = new NetworkVariable<ulong>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
// 이벤트
public event Action<int, int> OnHealthChanged; // (current, max)
public event Action OnDestroyed;
private BuildingHealthBar _healthBar;
private float _lastRegenTime;
private bool _isInitialized = false;
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsServer)
{
// 체력 초기화
if (_currentHealth.Value == 0)
{
_currentHealth.Value = buildingData != null ? buildingData.maxHealth : 100;
}
// 소유자 초기화 (사전 배치 건물 체크)
if (useInitialOwner && _ownerId.Value == 0)
{
_ownerId.Value = initialOwnerId;
Debug.Log($"<color=cyan>[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}</color>");
}
else if (!useInitialOwner && _ownerId.Value == 0)
{
// 동적 생성 건물은 NetworkObject의 Owner 사용
_ownerId.Value = OwnerClientId;
}
_lastRegenTime = Time.time;
// FogOfWar 시스템에 시야 제공자로 등록
if (buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
}
}
// 체력 변경 이벤트 구독
_currentHealth.OnValueChanged += OnHealthValueChanged;
// 체력바 생성
if (showHealthBar && healthBarPrefab != null)
{
CreateHealthBar();
}
// 초기 체력 UI 업데이트
UpdateHealthUI();
}
public override void OnNetworkDespawn()
{
_currentHealth.OnValueChanged -= OnHealthValueChanged;
// FogOfWar 시스템에서 제거
if (IsServer && buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
}
}
private void Update()
{
if (!IsServer || buildingData == null)
return;
// 자동 체력 회복
if (buildingData.autoRegenerate && _currentHealth.Value < buildingData.maxHealth)
{
if (Time.time - _lastRegenTime >= 1f)
{
int regenAmount = Mathf.Min(buildingData.regenPerSecond, buildingData.maxHealth - _currentHealth.Value);
_currentHealth.Value += regenAmount;
_lastRegenTime = Time.time;
}
}
}
/// <summary>
/// 건물 초기화 (BuildingManager가 동적 생성 시 호출)
/// </summary>
public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId)
{
buildingData = data;
gridPosition = gridPos;
rotation = rot;
// 이미 스폰된 경우
if (IsServer && IsSpawned)
{
_currentHealth.Value = data.maxHealth;
_ownerId.Value = ownerId;
// 시야 제공자 등록
if (data.providesVision)
{
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
}
}
_isInitialized = true;
}
/// <summary>
/// 건물 소유권 변경 (점령 등)
/// </summary>
public void SetOwner(ulong newOwnerId)
{
if (!IsServer) return;
ulong previousOwner = _ownerId.Value;
_ownerId.Value = newOwnerId;
Debug.Log($"<color=yellow>[Building] {buildingData?.buildingName ?? ""} 소유권 변경: {previousOwner} → {newOwnerId}</color>");
// 시야 제공자 재등록 (소유자가 바뀌었으므로)
if (buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
}
}
#region IVisionProvider Implementation
public ulong GetOwnerId() => _ownerId.Value;
public float GetVisionRange()
{
return buildingData != null ? buildingData.visionRange : 0f;
}
public Transform GetTransform() => transform;
public bool IsActive()
{
// 건물이 스폰되어 있고, 파괴되지 않았으며, 시야 제공 설정이 켜져있어야 함
return IsSpawned && !IsDestroyed() && buildingData != null && buildingData.providesVision;
}
#endregion
#region IDamageable Implementation
public void TakeDamage(int damage, ulong attackerId)
{
if (!IsServer)
{
// 클라이언트는 서버에 요청
TakeDamageServerRpc(damage, attackerId);
return;
}
// 무적 건물
if (buildingData != null && buildingData.isIndestructible)
{
Debug.Log($"<color=yellow>[Building] {buildingData.buildingName}은(는) 무적입니다.</color>");
return;
}
// 이미 파괴됨
if (_currentHealth.Value <= 0)
return;
// 데미지 적용
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
_currentHealth.Value -= actualDamage;
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? ""}이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{buildingData?.maxHealth ?? 100}</color>");
// 데미지 이펙트
ShowDamageEffectClientRpc();
// 체력이 0이 되면 파괴
if (_currentHealth.Value <= 0)
{
DestroyBuilding(attackerId);
}
}
[Rpc(SendTo.Server)]
private void TakeDamageServerRpc(int damage, ulong attackerId)
{
TakeDamage(damage, attackerId);
}
#endregion
#region Health Management
/// <summary>
/// 건물 파괴
/// </summary>
private void DestroyBuilding(ulong attackerId)
{
if (!IsServer)
return;
Debug.Log($"<color=red>[Building] {buildingData?.buildingName ?? ""}이(가) 파괴되었습니다! (공격자: {attackerId})</color>");
// 파괴 이벤트 발생
OnDestroyed?.Invoke();
NotifyDestroyedClientRpc();
// 파괴 이펙트
ShowDestroyEffectClientRpc();
// FogOfWar 시스템에서 제거
if (buildingData != null && buildingData.providesVision)
{
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
}
// BuildingManager에서 제거
if (BuildingManager.Instance != null)
{
BuildingManager.Instance.RemoveBuilding(this);
}
// 네트워크 오브젝트 파괴 (약간의 딜레이)
Invoke(nameof(DespawnBuilding), 0.5f);
}
private void DespawnBuilding()
{
if (IsServer && NetworkObject != null)
{
NetworkObject.Despawn(true);
}
}
/// <summary>
/// 체력 회복
/// </summary>
public void Heal(int amount)
{
if (!IsServer)
return;
if (buildingData == null)
return;
int healAmount = Mathf.Min(amount, buildingData.maxHealth - _currentHealth.Value);
_currentHealth.Value += healAmount;
Debug.Log($"<color=green>[Building] {buildingData.buildingName}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{buildingData.maxHealth}</color>");
}
/// <summary>
/// 현재 체력
/// </summary>
public int GetCurrentHealth() => _currentHealth.Value;
/// <summary>
/// 최대 체력
/// </summary>
public int GetMaxHealth() => buildingData != null ? buildingData.maxHealth : 100;
/// <summary>
/// 체력 비율 (0.0 ~ 1.0)
/// </summary>
public float GetHealthPercentage()
{
int maxHealth = GetMaxHealth();
return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
}
/// <summary>
/// 파괴되었는지 여부
/// </summary>
public bool IsDestroyed() => _currentHealth.Value <= 0;
#endregion
#region Health UI
private void CreateHealthBar()
{
if (_healthBar != null)
return;
GameObject healthBarObj = Instantiate(healthBarPrefab, transform);
_healthBar = healthBarObj.GetComponent<BuildingHealthBar>();
if (_healthBar != null)
{
_healthBar.Initialize(this);
}
}
private void UpdateHealthUI()
{
if (_healthBar != null)
{
_healthBar.UpdateHealth(_currentHealth.Value, GetMaxHealth());
}
OnHealthChanged?.Invoke(_currentHealth.Value, GetMaxHealth());
}
private void OnHealthValueChanged(int previousValue, int newValue)
{
UpdateHealthUI();
}
#endregion
#region Visual Effects
[ClientRpc]
private void ShowDamageEffectClientRpc()
{
if (damageEffectPrefab != null)
{
Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position, spawnPoint.rotation);
Destroy(effect, 2f);
}
}
[ClientRpc]
private void ShowDestroyEffectClientRpc()
{
if (destroyEffectPrefab != null)
{
Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, spawnPoint.rotation);
Destroy(effect, 3f);
}
}
[ClientRpc]
private void NotifyDestroyedClientRpc()
{
if (!IsServer)
{
OnDestroyed?.Invoke();
}
}
#endregion
#region Grid Bounds
/// <summary>
/// Gets the grid-based bounds (from BuildingData width/length/height)
/// This is used for placement validation, NOT the actual collider bounds
@@ -47,6 +418,10 @@ namespace Northbound
return GetGridBounds();
}
#endregion
#region Gizmos
private void OnDrawGizmos()
{
if (!showGridBounds || buildingData == null) return;
@@ -71,6 +446,29 @@ namespace Northbound
Gizmos.color = Color.magenta;
Gizmos.DrawSphere(worldPos, 0.2f);
}
// Draw vision range (if provides vision)
if (buildingData.providesVision)
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, buildingData.visionRange);
}
// Draw owner ID label
#if UNITY_EDITOR
if (Application.isPlaying)
{
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
$"Owner: {_ownerId.Value}");
}
else if (useInitialOwner)
{
UnityEditor.Handles.Label(transform.position + Vector3.up * 3f,
$"Initial Owner: {initialOwnerId}");
}
#endif
}
#endregion
}
}