using System; using UnityEngine; using Unity.Netcode; /// /// 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. /// 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 _networkHealth = new NetworkVariable( 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 /// /// Fired when health changes. Parameters: (currentHealth, maxHealth) /// public event Action OnHealthChanged; /// /// Fired when damage is taken. Parameter: DamageInfo /// public event Action OnDamaged; /// /// Fired when healed. Parameter: healAmount /// public event Action OnHealed; /// /// Fired when health reaches zero. /// public event Action OnDeath; /// /// Fired when health is restored from zero (revived). /// 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 /// /// Simple damage method (backwards compatible). /// public void TakeDamage(float amount) { TakeDamage(new DamageInfo(amount)); } /// /// Enhanced damage method with full context. /// 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 /// /// Heal the entity by a specified amount. /// 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(); } } /// /// Fully restore health to maximum. /// public void HealToFull() { Heal(maxHealth - CurrentHealth); } #endregion #region Health Modification /// /// Set the maximum health value. /// /// New maximum health /// If true, also sets current health to the new max 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); } } } /// /// Directly set the current health (use with caution). /// 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(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 /// /// Get health as a normalized value (0-1). /// public float GetHealthNormalized() { return maxHealth > 0 ? CurrentHealth / maxHealth : 0f; } /// /// Check if health is below a certain percentage. /// public bool IsHealthBelow(float percentage) { return GetHealthNormalized() < percentage; } /// /// Check if at full health. /// public bool IsAtFullHealth() { return CurrentHealth >= maxHealth; } /// /// Kill the entity instantly. /// public void Kill() { TakeDamage(new DamageInfo(CurrentHealth + 1, DamageType.True)); } #endregion }