258 lines
9.8 KiB
C#
258 lines
9.8 KiB
C#
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 <= 0 表示永久无敌(直到被外部取消)。
|
||
/// duration > 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);
|
||
}
|
||
}
|
||
}
|