using UnityEngine; using UnityEngine.UI; using TMPro; using IndianOceanAssets.Engine2_5D; using Architecture.Core; using Architecture.Variables; namespace GameFramework { /// /// 游戏内 HUD —— 魂灵计数 + 生命图标 + 技能CD。 /// /// 布局: /// - 左上角:收集到的魂灵(icon + 数字) /// - 左下角:5个生命图标 /// - 右下角:3个技能图标(冲刺/摇铃/灵灯)+ CD遮罩 /// public class GameHUD : MonoBehaviour { /// 魂灵图标的 RectTransform(供魂灵掉落物飞行定位) 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(); _lanternSystem = player.GetComponent(); _echoSystem = player.GetComponent(); _playerController = player.GetComponent(); } 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() == 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(); } /// /// 更新魂灵计数(左上角)。 /// 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 未赋值!"); } } /// /// 更新生命图标(左下角)。 /// 直接读取 PlayerHealth 共享变量,由 OnValueChanged 事件驱动,无反射、无逐帧 Find。 /// 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); } } /// /// 更新技能CD遮罩(右下角)。 /// 通过各系统的公共只读属性读取冷却状态,替代反射读取私有字段。 /// 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); } // 灵灯 CD(SpiritLanternSystem 已暴露公共 Cooldown / CooldownRemaining,无反射) if (_lanternSystem != null) { float remaining = _lanternSystem.CooldownRemaining; float maxCD = _lanternSystem.Cooldown; UpdateSkillCD(skillLanternCD, skillLanternIcon, remaining, maxCD); } } /// /// 更新单个技能CD遮罩。 /// fillAmount = 1 表示CD中(完全遮挡),0 表示就绪(无遮挡)。 /// CD 时图标变灰,就绪时恢复。 /// private void UpdateSkillCD(Image cdImage, float remaining, float total) { UpdateSkillCD(cdImage, null, remaining, total); } /// /// 更新单个技能CD遮罩 + 图标去色。 /// 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(); } } }