코드 리팩토링
재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
384
Assets/Scripts/GameBase/HealthComponent.cs
Normal file
384
Assets/Scripts/GameBase/HealthComponent.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable health component that can be attached to any entity.
|
||||
/// Supports both networked (multiplayer) and local-only usage.
|
||||
/// Implements IDamageable for compatibility with existing damage systems.
|
||||
/// </summary>
|
||||
public class HealthComponent : NetworkBehaviour, IDamageable
|
||||
{
|
||||
[Header("Health Settings")]
|
||||
[SerializeField] private float maxHealth = 100f;
|
||||
[SerializeField] private bool destroyOnDeath = false;
|
||||
[SerializeField] private float destroyDelay = 0f;
|
||||
|
||||
// Network-synced health for multiplayer
|
||||
private NetworkVariable<float> _networkHealth = new NetworkVariable<float>(
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Local health for non-networked entities or single-player
|
||||
private float _localHealth;
|
||||
|
||||
// Track if we're in a networked context
|
||||
private bool _isNetworked;
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Fired when health changes. Parameters: (currentHealth, maxHealth)
|
||||
/// </summary>
|
||||
public event Action<float, float> OnHealthChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when damage is taken. Parameter: DamageInfo
|
||||
/// </summary>
|
||||
public event Action<DamageInfo> OnDamaged;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when healed. Parameter: healAmount
|
||||
/// </summary>
|
||||
public event Action<float> OnHealed;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when health reaches zero.
|
||||
/// </summary>
|
||||
public event Action OnDeath;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when health is restored from zero (revived).
|
||||
/// </summary>
|
||||
public event Action OnRevived;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDamageable Implementation
|
||||
|
||||
public float CurrentHealth => _isNetworked ? _networkHealth.Value : _localHealth;
|
||||
public float MaxHealth => maxHealth;
|
||||
public bool IsAlive => CurrentHealth > 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_localHealth = maxHealth;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_isNetworked = true;
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
_networkHealth.Value = maxHealth;
|
||||
}
|
||||
|
||||
_networkHealth.OnValueChanged += HandleNetworkHealthChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_networkHealth.OnValueChanged -= HandleNetworkHealthChanged;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Damage Methods
|
||||
|
||||
/// <summary>
|
||||
/// Simple damage method (backwards compatible).
|
||||
/// </summary>
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
TakeDamage(new DamageInfo(amount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced damage method with full context.
|
||||
/// </summary>
|
||||
public void TakeDamage(DamageInfo damageInfo)
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
if (damageInfo.Amount <= 0) return;
|
||||
|
||||
if (_isNetworked)
|
||||
{
|
||||
// In networked mode, request damage through server
|
||||
ApplyDamageServerRpc(damageInfo.Amount, (int)damageInfo.Type);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Local mode - apply directly
|
||||
ApplyDamageLocal(damageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void ApplyDamageServerRpc(float amount, int damageType)
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
|
||||
float newHealth = Mathf.Max(0, _networkHealth.Value - amount);
|
||||
_networkHealth.Value = newHealth;
|
||||
|
||||
// Notify all clients about the damage
|
||||
NotifyDamageClientRpc(amount, damageType);
|
||||
|
||||
if (newHealth <= 0)
|
||||
{
|
||||
HandleDeathServer();
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyDamageClientRpc(float amount, int damageType)
|
||||
{
|
||||
OnDamaged?.Invoke(new DamageInfo(amount, (DamageType)damageType));
|
||||
}
|
||||
|
||||
private void ApplyDamageLocal(DamageInfo info)
|
||||
{
|
||||
float previousHealth = _localHealth;
|
||||
_localHealth = Mathf.Max(0, _localHealth - info.Amount);
|
||||
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
OnDamaged?.Invoke(info);
|
||||
|
||||
if (_localHealth <= 0 && previousHealth > 0)
|
||||
{
|
||||
HandleDeathLocal();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Healing Methods
|
||||
|
||||
/// <summary>
|
||||
/// Heal the entity by a specified amount.
|
||||
/// </summary>
|
||||
public void Heal(float amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
|
||||
if (_isNetworked)
|
||||
{
|
||||
HealServerRpc(amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
HealLocal(amount);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void HealServerRpc(float amount)
|
||||
{
|
||||
bool wasAlive = _networkHealth.Value > 0;
|
||||
float newHealth = Mathf.Min(maxHealth, _networkHealth.Value + amount);
|
||||
_networkHealth.Value = newHealth;
|
||||
|
||||
NotifyHealClientRpc(amount, !wasAlive && newHealth > 0);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyHealClientRpc(float amount, bool revived)
|
||||
{
|
||||
OnHealed?.Invoke(amount);
|
||||
if (revived)
|
||||
{
|
||||
OnRevived?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void HealLocal(float amount)
|
||||
{
|
||||
bool wasAlive = _localHealth > 0;
|
||||
_localHealth = Mathf.Min(maxHealth, _localHealth + amount);
|
||||
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
OnHealed?.Invoke(amount);
|
||||
|
||||
if (!wasAlive && _localHealth > 0)
|
||||
{
|
||||
OnRevived?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fully restore health to maximum.
|
||||
/// </summary>
|
||||
public void HealToFull()
|
||||
{
|
||||
Heal(maxHealth - CurrentHealth);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Health Modification
|
||||
|
||||
/// <summary>
|
||||
/// Set the maximum health value.
|
||||
/// </summary>
|
||||
/// <param name="newMax">New maximum health</param>
|
||||
/// <param name="healToMax">If true, also sets current health to the new max</param>
|
||||
public void SetMaxHealth(float newMax, bool healToMax = false)
|
||||
{
|
||||
maxHealth = Mathf.Max(1, newMax);
|
||||
|
||||
if (healToMax)
|
||||
{
|
||||
if (_isNetworked && IsServer)
|
||||
{
|
||||
_networkHealth.Value = maxHealth;
|
||||
}
|
||||
else if (!_isNetworked)
|
||||
{
|
||||
_localHealth = maxHealth;
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clamp current health to new max
|
||||
if (_isNetworked && IsServer)
|
||||
{
|
||||
_networkHealth.Value = Mathf.Min(_networkHealth.Value, maxHealth);
|
||||
}
|
||||
else if (!_isNetworked)
|
||||
{
|
||||
_localHealth = Mathf.Min(_localHealth, maxHealth);
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directly set the current health (use with caution).
|
||||
/// </summary>
|
||||
public void SetHealth(float health)
|
||||
{
|
||||
float newHealth = Mathf.Clamp(health, 0, maxHealth);
|
||||
|
||||
if (_isNetworked && IsServer)
|
||||
{
|
||||
_networkHealth.Value = newHealth;
|
||||
}
|
||||
else if (!_isNetworked)
|
||||
{
|
||||
_localHealth = newHealth;
|
||||
OnHealthChanged?.Invoke(_localHealth, maxHealth);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Death Handling
|
||||
|
||||
private void HandleDeathServer()
|
||||
{
|
||||
NotifyDeathClientRpc();
|
||||
|
||||
if (destroyOnDeath)
|
||||
{
|
||||
if (destroyDelay > 0)
|
||||
{
|
||||
// Schedule despawn
|
||||
Invoke(nameof(DespawnObject), destroyDelay);
|
||||
}
|
||||
else
|
||||
{
|
||||
DespawnObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DespawnObject()
|
||||
{
|
||||
if (TryGetComponent<NetworkObject>(out var netObj))
|
||||
{
|
||||
netObj.Despawn();
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void NotifyDeathClientRpc()
|
||||
{
|
||||
OnDeath?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleDeathLocal()
|
||||
{
|
||||
OnDeath?.Invoke();
|
||||
|
||||
if (destroyOnDeath)
|
||||
{
|
||||
if (destroyDelay > 0)
|
||||
{
|
||||
Destroy(gameObject, destroyDelay);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Health Change Handler
|
||||
|
||||
private void HandleNetworkHealthChanged(float previousValue, float newValue)
|
||||
{
|
||||
OnHealthChanged?.Invoke(newValue, maxHealth);
|
||||
|
||||
// Check for death on clients (server handles it in ApplyDamageServerRpc)
|
||||
if (previousValue > 0 && newValue <= 0 && !IsServer)
|
||||
{
|
||||
// Death event already sent via ClientRpc, just update local state
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Methods
|
||||
|
||||
/// <summary>
|
||||
/// Get health as a normalized value (0-1).
|
||||
/// </summary>
|
||||
public float GetHealthNormalized()
|
||||
{
|
||||
return maxHealth > 0 ? CurrentHealth / maxHealth : 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if health is below a certain percentage.
|
||||
/// </summary>
|
||||
public bool IsHealthBelow(float percentage)
|
||||
{
|
||||
return GetHealthNormalized() < percentage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if at full health.
|
||||
/// </summary>
|
||||
public bool IsAtFullHealth()
|
||||
{
|
||||
return CurrentHealth >= maxHealth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill the entity instantly.
|
||||
/// </summary>
|
||||
public void Kill()
|
||||
{
|
||||
TakeDamage(new DamageInfo(CurrentHealth + 1, DamageType.True));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user