Реализовать систему здоровья с UI
Условие
Реализуйте универсальную систему здоровья для персонажей с отображением на UI.
Требования
-
Компонент Health:
- Текущее и максимальное здоровье
- Методы TakeDamage(float amount) и Heal(float amount)
- События OnDamaged, OnHealed, OnDied
- Неуязвимость на короткое время после получения урона
-
UI компонент HealthBar:
- Плавное изменение полоски здоровья (Lerp)
- Отображение текущего/максимального значения
- Изменение цвета при низком здоровье
-
Создайте префаб с обоими компонентами
Бонус
- Система брони/защиты
- Floating damage numbers
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Архитектура системы здоровья
Хорошая система здоровья должна быть модульной, расширяемой и событийной. Я реализую отдельные компоненты: 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 — визуальное отображение урона и лечения.
Эта система обеспечивает универсальность, расширяемость и красивую визуализацию.