Files
2026-07-07 03:34:56 +08:00

258 lines
9.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using System;
using System.Collections;
using GameFramework;
using Architecture.Core;
using Architecture.Variables;
namespace IndianOceanAssets.Engine2_5D
{
// Handles health, damage, and death for entities
public class HealthSystem : MonoBehaviour
{
[Header("SO 事件通道(替代 static onPlayerDamaged 事件)")]
[SerializeField] private GameEvent onPlayerDamagedEvent;
[Header("SO 事件通道(玩家死亡,替代直接调用 GameManager.GameOver")]
[SerializeField] private GameEvent onPlayerDiedEvent;
[Header("SO 变量(供 EnemyHealthBar / HUD 订阅,替代反射读取私有字段)")]
[SerializeField] private IntVariable healthVar;
[SerializeField] private IntVariable maxHealthVar;
[Range(1, 100)]
[SerializeField]
private int maxHealth = 5; // Maximum health
private int health = 5; // Current health
[SerializeField]
private GameObject deathEffect; // Effect prefab on death
[SerializeField]
private bool isPlayer; // Is this the player?
[Header("消隐特效")]
[Tooltip("死亡消隐使用的溶解材质(Custom/SpriteDissolve shader")]
[SerializeField] private Material dissolveMaterial;
[Tooltip("消隐动画持续时间(秒)")]
[SerializeField] private float fadeDuration = 2f;
private bool _isDead = false; // 防止重复触发死亡
private bool _isInvincible = false; // 无敌状态
private float _invincibleTimer = 0f; // 无敌剩余时间(<=0 表示永久无敌)
private bool _warnedDamaged = false; // 防止 onPlayerDamagedEvent 空引用告警刷屏
[Header("受伤无敌")]
[Tooltip("玩家受伤后的无敌时间(秒)")]
[SerializeField] private float damageInvincibleDuration = 2f;
/// <summary>是否已死亡(只读)</summary>
public bool IsDead => _isDead;
/// <summary>是否处于无敌状态(只读)</summary>
public bool IsInvincible => _isInvincible;
/// <summary>当前血量(只读)</summary>
public int CurrentHealth => health;
/// <summary>最大血量(只读)</summary>
public int MaxHealth => maxHealth;
/// <summary>血量变化事件(current, max)。敌人血条等本地订阅者用于刷新显示,替代反射读取私有字段。</summary>
public event System.Action<int, int> OnHealthChanged;
// 编辑器实时校验:事件字段未接线时给出黄色警告三角 + 控制台告警
private void OnValidate()
{
if (isPlayer && onPlayerDamagedEvent == null)
Debug.LogWarning($"[HealthSystem] 玩家 HealthSystem 的 On Player Damaged Event 未接线({gameObject.name})。受击泛红不会出现。", this);
if (isPlayer && onPlayerDiedEvent == null)
Debug.LogWarning($"[HealthSystem] 玩家 HealthSystem 的 On Player Died Event 未接线({gameObject.name})。玩家死亡不会触发过场。", this);
}
// Initializes health
private void Start()
{
health = maxHealth;
if (isPlayer)
{
if (maxHealthVar != null) maxHealthVar.Value = maxHealth;
if (healthVar != null) healthVar.Value = health;
}
}
// Applies damage and checks for death
public void Damage(int damageAmount)
{
if (_isDead) return; // 已死亡,不再处理
if (_isInvincible) return; // 无敌状态,免疫伤害
health -= damageAmount;
if (isPlayer && healthVar != null) healthVar.Value = health;
OnHealthChanged?.Invoke(health, maxHealth);
// 玩家受伤时触发事件(用于受击泛红特效)
if (isPlayer)
{
if (onPlayerDamagedEvent != null)
onPlayerDamagedEvent.Raise();
else if (!_warnedDamaged)
{
Debug.LogWarning("[HealthSystem] onPlayerDamagedEvent 未接线!受击泛红特效不会触发。请在玩家 HealthSystem 的 On Player Damaged Event 字段拖入 OnPlayerDamaged 资产。", this);
_warnedDamaged = true;
}
}
// 播放受伤音效
if (isPlayer && AudioManager.Instance != null)
AudioManager.Instance.PlaySFXFromResources("SFX/Hurt", 0.7f);
// 玩家受伤后进入短暂无敌(防止连续掉血)
if (isPlayer && !_isDead)
MakeInvincible(damageInvincibleDuration);
// If dead
if (health <= 0)
{
_isDead = true;
if (isPlayer)
{
// 玩家死亡:通过 OnPlayerDied SO 事件通知 GameManager(解耦,不再直接调用单例)
onPlayerDiedEvent?.Raise();
}
else
{
// 非玩家:正常播放死亡特效并销毁
Die();
}
}
}
/// <summary>
/// 进入无敌状态。
/// duration &lt;= 0 表示永久无敌(直到被外部取消)。
/// duration &gt; 0 表示限时无敌,到期后自动解除。
/// </summary>
public void MakeInvincible(float duration = 0f)
{
_isInvincible = true;
if (duration <= 0f)
{
// 永久无敌
_invincibleTimer = -1f;
StopCoroutine(nameof(InvincibleCountdown));
}
else
{
// 限时无敌:刷新计时器
_invincibleTimer = duration;
StopCoroutine(nameof(InvincibleCountdown));
StartCoroutine(InvincibleCountdown());
}
}
/// <summary>限时无敌倒计时协程</summary>
private IEnumerator InvincibleCountdown()
{
while (_invincibleTimer > 0f)
{
_invincibleTimer -= Time.deltaTime;
yield return null;
}
_isInvincible = false;
}
// Handles death logic and effects
public void Die()
{
Debug.Log($"[HealthSystem] Die() called on {gameObject.name}, dissolveMaterial={(dissolveMaterial != null ? dissolveMaterial.name : "NULL")}");
if (deathEffect != null)
Instantiate(deathEffect, transform.position + new Vector3(0f, .5f, 0f), Quaternion.identity);
// 如果有溶解材质,播放消隐动画后再销毁
if (dissolveMaterial != null)
{
Debug.Log($"[HealthSystem] Starting DissolveAndDestroy coroutine on {gameObject.name}");
StartCoroutine(DissolveAndDestroy());
}
else
{
Debug.Log($"[HealthSystem] No dissolve material, destroying {gameObject.name} immediately");
Destroy(gameObject);
}
}
/// <summary>
/// 溶解消隐协程:切换材质 → 禁用碰撞/AI → 动画溶解 → 销毁。
/// </summary>
private IEnumerator DissolveAndDestroy()
{
Debug.Log($"[HealthSystem] DissolveAndDestroy START on {gameObject.name}");
// 禁用碰撞体和刚体,防止死亡过程中仍能触发碰撞或受重力下坠
foreach (var col in GetComponents<Collider>())
col.enabled = false;
foreach (var col in GetComponents<Collider2D>())
col.enabled = false;
foreach (var rb in GetComponents<Rigidbody>())
rb.isKinematic = true;
foreach (var rb in GetComponents<Rigidbody2D>())
rb.isKinematic = true;
// 禁用所有 MonoBehaviour(AI、动画等),防止死亡后仍移动/攻击
foreach (var mb in GetComponents<MonoBehaviour>())
{
if (mb != this)
mb.enabled = false;
}
// 等一帧,确保禁用生效
yield return null;
// 切换所有 SpriteRenderer 到溶解材质(保留原始纹理和颜色)
var renderers = GetComponentsInChildren<SpriteRenderer>();
Debug.Log($"[HealthSystem] DissolveAndDestroy: {renderers.Length} SpriteRenderers on {gameObject.name}");
for (int i = 0; i < renderers.Length; i++)
{
// 创建溶解材质实例,并复制原始纹理 + 颜色
var dissolveMat = new Material(dissolveMaterial);
if (renderers[i].sprite != null)
dissolveMat.mainTexture = renderers[i].sprite.texture;
dissolveMat.SetColor("_Color", renderers[i].color);
dissolveMat.SetFloat("_DissolveAmount", 0f);
renderers[i].material = dissolveMat;
}
// 再等一帧,确保材质切换生效
yield return null;
Debug.Log($"[HealthSystem] Starting dissolve animation, fadeDuration={fadeDuration}");
// 动画溶解
float elapsed = 0f;
while (elapsed < fadeDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / fadeDuration);
for (int i = 0; i < renderers.Length; i++)
{
if (renderers[i] != null && renderers[i].material != null)
renderers[i].material.SetFloat("_DissolveAmount", t);
}
yield return null;
}
Debug.Log($"[HealthSystem] Dissolve complete, destroying {gameObject.name}");
Destroy(gameObject);
}
}
}