← Назад к вопросам

Реализовать систему здоровья с UI

1.0 Junior🔥 241 комментариев
#C# и ООП#UI#Unity Core#Анимация

Условие

Реализуйте универсальную систему здоровья для персонажей с отображением на UI.

Требования

  1. Компонент Health:

    • Текущее и максимальное здоровье
    • Методы TakeDamage(float amount) и Heal(float amount)
    • События OnDamaged, OnHealed, OnDied
    • Неуязвимость на короткое время после получения урона
  2. UI компонент HealthBar:

    • Плавное изменение полоски здоровья (Lerp)
    • Отображение текущего/максимального значения
    • Изменение цвета при низком здоровье
  3. Создайте префаб с обоими компонентами

Бонус

  • Система брони/защиты
  • Floating damage numbers

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Архитектура системы здоровья

Хорошая система здоровья должна быть модульной, расширяемой и событийной. Я реализую отдельные компоненты: Health (логика), HealthBar (UI), и систему брони для защиты. События позволяют другим системам реагировать на урон и смерть.

1. Компонент Health с защитой и неуязвимостью

using UnityEngine;
using System;

/// <summary>Компонент для управления здоровьем персонажа</summary>
public class Health : MonoBehaviour
{
    [Header("Health Settings")]
    [SerializeField] private float maxHealth = 100f;
    [SerializeField] private float currentHealth;
    [SerializeField] private float armor = 0f; // Броня уменьшает получаемый урон
    
    [Header("Invulnerability")]
    [SerializeField] private float invulnerabilityDuration = 0.5f; // Неуязвимость после урона
    [SerializeField] private float damageFlashDuration = 0.1f;
    
    [Header("Audio")]
    [SerializeField] private AudioClip damageSFX;
    [SerializeField] private AudioClip deathSFX;
    [SerializeField] private AudioClip healSFX;
    
    // События
    public event Action<float, float> OnDamaged; // (damageAmount, currentHealth)
    public event Action<float, float> OnHealed;  // (healAmount, currentHealth)
    public event Action OnDied;
    public event Action<float> OnArmorChanged;
    public event Action OnInvulnerabilityStarted;
    public event Action OnInvulnerabilityEnded;
    
    // Состояние
    private bool isDead = false;
    private bool isInvulnerable = false;
    private float invulnerabilityTimer = 0f;
    private float damageFlashTimer = 0f;
    
    // Компоненты
    private Renderer rend;
    private Color originalColor;
    private Color damageColor = Color.red;
    
    void Awake()
    {
        currentHealth = maxHealth;
        
        // Кэшируем рендер для эффекта вспышки
        rend = GetComponent<Renderer>();
        if (rend != null)
        {
            originalColor = rend.material.color;
        }
    }
    
    void Update()
    {
        // Обновляем неуязвимость
        if (isInvulnerable)
        {
            invulnerabilityTimer -= Time.deltaTime;
            if (invulnerabilityTimer <= 0)
            {
                isInvulnerable = false;
                OnInvulnerabilityEnded?.Invoke();
            }
        }
        
        // Обновляем красную вспышку при урона
        if (damageFlashTimer > 0)
        {
            damageFlashTimer -= Time.deltaTime;
            float flashIntensity = damageFlashTimer / damageFlashDuration;
            
            if (rend != null)
            {
                Color flashColor = Color.Lerp(originalColor, damageColor, flashIntensity);
                rend.material.color = flashColor;
            }
        }
        else if (rend != null)
        {
            rend.material.color = originalColor;
        }
    }
    
    /// <summary>Получить урон (с учётом брони)</summary>
    public void TakeDamage(float damageAmount, GameObject damageSource = null)
    {
        // Не получаем урон если уже мертвы
        if (isDead)
            return;
        
        // Не получаем урон в неуязвимости
        if (isInvulnerable)
        {
            Debug.Log($"{gameObject.name} защищён неуязвимостью");
            return;
        }
        
        // Расчитываем урон с учётом брони
        // Броня уменьшает урон на процент: финальный урон = исходный * (1 - броня/100)
        float armorReduction = armor / 100f;
        float finalDamage = damageAmount * (1f - Mathf.Clamp01(armorReduction));
        
        // Применяем урон
        currentHealth -= finalDamage;
        currentHealth = Mathf.Max(0, currentHealth);
        
        // Активируем неуязвимость
        isInvulnerable = true;
        invulnerabilityTimer = invulnerabilityDuration;
        OnInvulnerabilityStarted?.Invoke();
        
        // Визуальный эффект вспышки
        damageFlashTimer = damageFlashDuration;
        
        // Публикуем событие
        OnDamaged?.Invoke(finalDamage, currentHealth);
        
        // Проигрываем звук урона
        if (damageSFX != null)
            AudioManager.Instance?.PlaySFX(damageSFX);
        
        Debug.Log($"{gameObject.name} получил {finalDamage} урона (исходный: {damageAmount}, броня: {armor}). HP: {currentHealth}/{maxHealth}");
        
        // Проверяем смерть
        if (currentHealth <= 0)
        {
            Die(damageSource);
        }
    }
    
    /// <summary>Лечение</summary>
    public void Heal(float healAmount)
    {
        if (isDead)
            return;
        
        float previousHealth = currentHealth;
        currentHealth = Mathf.Min(currentHealth + healAmount, maxHealth);
        float actualHeal = currentHealth - previousHealth;
        
        if (actualHeal > 0)
        {
            OnHealed?.Invoke(actualHeal, currentHealth);
            
            if (healSFX != null)
                AudioManager.Instance?.PlaySFX(healSFX);
            
            Debug.Log($"{gameObject.name} исцелился на {actualHeal}. HP: {currentHealth}/{maxHealth}");
        }
    }
    
    /// <summary>Мгновенно восстановить полное здоровье</summary>
    public void FullHeal()
    {
        Heal(maxHealth);
    }
    
    /// <summary>Установить броню (0-100)</summary>
    public void SetArmor(float armorValue)
    {
        armor = Mathf.Clamp(armorValue, 0, 100);
        OnArmorChanged?.Invoke(armor);
        Debug.Log($"{gameObject.name} броня установлена на {armor}");
    }
    
    /// <summary>Добавить броню</summary>
    public void AddArmor(float armorBonus)
    {
        SetArmor(armor + armorBonus);
    }
    
    /// <summary>Смерть</summary>
    private void Die(GameObject killer = null)
    {
        isDead = true;
        OnDied?.Invoke();
        
        if (deathSFX != null)
            AudioManager.Instance?.PlaySFX(deathSFX);
        
        Debug.Log($"{gameObject.name} умер");
        
        // Отключаем коллайдер чтобы нельзя было взаимодействовать
        Collider collider = GetComponent<Collider>();
        if (collider != null)
            collider.enabled = false;
        
        // Можно добавить анимацию смерти
        // Затем удалить объект или остановить его
    }
    
    /// <summary>Воскресить персонажа</summary>
    public void Resurrect()
    {
        if (!isDead)
            return;
        
        isDead = false;
        currentHealth = maxHealth;
        
        Collider collider = GetComponent<Collider>();
        if (collider != null)
            collider.enabled = true;
        
        Debug.Log($"{gameObject.name} воскрешён");
    }
    
    // Getters
    public float GetCurrentHealth() => currentHealth;
    public float GetMaxHealth() => maxHealth;
    public float GetHealthPercent() => currentHealth / maxHealth;
    public float GetArmor() => armor;
    public bool IsDead() => isDead;
    public bool IsInvulnerable() => isInvulnerable;
}

2. UI компонент HealthBar

using UnityEngine;
using UnityEngine.UI;

/// <summary>Визуальное отображение здоровья на UI</summary>
public class HealthBar : MonoBehaviour
{
    [Header("UI Components")]
    [SerializeField] private Image fillImage;
    [SerializeField] private Image damageDelayImage; // Показывает урон с задержкой
    [SerializeField] private Text healthText;
    [SerializeField] private Image backgroundImage;
    
    [Header("Animation")]
    [SerializeField] private float fillSpeed = 2f; // Скорость заполнения полоски
    [SerializeField] private float damageDelayDuration = 0.5f; // Время показа урона
    
    [Header("Colors")]
    [SerializeField] private Color healthyColor = Color.green;
    [SerializeField] private Color warningColor = new Color(1f, 0.65f, 0f); // Оранжевый
    [SerializeField] private Color criticalColor = Color.red;
    [SerializeField] private float warningThreshold = 0.5f; // 50% здоровья
    [SerializeField] private float criticalThreshold = 0.25f; // 25% здоровья
    
    // Ссылка на компонент здоровья
    private Health healthComponent;
    private float targetFillAmount = 1f;
    private float damageDelayTimer = 0f;
    
    void Awake()
    {
        healthComponent = GetComponentInParent<Health>();
        
        if (healthComponent == null)
        {
            Debug.LogError("HealthBar: Health компонент не найден у родителя");
            enabled = false;
            return;
        }
    }
    
    void Start()
    {
        // Подписываемся на события здоровья
        healthComponent.OnDamaged += HandleDamage;
        healthComponent.OnHealed += HandleHealed;
        healthComponent.OnDied += HandleDied;
        healthComponent.OnInvulnerabilityStarted += OnInvulnerabilityStarted;
        
        // Инициализируем UI
        UpdateHealthBar();
    }
    
    void Update()
    {
        // Плавно интерполируем полоску здоровья
        if (fillImage.fillAmount != targetFillAmount)
        {
            fillImage.fillAmount = Mathf.Lerp(
                fillImage.fillAmount,
                targetFillAmount,
                Time.deltaTime * fillSpeed
            );
        }
        
        // Обновляем текст здоровья
        if (healthText != null)
        {
            int current = Mathf.RoundToInt(healthComponent.GetCurrentHealth());
            int max = Mathf.RoundToInt(healthComponent.GetMaxHealth());
            healthText.text = $"{current}/{max}";
        }
        
        // Обновляем задержку урона
        if (damageDelayTimer > 0)
        {
            damageDelayTimer -= Time.deltaTime;
            if (damageDelayTimer <= 0 && damageDelayImage != null)
            {
                damageDelayImage.fillAmount = targetFillAmount;
            }
        }
    }
    
    void OnDestroy()
    {
        // Отписываемся от событий
        if (healthComponent != null)
        {
            healthComponent.OnDamaged -= HandleDamage;
            healthComponent.OnHealed -= HandleHealed;
            healthComponent.OnDied -= HandleDied;
            healthComponent.OnInvulnerabilityStarted -= OnInvulnerabilityStarted;
        }
    }
    
    private void UpdateHealthBar()
    {
        targetFillAmount = healthComponent.GetHealthPercent();
        UpdateHealthBarColor();
    }
    
    private void UpdateHealthBarColor()
    {
        if (fillImage == null)
            return;
        
        float healthPercent = healthComponent.GetHealthPercent();
        
        // Выбираем цвет в зависимости от уровня здоровья
        if (healthPercent > warningThreshold)
        {
            fillImage.color = healthyColor;
        }
        else if (healthPercent > criticalThreshold)
        {
            // Интерполируем между жёлтым и красным
            float t = (healthPercent - criticalThreshold) / (warningThreshold - criticalThreshold);
            fillImage.color = Color.Lerp(criticalColor, warningColor, t);
        }
        else
        {
            fillImage.color = criticalColor;
        }
    }
    
    private void HandleDamage(float damageAmount, float currentHealth)
    {
        UpdateHealthBar();
        
        // Показываем задержанный урон
        if (damageDelayImage != null)
        {
            damageDelayImage.fillAmount = healthComponent.GetHealthPercent();
            damageDelayTimer = damageDelayDuration;
        }
        
        // Визуальный эффект встряски
        StartCoroutine(ShakeHealthBar());
    }
    
    private void HandleHealed(float healAmount, float currentHealth)
    {
        UpdateHealthBar();
    }
    
    private void HandleDied()
    {
        targetFillAmount = 0;
        if (healthText != null)
        {
            healthText.text = "0/" + Mathf.RoundToInt(healthComponent.GetMaxHealth());
        }
        if (backgroundImage != null)
        {
            backgroundImage.color = new Color(0.3f, 0.3f, 0.3f); // Серый цвет
        }
    }
    
    private void OnInvulnerabilityStarted()
    {
        // Можно добавить визуальный эффект неуязвимости
        if (fillImage != null)
        {
            // Например, мигание
            StartCoroutine(FlashInvulnerability());
        }
    }
    
    private System.Collections.IEnumerator ShakeHealthBar()
    {
        Vector3 originalPosition = transform.localPosition;
        float shakeAmount = 5f;
        float shakeDuration = 0.2f;
        float elapsedTime = 0f;
        
        while (elapsedTime < shakeDuration)
        {
            float randomX = Random.Range(-shakeAmount, shakeAmount);
            transform.localPosition = originalPosition + new Vector3(randomX, 0, 0);
            elapsedTime += Time.deltaTime;
            yield return null;
        }
        
        transform.localPosition = originalPosition;
    }
    
    private System.Collections.IEnumerator FlashInvulnerability()
    {
        Color originalColor = fillImage.color;
        float flashDuration = 0.1f;
        float elapsedTime = 0f;
        
        while (elapsedTime < flashDuration)
        {
            fillImage.color = Color.Lerp(originalColor, Color.white, elapsedTime / flashDuration);
            elapsedTime += Time.deltaTime;
            yield return null;
        }
        
        fillImage.color = originalColor;
    }
}

3. Floating Damage Numbers

using UnityEngine;
using UnityEngine.UI;

public class DamageNumber : MonoBehaviour
{
    [Header("Settings")]
    [SerializeField] private float duration = 1f;
    [SerializeField] private float moveDistance = 2f;
    [SerializeField] private Vector3 moveDirection = Vector3.up;
    [SerializeField] private Text damageText;
    [SerializeField] private Color damageColor = Color.red;
    [SerializeField] private Color healColor = Color.green;
    
    private float elapsedTime = 0f;
    private Vector3 startPosition;
    
    void Start()
    {
        startPosition = transform.position;
    }
    
    void Update()
    {
        elapsedTime += Time.deltaTime;
        
        // Движение вверх
        float progress = elapsedTime / duration;
        transform.position = startPosition + moveDirection * moveDistance * progress;
        
        // Угасание
        Color color = damageText.color;
        color.a = Mathf.Lerp(1f, 0f, progress);
        damageText.color = color;
        
        // Удаление
        if (elapsedTime >= duration)
        {
            Destroy(gameObject);
        }
    }
    
    public void ShowDamage(float amount)
    {
        damageText.text = $"-{amount:F0}";
        damageText.color = damageColor;
    }
    
    public void ShowHealing(float amount)
    {
        damageText.text = $"+{amount:F0}";
        damageText.color = healColor;
    }
}

public class DamageNumberSpawner : MonoBehaviour
{
    [SerializeField] private DamageNumber damageNumberPrefab;
    [SerializeField] private Transform spawnPoint; // Точка спауна (обычно над головой)
    
    private Health healthComponent;
    
    void Awake()
    {
        healthComponent = GetComponent<Health>();
        
        if (healthComponent != null)
        {
            healthComponent.OnDamaged += ShowDamage;
            healthComponent.OnHealed += ShowHealing;
        }
    }
    
    private void ShowDamage(float damageAmount, float currentHealth)
    {
        if (damageNumberPrefab == null)
            return;
        
        Vector3 spawnPos = spawnPoint != null ? spawnPoint.position : transform.position + Vector3.up * 2f;
        DamageNumber damageNum = Instantiate(damageNumberPrefab, spawnPos, Quaternion.identity);
        damageNum.ShowDamage(damageAmount);
    }
    
    private void ShowHealing(float healAmount, float currentHealth)
    {
        if (damageNumberPrefab == null)
            return;
        
        Vector3 spawnPos = spawnPoint != null ? spawnPoint.position : transform.position + Vector3.up * 2f;
        DamageNumber damageNum = Instantiate(damageNumberPrefab, spawnPos, Quaternion.identity);
        damageNum.ShowHealing(healAmount);
    }
}

Пример использования

public class Player : MonoBehaviour
{
    private Health health;
    
    void Awake()
    {
        health = GetComponent<Health>();
    }
    
    void Update()
    {
        // Получить урон
        if (Input.GetKeyDown(KeyCode.D))
        {
            health.TakeDamage(10);
        }
        
        // Лечение
        if (Input.GetKeyDown(KeyCode.H))
        {
            health.Heal(20);
        }
    }
}

Ключевые особенности

1. События — позволяют другим системам реагировать на урон и смерть.

2. Броня — уменьшает получаемый урон на процент.

3. Неуязвимость — защита от урона после получения повреждений.

4. Плавная интерполяция — UI полоска плавно изменяется через Lerp.

5. Задержанный урон — вторая полоска показывает урон с задержкой.

6. Floating numbers — визуальное отображение урона и лечения.

Эта система обеспечивает универсальность, расширяемость и красивую визуализацию.

Реализовать систему здоровья с UI | PrepBro