210 lines
7.9 KiB
C#
210 lines
7.9 KiB
C#
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);
|
||
}
|
||
|
||
// 灵灯 CD(SpiritLanternSystem 已暴露公共 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();
|
||
}
|
||
}
|
||
}
|