UI系统优化

This commit is contained in:
2026-05-25 11:54:37 +08:00
parent c7057db27d
commit 3c812cfb41
130 changed files with 4738 additions and 477 deletions

View 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_QuestStartedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestStarted;
[Tooltip("EVT_QuestCompletedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestCompleted;
[Tooltip("EVT_QuestFailedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestFailed;
[Tooltip("EVT_QuestAbandonedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestAbandoned;
[Tooltip("EVT_QuestReadyToCompletepayload = 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;
}
}
}