UI系统优化
This commit is contained in:
233
Assets/_Game/Scripts/UI/CharmEquipPanel.cs
Normal file
233
Assets/_Game/Scripts/UI/CharmEquipPanel.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using UnityEngine.EventSystems;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 护符装备面板。
|
||||
///
|
||||
/// 布局:
|
||||
/// 顶部 — 标题 + 凹槽容量进度条(已用/总数)
|
||||
/// 左侧 — 已装备护符区域(动态格子列表)
|
||||
/// 右侧 — 已收集护符目录(可滚动,点击即装备/卸下)
|
||||
///
|
||||
/// 数据来源:ServiceLocator 获取 EquipmentManager,读取 Collected / Equipped 列表。
|
||||
/// 反应式更新:订阅 _onEquipmentChanged VoidEventChannelSO,变更时重建两侧列表。
|
||||
///
|
||||
/// Inspector 必填:
|
||||
/// _notchText — "X / Y 格子" 文本
|
||||
/// _equippedContainer — 已装备列表父节点
|
||||
/// _catalogContainer — 收藏目录列表父节点
|
||||
/// _charmCardTemplate — 护符卡片预制(kept inactive)
|
||||
/// _btnClose — 关闭按钮
|
||||
/// _onEquipmentChanged — EquipmentManager 广播的装备变更事件频道
|
||||
/// </summary>
|
||||
public class CharmEquipPanel : MonoBehaviour, IFocusable
|
||||
{
|
||||
// ── Inspector 字段 ────────────────────────────────────────────────────
|
||||
|
||||
[Header("UI 根节点")]
|
||||
[SerializeField] private TMP_Text _notchText;
|
||||
[SerializeField] private Image _notchBarFill;
|
||||
[SerializeField] private Transform _equippedContainer;
|
||||
[SerializeField] private Transform _catalogContainer;
|
||||
[SerializeField] private GameObject _charmCardTemplate; // kept inactive
|
||||
[SerializeField] private Button _btnClose;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private VoidEventChannelSO _onEquipmentChanged;
|
||||
|
||||
// ── 私有状态 ──────────────────────────────────────────────────────────
|
||||
|
||||
private IEquipmentService _manager;
|
||||
private readonly List<GameObject> _equippedCards = new();
|
||||
private readonly List<GameObject> _catalogCards = new();
|
||||
private readonly Queue<GameObject> _cardPool = new(); // O(1) 取用归还
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_manager = ServiceLocator.GetOrDefault<IEquipmentService>();
|
||||
|
||||
if (_btnClose != null)
|
||||
{
|
||||
_btnClose.onClick.RemoveAllListeners();
|
||||
_btnClose.onClick.AddListener(OnCloseBtnClicked);
|
||||
}
|
||||
|
||||
_onEquipmentChanged?.Subscribe(Rebuild).AddTo(_subs);
|
||||
Rebuild();
|
||||
|
||||
// 手柄导航:面板打开时将焦点置于关闭按钮
|
||||
EventSystem.current?.SetSelectedGameObject(_btnClose?.gameObject);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
RecycleCards(_equippedCards);
|
||||
RecycleCards(_catalogCards);
|
||||
}
|
||||
|
||||
// ── 重建 UI ───────────────────────────────────────────────────────────
|
||||
|
||||
private void Rebuild()
|
||||
{
|
||||
if (_manager == null) return;
|
||||
|
||||
RefreshNotchBar();
|
||||
RebuildEquippedList();
|
||||
RebuildCatalogList();
|
||||
}
|
||||
|
||||
private void RefreshNotchBar()
|
||||
{
|
||||
int used = _manager.UsedNotches;
|
||||
int total = _manager.TotalNotches;
|
||||
|
||||
if (_notchText != null)
|
||||
_notchText.text = $"{used} / {total}";
|
||||
|
||||
if (_notchBarFill != null)
|
||||
_notchBarFill.fillAmount = total > 0 ? (float)used / total : 0f;
|
||||
}
|
||||
|
||||
private void RebuildEquippedList()
|
||||
{
|
||||
RecycleCards(_equippedCards);
|
||||
if (_equippedContainer == null) return;
|
||||
|
||||
foreach (var charm in _manager.Equipped)
|
||||
{
|
||||
var card = SpawnCard(_equippedContainer, charm, isEquipped: true);
|
||||
if (card != null) _equippedCards.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildCatalogList()
|
||||
{
|
||||
RecycleCards(_catalogCards);
|
||||
if (_catalogContainer == null) return;
|
||||
|
||||
foreach (var charm in _manager.Collected)
|
||||
{
|
||||
bool isEquipped = _manager.Equipped.Contains(charm);
|
||||
var card = SpawnCard(_catalogContainer, charm, isEquipped);
|
||||
if (card != null) _catalogCards.Add(card);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 卡片生成 ──────────────────────────────────────────────────────────
|
||||
|
||||
private GameObject SpawnCard(Transform parent, CharmSO charm, bool isEquipped)
|
||||
{
|
||||
if (_charmCardTemplate == null || parent == null) return null;
|
||||
|
||||
if (!TryDequeueCard(out GameObject go))
|
||||
go = Instantiate(_charmCardTemplate);
|
||||
|
||||
go.transform.SetParent(parent, worldPositionStays: false);
|
||||
go.SetActive(true);
|
||||
|
||||
// Icon(第一个 Image)
|
||||
var images = go.GetComponentsInChildren<Image>(includeInactive: true);
|
||||
if (images.Length > 0 && charm.icon != null)
|
||||
images[0].sprite = charm.icon;
|
||||
|
||||
// 文本(约定:TMP_Text[0] = 名称, TMP_Text[1] = 凹槽消耗, TMP_Text[2] = 描述)
|
||||
var texts = go.GetComponentsInChildren<TMP_Text>(includeInactive: true);
|
||||
if (texts.Length > 0)
|
||||
{
|
||||
string name = LocalizationManager.Get(charm.displayNameKey, LocalizationTable.Items);
|
||||
texts[0].text = string.IsNullOrEmpty(name) || name == charm.displayNameKey
|
||||
? charm.charmId : name;
|
||||
}
|
||||
if (texts.Length > 1)
|
||||
texts[1].text = charm.notchCost.ToString();
|
||||
|
||||
if (texts.Length > 2)
|
||||
{
|
||||
string desc = LocalizationManager.Get(charm.descriptionKey, LocalizationTable.Items);
|
||||
texts[2].text = string.IsNullOrEmpty(desc) || desc == charm.descriptionKey
|
||||
? string.Empty : desc;
|
||||
}
|
||||
|
||||
// 按钮(约定:第一个 Button = 装备/卸下切换)
|
||||
var btn = go.GetComponentInChildren<Button>(includeInactive: true);
|
||||
if (btn != null)
|
||||
{
|
||||
btn.onClick.RemoveAllListeners();
|
||||
CharmSO captured = charm;
|
||||
bool equip = !isEquipped;
|
||||
|
||||
if (equip)
|
||||
{
|
||||
btn.onClick.AddListener(() =>
|
||||
{
|
||||
string err = _manager?.TryEquipCharm(captured);
|
||||
if (err != null)
|
||||
Debug.LogWarning($"[CharmEquipPanel] 装备失败: {err}");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.onClick.AddListener(() => _manager?.UnequipCharm(captured));
|
||||
}
|
||||
|
||||
// 已装备时使用不同视觉(可选:标记文本)
|
||||
if (texts.Length > 3)
|
||||
texts[3].text = isEquipped ? "✓" : string.Empty;
|
||||
}
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
// ── 回收 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>取队列头第一个非空实例;队列为空时返回 false。</summary>
|
||||
private bool TryDequeueCard(out GameObject go)
|
||||
{
|
||||
while (_cardPool.Count > 0)
|
||||
{
|
||||
go = _cardPool.Dequeue();
|
||||
if (go != null) return true;
|
||||
}
|
||||
go = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RecycleCards(List<GameObject> cards)
|
||||
{
|
||||
foreach (var card in cards)
|
||||
{
|
||||
if (card == null) continue;
|
||||
card.SetActive(false);
|
||||
_cardPool.Enqueue(card);
|
||||
}
|
||||
cards.Clear();
|
||||
}
|
||||
|
||||
private void OnCloseBtnClicked()
|
||||
{
|
||||
var uiMgr = ServiceLocator.GetOrDefault<IUIManager>();
|
||||
if (uiMgr != null) uiMgr.CloseTopPanel();
|
||||
else gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── IFocusable ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>面板恢复为栈顶时将焦点移回关闭按鈕。</summary>
|
||||
public void OnFocusRestored()
|
||||
=> EventSystem.current?.SetSelectedGameObject(_btnClose?.gameObject);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user