UI系统优化
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Save",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Localization",
|
||||
"Unity.TextMeshPro"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
|
||||
@@ -56,10 +56,13 @@ namespace BaseGames.World.Map
|
||||
private List<Image> _pinImages = new();
|
||||
private List<Image> _exitImages = new();
|
||||
private string _highlightedRoomId;
|
||||
private IMapService _mapSvc; // 面板活跃期间缓存,避免高频 ServiceLocator 查询
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_mapSvc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
|
||||
// 首次打开时建立格子;后续打开只刷新探索状态,跳过重复 Instantiate
|
||||
if (_cells.Count == 0)
|
||||
BuildGrid();
|
||||
@@ -75,6 +78,7 @@ namespace BaseGames.World.Map
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
_mapSvc = null;
|
||||
HideTooltip();
|
||||
}
|
||||
|
||||
@@ -95,11 +99,10 @@ namespace BaseGames.World.Map
|
||||
// 面板重新打开时同步关闭期间积累的探索进度
|
||||
private void RefreshAllCells()
|
||||
{
|
||||
var svc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
foreach (var (roomId, cell) in _cells)
|
||||
{
|
||||
if (cell == null) continue;
|
||||
cell.SetVisibility(GetVisibility(svc, roomId));
|
||||
cell.SetVisibility(GetVisibility(_mapSvc, roomId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +111,11 @@ namespace BaseGames.World.Map
|
||||
private void BuildGrid()
|
||||
{
|
||||
if (_database?.AllRooms == null) return;
|
||||
var svc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
foreach (var room in _database.AllRooms)
|
||||
{
|
||||
if (room == null) continue;
|
||||
var cell = Instantiate(_cellPrefab, _roomContainer);
|
||||
cell.Setup(room, GetVisibility(svc, room.RoomId), ChooseIcon(room),
|
||||
cell.Setup(room, GetVisibility(_mapSvc, room.RoomId), ChooseIcon(room),
|
||||
ShowTooltip, HideTooltip);
|
||||
_cells[room.RoomId] = cell;
|
||||
}
|
||||
@@ -149,9 +151,8 @@ namespace BaseGames.World.Map
|
||||
|
||||
private void OnMapUpdated(string roomId)
|
||||
{
|
||||
var svc = ServiceLocator.GetOrDefault<IMapService>();
|
||||
if (_cells.TryGetValue(roomId, out var cell))
|
||||
cell.SetVisibility(GetVisibility(svc, roomId));
|
||||
cell.SetVisibility(GetVisibility(_mapSvc, roomId));
|
||||
}
|
||||
|
||||
// ── 玩家位置图标 ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -4,6 +4,7 @@ using UnityEngine;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.World.Map
|
||||
{
|
||||
@@ -90,7 +91,8 @@ namespace BaseGames.World.Map
|
||||
{
|
||||
if (_regionNames != null)
|
||||
foreach (var e in _regionNames)
|
||||
if (e.RegionId == regionId) return e.DisplayName;
|
||||
if (e.RegionId == regionId)
|
||||
return e.GetDisplayName();
|
||||
return regionId;
|
||||
}
|
||||
}
|
||||
@@ -98,7 +100,26 @@ namespace BaseGames.World.Map
|
||||
[Serializable]
|
||||
public struct RegionNameEntry
|
||||
{
|
||||
[Tooltip("场景/区域 ID,与 EVT_RegionChanged 事件传递的字符串匹配。")]
|
||||
public string RegionId;
|
||||
|
||||
[Tooltip("本地化 Key(如 REGION_CITY_NAME)。设置后运行时通过 LocalizationManager 自动解析。")]
|
||||
public string LocKey;
|
||||
|
||||
[Tooltip("直接显示名(LocKey 为空时使用)。建议优先配置 LocKey,仅在开发或单语言项目中直接善用。")]
|
||||
public string DisplayName;
|
||||
|
||||
/// <summary>返回最终显示名:优先读 LocKey,其次 DisplayName,最后回退到 RegionId。</summary>
|
||||
public string GetDisplayName()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(LocKey))
|
||||
{
|
||||
string localized = LocalizationManager.Get(LocKey, LocalizationTable.UI);
|
||||
// LocalizationManager 在未找到 Key 时返回 Key 本身,判断是否是真正翻译结果
|
||||
if (!string.IsNullOrEmpty(localized) && localized != LocKey)
|
||||
return localized;
|
||||
}
|
||||
return !string.IsNullOrEmpty(DisplayName) ? DisplayName : RegionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
"BaseGames.Core.Save",
|
||||
"BaseGames.Equipment",
|
||||
"BaseGames.Dialogue",
|
||||
"BaseGames.World.Map"
|
||||
"BaseGames.World.Map",
|
||||
"BaseGames.Localization",
|
||||
"BaseGames.UI",
|
||||
"Unity.TextMeshPro"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
@@ -68,6 +68,12 @@ namespace BaseGames.World.Shop
|
||||
public string Description;
|
||||
public Sprite Icon;
|
||||
|
||||
[Header("本地化(可选,优先于直接文本)")]
|
||||
[Tooltip("显示名本地化 Key(如 SHOP_ITEM_POTION_NAME);设置后优先于 DisplayName 字段。")]
|
||||
public string DisplayNameKey;
|
||||
[Tooltip("描述本地化 Key(如 SHOP_ITEM_POTION_DESC);设置后优先于 Description 字段。")]
|
||||
public string DescriptionKey;
|
||||
|
||||
[Header("价格")]
|
||||
public int BasePrice;
|
||||
public bool IsUnique; // 购买一次后永久从库存移除
|
||||
|
||||
240
Assets/_Game/Scripts/World/Shop/ShopPanelUI.cs
Normal file
240
Assets/_Game/Scripts/World/Shop/ShopPanelUI.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.UI;
|
||||
|
||||
namespace BaseGames.World.Shop
|
||||
{
|
||||
/// <summary>
|
||||
/// 商店 UI 面板具体实现(架构 15_MapShopModule §2.3)。
|
||||
/// 挂在 ShopRoot 预制体上,继承 ShopPanel 基类,由 ShopController.Open() 驱动。
|
||||
///
|
||||
/// 布局:
|
||||
/// 标题栏(商店名 + 灵珠余额 + 关闭按钮)
|
||||
/// 商品列表(每项:图标 + 名称 + 描述 + 价格 + 购买按钮)
|
||||
/// </summary>
|
||||
public sealed class ShopPanelUI : ShopPanel, IFocusable
|
||||
{
|
||||
[Header("面板根节点")]
|
||||
[SerializeField] private TMP_Text _titleText;
|
||||
[SerializeField] private TMP_Text _lingZhuText;
|
||||
[SerializeField] private Button _btnClose;
|
||||
|
||||
[Header("商品列表")]
|
||||
[SerializeField] private Transform _itemListRoot;
|
||||
[SerializeField] private ShopItemSlotUI _slotTemplate; // 用作对象池模板,初始设为 inactive
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private IntEventChannelSO _onLingZhuChanged; // EVT_LingZhuChanged
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
private ShopController _controller;
|
||||
private int _currentLingZhu;
|
||||
private readonly List<ShopItemSlotUI> _activeSlots = new();
|
||||
private readonly Queue<ShopItemSlotUI> _slotPool = new(); // O(1) 取用归还
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 生命周期 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 模板槽在运行时始终隐藏,仅作克隆原型
|
||||
if (_slotTemplate != null) _slotTemplate.gameObject.SetActive(false);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onLingZhuChanged?.Subscribe(OnLingZhuChanged).AddTo(_subs);
|
||||
if (_btnClose != null) _btnClose.onClick.AddListener(OnCloseClicked);
|
||||
// 手柄导航:商店面板打开时将焦点置于关闭按钮
|
||||
EventSystem.current?.SetSelectedGameObject(_btnClose?.gameObject);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
if (_btnClose != null) _btnClose.onClick.RemoveListener(OnCloseClicked);
|
||||
}
|
||||
|
||||
// ── ShopPanel API ─────────────────────────────────────────────────────
|
||||
|
||||
public override void Show(List<ShopItemSO> items, ShopController controller)
|
||||
{
|
||||
_controller = controller;
|
||||
gameObject.SetActive(true);
|
||||
BuildSlots(items);
|
||||
RefreshLingZhuText();
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
RecycleSlots();
|
||||
_controller = null;
|
||||
}
|
||||
|
||||
// ── 事件处理 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void OnLingZhuChanged(int newValue)
|
||||
{
|
||||
_currentLingZhu = newValue;
|
||||
RefreshLingZhuText();
|
||||
// 购买后余额变化,同步刷新所有槽的购买按钮可用状态
|
||||
foreach (var slot in _activeSlots)
|
||||
slot.UpdateAffordability(_currentLingZhu);
|
||||
}
|
||||
|
||||
private void OnCloseClicked()
|
||||
{
|
||||
_controller?.Close();
|
||||
}
|
||||
|
||||
// ── 列表构建 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildSlots(List<ShopItemSO> items)
|
||||
{
|
||||
RecycleSlots();
|
||||
if (items == null || _slotTemplate == null) return;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item == null) continue;
|
||||
|
||||
var slot = GetOrCreateSlot();
|
||||
if (slot == null) continue;
|
||||
|
||||
slot.transform.SetParent(_itemListRoot, false);
|
||||
slot.gameObject.SetActive(true);
|
||||
int price = _controller != null
|
||||
? _controller.GetEffectivePrice(item)
|
||||
: item.BasePrice;
|
||||
|
||||
slot.Init(item, price, OnBuyClicked);
|
||||
slot.UpdateAffordability(_currentLingZhu);
|
||||
_activeSlots.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecycleSlots()
|
||||
{
|
||||
foreach (var slot in _activeSlots)
|
||||
{
|
||||
if (slot == null) continue;
|
||||
slot.gameObject.SetActive(false);
|
||||
_slotPool.Enqueue(slot);
|
||||
}
|
||||
_activeSlots.Clear();
|
||||
}
|
||||
|
||||
/// <summary>从对象池取出一个槽实例;池为空时克隆模板。</summary>
|
||||
private ShopItemSlotUI GetOrCreateSlot()
|
||||
{
|
||||
while (_slotPool.Count > 0)
|
||||
{
|
||||
var s = _slotPool.Dequeue();
|
||||
if (s != null) return s;
|
||||
}
|
||||
var go = Instantiate(_slotTemplate.gameObject, _itemListRoot);
|
||||
return go.GetComponent<ShopItemSlotUI>();
|
||||
}
|
||||
|
||||
private void OnBuyClicked(ShopItemSO item)
|
||||
{
|
||||
if (_controller == null) return;
|
||||
bool success = _controller.TryPurchase(item, _currentLingZhu);
|
||||
if (success)
|
||||
{
|
||||
// 购买成功:从列表中移除该槽(商品已消耗或已达上限)
|
||||
var toRemove = _activeSlots.Find(s => s.Item == item);
|
||||
if (toRemove != null)
|
||||
{
|
||||
_activeSlots.Remove(toRemove);
|
||||
toRemove.gameObject.SetActive(false);
|
||||
_slotPool.Enqueue(toRemove);
|
||||
}
|
||||
}
|
||||
// 失败时(资金不足等)UI 保持不动;ShakeFeedback 可在此扩展
|
||||
}
|
||||
|
||||
private void RefreshLingZhuText()
|
||||
{
|
||||
if (_lingZhuText != null)
|
||||
_lingZhuText.text = _currentLingZhu.ToString();
|
||||
}
|
||||
|
||||
// ── IFocusable ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>面板恢复为栈顶时将焦点移回关闭按钮。</summary>
|
||||
public void OnFocusRestored()
|
||||
=> EventSystem.current?.SetSelectedGameObject(_btnClose?.gameObject);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 商品槽 UI:图标 + 名称 + 描述 + 价格标签 + 购买按钮。
|
||||
/// 由 ShopPanelUI.BuildSlots 动态创建。
|
||||
/// </summary>
|
||||
public sealed class ShopItemSlotUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Image _icon;
|
||||
[SerializeField] private TMP_Text _nameText;
|
||||
[SerializeField] private TMP_Text _descText;
|
||||
[SerializeField] private TMP_Text _priceText;
|
||||
[SerializeField] private Button _btnBuy;
|
||||
|
||||
private System.Action<ShopItemSO> _onBuy;
|
||||
|
||||
/// <summary>绑定到此槽的商品数据(ShopPanelUI 用于识别槽)。</summary>
|
||||
public ShopItemSO Item { get; private set; }
|
||||
|
||||
public void Init(ShopItemSO item, int effectivePrice, System.Action<ShopItemSO> onBuy)
|
||||
{
|
||||
Item = item;
|
||||
_onBuy = onBuy;
|
||||
|
||||
if (_icon != null) { _icon.sprite = item.Icon; _icon.enabled = item.Icon != null; }
|
||||
|
||||
if (_nameText != null)
|
||||
{
|
||||
string loc = !string.IsNullOrEmpty(item.DisplayNameKey)
|
||||
? LocalizationManager.Get(item.DisplayNameKey, LocalizationTable.Items)
|
||||
: null;
|
||||
_nameText.text = !string.IsNullOrEmpty(loc) && loc != item.DisplayNameKey
|
||||
? loc : item.DisplayName;
|
||||
}
|
||||
|
||||
if (_descText != null)
|
||||
{
|
||||
string loc = !string.IsNullOrEmpty(item.DescriptionKey)
|
||||
? LocalizationManager.Get(item.DescriptionKey, LocalizationTable.Items)
|
||||
: null;
|
||||
_descText.text = !string.IsNullOrEmpty(loc) && loc != item.DescriptionKey
|
||||
? loc : item.Description;
|
||||
}
|
||||
|
||||
if (_priceText != null) _priceText.text = effectivePrice.ToString();
|
||||
|
||||
if (_btnBuy != null)
|
||||
{
|
||||
_btnBuy.onClick.RemoveAllListeners();
|
||||
_btnBuy.onClick.AddListener(() => _onBuy?.Invoke(Item));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>根据玩家当前灵珠余额更新购买按钮的可交互状态。</summary>
|
||||
public void UpdateAffordability(int lingZhu)
|
||||
{
|
||||
if (_btnBuy == null || Item == null) return;
|
||||
// 从价格文本反推有效价格(避免重新请求 Controller)
|
||||
bool canAfford = int.TryParse(_priceText != null ? _priceText.text : "0", out int p)
|
||||
&& lingZhu >= p;
|
||||
_btnBuy.interactable = canAfford;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/World/Shop/ShopPanelUI.cs.meta
Normal file
11
Assets/_Game/Scripts/World/Shop/ShopPanelUI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b130d817f8485c840b578807df832da1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user