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

210 lines
7.9 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 UnityEngine.UI;
using TMPro;
using IndianOceanAssets.Engine2_5D;
using Architecture.Core;
using Architecture.Variables;
namespace GameFramework
{
/// <summary>
/// 游戏内 HUD —— 魂灵计数 + 生命图标 + 技能CD。
///
/// 布局:
/// - 左上角:收集到的魂灵(icon + 数字)
/// - 左下角:5个生命图标
/// - 右下角:3个技能图标(冲刺/摇铃/灵灯)+ CD遮罩
/// </summary>
public class GameHUD : MonoBehaviour
{
/// <summary>魂灵图标的 RectTransform(供魂灵掉落物飞行定位)</summary>
public static RectTransform SoulIconRect { get; private set; }
[Header("魂灵计数(左上角)")]
[SerializeField] private Image soulIcon;
[SerializeField] private TextMeshProUGUI soulCountText;
[Header("生命图标(左下角)")]
[SerializeField] private Image[] lifeIcons; // 5个生命图标
[Header("技能图标(右下角)")]
[SerializeField] private Image skillSprintIcon;
[SerializeField] private Image skillSprintCD; // CD 遮罩(Image.fillAmount
[SerializeField] private Image skillBellIcon;
[SerializeField] private Image skillBellCD;
[SerializeField] private Image skillLanternIcon;
[SerializeField] private Image skillLanternCD;
[Header("暂停")]
[SerializeField] private Button pauseButton;
[Header("SO 事件通道 / 变量")]
[SerializeField] private IntEvent scoreChangedEvent;
[SerializeField] private IntVariable playerHealthVar;
private HealthSystem _playerHealth;
private SpiritLanternSystem _lanternSystem;
private EchoSystem _echoSystem;
private PlayerController _playerController;
private int _lastSoulCount = -1;
void Start()
{
var player = GameObject.FindWithTag("Player");
if (player != null)
{
_playerHealth = player.GetComponent<HealthSystem>();
_lanternSystem = player.GetComponent<SpiritLanternSystem>();
_echoSystem = player.GetComponent<EchoSystem>();
_playerController = player.GetComponent<PlayerController>();
}
if (pauseButton != null)
pauseButton.onClick.AddListener(OnPauseClick);
// 暴露魂灵图标 RectTransform(供魂灵掉落物飞行定位)
if (soulIcon != null)
SoulIconRect = soulIcon.rectTransform;
// 订阅分数事件(魂灵计数复用分数系统):无条件注册,事件资产始终存在;
// 初始值若 ScoreManager 已就绪则取实时分,否则先显示 0,首个 ScoreChanged 会刷新。
scoreChangedEvent?.Register(UpdateSoulCount);
UpdateSoulCount(ScoreManager.Instance != null ? ScoreManager.Instance.CurrentScore : 0);
// 订阅玩家血量变量(替代反射读取私有字段 + Update 条件 Find 回退)
if (playerHealthVar != null)
{
playerHealthVar.OnValueChanged += UpdateLifeIcons;
UpdateLifeIcons(playerHealthVar.Value);
}
// 受击泛红特效:场景需预置已接线的 DamageFlashOverlay 实例(不再自动创建,
// 否则会生成一个事件为 null 的实例,既无效果又掩盖「未接线」问题)
if (FindObjectOfType<DamageFlashOverlay>() == null)
Debug.LogWarning("[GameHUD] 场景中未找到 DamageFlashOverlay,受击泛红特效不会显示。请在场景中放置一个、并把它 On Player Damaged Event 字段接上 OnPlayerDamaged 资产。", this);
}
void OnDestroy()
{
scoreChangedEvent?.Unregister(UpdateSoulCount);
if (playerHealthVar != null)
playerHealthVar.OnValueChanged -= UpdateLifeIcons;
}
void Update()
{
// 事件已在 Start 中无条件订阅,无需逐帧延迟订阅(避免反模式)。
UpdateSkillCooldowns();
}
/// <summary>
/// 更新魂灵计数(左上角)。
/// </summary>
private void UpdateSoulCount(int count)
{
if (soulCountText != null)
{
soulCountText.text = count.ToString();
if (count != _lastSoulCount)
{
Debug.Log($"[GameHUD] UpdateSoulCount: {count}");
_lastSoulCount = count;
}
}
else
{
Debug.LogWarning("[GameHUD] soulCountText 未赋值!");
}
}
/// <summary>
/// 更新生命图标(左下角)。
/// 直接读取 PlayerHealth 共享变量,由 OnValueChanged 事件驱动,无反射、无逐帧 Find。
/// </summary>
private void UpdateLifeIcons(int value)
{
if (lifeIcons == null || lifeIcons.Length == 0) return;
if (playerHealthVar == null) return;
int currentHealth = value;
for (int i = 0; i < lifeIcons.Length; i++)
{
if (lifeIcons[i] != null)
lifeIcons[i].enabled = (i < currentHealth);
}
}
/// <summary>
/// 更新技能CD遮罩(右下角)。
/// 通过各系统的公共只读属性读取冷却状态,替代反射读取私有字段。
/// </summary>
private void UpdateSkillCooldowns()
{
// 冲刺 CD(从 PlayerController.RollCooldown 读取)
if (_playerController != null)
{
var cd = _playerController.RollCooldown;
UpdateSkillCD(skillSprintCD, skillSprintIcon, cd.remaining, cd.total);
}
// 摇铃 CD(从 EchoSystem.BellCooldown 读取)
if (_echoSystem != null)
{
var cd = _echoSystem.BellCooldown;
UpdateSkillCD(skillBellCD, skillBellIcon, cd.remaining, cd.total);
}
// 灵灯 CDSpiritLanternSystem 已暴露公共 Cooldown / CooldownRemaining,无反射)
if (_lanternSystem != null)
{
float remaining = _lanternSystem.CooldownRemaining;
float maxCD = _lanternSystem.Cooldown;
UpdateSkillCD(skillLanternCD, skillLanternIcon, remaining, maxCD);
}
}
/// <summary>
/// 更新单个技能CD遮罩。
/// fillAmount = 1 表示CD中(完全遮挡),0 表示就绪(无遮挡)。
/// CD 时图标变灰,就绪时恢复。
/// </summary>
private void UpdateSkillCD(Image cdImage, float remaining, float total)
{
UpdateSkillCD(cdImage, null, remaining, total);
}
/// <summary>
/// 更新单个技能CD遮罩 + 图标去色。
/// </summary>
private void UpdateSkillCD(Image cdImage, Image iconImage, float remaining, float total)
{
if (cdImage == null) return;
bool onCooldown = total > 0f && remaining > 0f;
if (onCooldown)
{
cdImage.gameObject.SetActive(true);
cdImage.fillAmount = remaining / total;
// CD 中图标变灰
if (iconImage != null)
iconImage.color = new Color(0.4f, 0.4f, 0.4f, 1f);
}
else
{
cdImage.fillAmount = 0f;
cdImage.gameObject.SetActive(false);
// CD 就绪图标恢复原色
if (iconImage != null)
iconImage.color = Color.white;
}
}
private void OnPauseClick()
{
if (TimeController.Instance != null)
TimeController.Instance.Pause();
}
}
}