UI系统优化
This commit is contained in:
223
Assets/_Game/Scripts/UI/HUD/QuestTrackerWidget.cs
Normal file
223
Assets/_Game/Scripts/UI/HUD/QuestTrackerWidget.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Quest;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务追踪 HUD 控件(架构 10_UIModule §HUD)。
|
||||
/// 自动追踪最近开始的活跃任务,显示任务名称与各目标的完成进度。
|
||||
/// 订阅 QuestEventChannelRegistry 中的广播频道,实现响应式更新。
|
||||
///
|
||||
/// Inspector 必填:
|
||||
/// _questDatabase — 注册到游戏中的所有 QuestSO 列表(与 QuestManager._allQuests 保持一致)
|
||||
/// _questTitleText — 显示任务标题的 TMP_Text
|
||||
/// _objectiveRowTemplate — 目标行模板(保持非激活状态,用于实例化)
|
||||
/// _objectiveContainer — 目标行父节点
|
||||
/// 事件频道字段 — 从 QuestEventChannelRegistry 对应字段拖入
|
||||
/// </summary>
|
||||
public class QuestTrackerWidget : MonoBehaviour
|
||||
{
|
||||
// ── Inspector 字段 ────────────────────────────────────────────────────
|
||||
|
||||
[Header("UI 根节点")]
|
||||
[SerializeField] private TMP_Text _questTitleText;
|
||||
[SerializeField] private GameObject _objectiveRowTemplate; // kept inactive
|
||||
[SerializeField] private Transform _objectiveContainer;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[Tooltip("EVT_QuestStarted:payload = questId")]
|
||||
[SerializeField] private StringEventChannelSO _onQuestStarted;
|
||||
[Tooltip("EVT_QuestCompleted:payload = questId")]
|
||||
[SerializeField] private StringEventChannelSO _onQuestCompleted;
|
||||
[Tooltip("EVT_QuestFailed:payload = questId")]
|
||||
[SerializeField] private StringEventChannelSO _onQuestFailed;
|
||||
[Tooltip("EVT_QuestAbandoned:payload = questId")]
|
||||
[SerializeField] private StringEventChannelSO _onQuestAbandoned;
|
||||
[Tooltip("EVT_QuestReadyToComplete:payload = questId")]
|
||||
[SerializeField] private StringEventChannelSO _onQuestReadyToComplete;
|
||||
[Tooltip("EVT_QuestObjectiveBatchUpdated:同帧内多目标聚合更新")]
|
||||
[SerializeField] private QuestObjectiveBatchEventChannelSO _onObjectiveBatchUpdated;
|
||||
|
||||
// ── 私有状态 ──────────────────────────────────────────────────────────
|
||||
|
||||
private string _trackedQuestId;
|
||||
private QuestSO _trackedQuest;
|
||||
private readonly List<GameObject> _rowPool = new();
|
||||
private readonly List<(TMP_Text label, TMP_Text progress)> _activeRows = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// IQuestManager 在 OnEnable 时从 ServiceLocator 解析,消除对 _questDatabase 数组的重复注入需求
|
||||
private IQuestManager _questManager;
|
||||
|
||||
// ── 进度字典:objectiveId → (Progress, Required) ─────────────────────
|
||||
|
||||
private readonly Dictionary<string, (int Progress, int Required)> _progressCache =
|
||||
new(System.StringComparer.Ordinal);
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 延迟解析而非缓存到字段:支持 ServiceLocator 中途替换实现(测试/多场景)
|
||||
_questManager = ServiceLocator.GetOrDefault<IQuestManager>();
|
||||
_onQuestStarted?.Subscribe(OnQuestStarted).AddTo(_subs);
|
||||
_onQuestCompleted?.Subscribe(OnQuestEnded).AddTo(_subs);
|
||||
_onQuestFailed?.Subscribe(OnQuestEnded).AddTo(_subs);
|
||||
_onQuestAbandoned?.Subscribe(OnQuestEnded).AddTo(_subs);
|
||||
_onQuestReadyToComplete?.Subscribe(OnQuestReadyToComplete).AddTo(_subs);
|
||||
_onObjectiveBatchUpdated?.Subscribe(OnObjectiveBatchUpdated).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
// ── 事件处理 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnQuestStarted(string questId)
|
||||
{
|
||||
// 自动追踪最新开始的任务
|
||||
_trackedQuestId = questId;
|
||||
// 通过 IQuestManager 接口查找 QuestSO,无需在 HUD 中重复注入数据库
|
||||
_questManager?.TryGetQuest(questId, out _trackedQuest);
|
||||
_progressCache.Clear();
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
private void OnQuestEnded(string questId)
|
||||
{
|
||||
if (questId != _trackedQuestId) return;
|
||||
_trackedQuestId = null;
|
||||
_trackedQuest = null;
|
||||
_progressCache.Clear();
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnQuestReadyToComplete(string questId)
|
||||
{
|
||||
if (questId == _trackedQuestId)
|
||||
{
|
||||
// 可选:更改标题颜色以提示玩家交任务
|
||||
if (_questTitleText != null)
|
||||
_questTitleText.color = Color.yellow;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnObjectiveBatchUpdated(QuestObjectiveBatchEvent batch)
|
||||
{
|
||||
if (batch.QuestId != _trackedQuestId) return;
|
||||
if (batch.Updates == null) return;
|
||||
|
||||
foreach (var update in batch.Updates)
|
||||
_progressCache[update.ObjectiveId] = (update.Progress, update.Required);
|
||||
|
||||
RefreshObjectiveRows();
|
||||
}
|
||||
|
||||
// ── UI 重建 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void Rebuild()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_trackedQuestId))
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
// 标题
|
||||
if (_questTitleText != null)
|
||||
{
|
||||
_questTitleText.color = Color.white;
|
||||
if (_trackedQuest != null && !string.IsNullOrEmpty(_trackedQuest.displayNameKey))
|
||||
{
|
||||
string title = LocalizationManager.Get(_trackedQuest.displayNameKey, LocalizationTable.Quest);
|
||||
_questTitleText.text = string.IsNullOrEmpty(title) || title == _trackedQuest.displayNameKey
|
||||
? _trackedQuestId
|
||||
: title;
|
||||
}
|
||||
else
|
||||
{
|
||||
_questTitleText.text = _trackedQuestId;
|
||||
}
|
||||
}
|
||||
|
||||
// 返还所有行到对象池
|
||||
foreach (var row in _activeRows)
|
||||
if (row.label != null) row.label.transform.parent.gameObject.SetActive(false);
|
||||
_activeRows.Clear();
|
||||
|
||||
// 生成目标行
|
||||
if (_trackedQuest?.objectives == null) return;
|
||||
foreach (var obj in _trackedQuest.objectives)
|
||||
{
|
||||
if (obj == null) continue;
|
||||
|
||||
var rowGo = GetOrCreateRow();
|
||||
var texts = rowGo.GetComponentsInChildren<TMP_Text>(includeInactive: true);
|
||||
TMP_Text labelText = texts.Length > 0 ? texts[0] : null;
|
||||
TMP_Text progressText = texts.Length > 1 ? texts[1] : null;
|
||||
|
||||
if (labelText != null)
|
||||
{
|
||||
string objText = LocalizationManager.Get(obj.displayTextKey, LocalizationTable.Quest);
|
||||
labelText.text = string.IsNullOrEmpty(objText) || objText == obj.displayTextKey
|
||||
? obj.objectiveId
|
||||
: objText;
|
||||
}
|
||||
|
||||
_progressCache.TryGetValue(obj.objectiveId, out var cached);
|
||||
int prog = cached.Progress;
|
||||
int req = cached.Required > 0 ? cached.Required : obj.GetRequiredCount();
|
||||
if (progressText != null)
|
||||
progressText.text = req > 1 ? $"{prog}/{req}" : string.Empty;
|
||||
|
||||
rowGo.SetActive(true);
|
||||
_activeRows.Add((labelText, progressText));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshObjectiveRows()
|
||||
{
|
||||
if (_trackedQuest?.objectives == null) return;
|
||||
for (int i = 0; i < _activeRows.Count && i < _trackedQuest.objectives.Length; i++)
|
||||
{
|
||||
var obj = _trackedQuest.objectives[i];
|
||||
if (obj == null) continue;
|
||||
|
||||
_progressCache.TryGetValue(obj.objectiveId, out var cached);
|
||||
int prog = cached.Progress;
|
||||
int req = cached.Required > 0 ? cached.Required : obj.GetRequiredCount();
|
||||
|
||||
var (_, progressText) = _activeRows[i];
|
||||
if (progressText != null)
|
||||
progressText.text = req > 1 ? $"{prog}/{req}" : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 对象池 ────────────────────────────────────────────────────────────
|
||||
|
||||
private GameObject GetOrCreateRow()
|
||||
{
|
||||
foreach (var r in _rowPool)
|
||||
if (r != null && !r.activeSelf) return r;
|
||||
|
||||
var go = Instantiate(_objectiveRowTemplate, _objectiveContainer);
|
||||
_rowPool.Add(go);
|
||||
return go;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user