코드 리팩토링

재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
2026-01-21 01:45:15 +09:00
parent b4ac8f600f
commit db5db4b106
45 changed files with 2775 additions and 248 deletions

View 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
}