UI 系统

This commit is contained in:
2026-06-08 11:26:17 +08:00
parent 1897658a00
commit b582317692
94 changed files with 33540 additions and 3726 deletions

View File

@@ -0,0 +1,35 @@
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Localization;
namespace BaseGames.UI
{
/// <summary>
/// 能力总览单元格视图:图标 + 名称 + 已解锁/未解锁视觉(未解锁时暗化 + 显示锁定遮罩)。
/// 由 <see cref="DataDrivenAbilityPanel"/> 据 AbilityMetaSO 实例化并 Bind。
/// </summary>
[DisallowMultipleComponent]
public class AbilityCellView : MonoBehaviour
{
[SerializeField] private Image _icon;
[SerializeField] private LocalizedText _label;
[Tooltip("未解锁时显示(锁图标 / 暗罩,可空)。")]
[SerializeField] private GameObject _lockedOverlay;
[Tooltip("整格暗化用(可空)。")]
[SerializeField] private CanvasGroup _group;
public void Bind(string labelKey, Sprite icon, bool unlocked)
{
if (_label != null) _label.SetKey(labelKey);
if (_icon != null) { _icon.sprite = icon; _icon.enabled = icon != null; }
SetUnlocked(unlocked);
}
/// <summary>只更新解锁态(保留图标/名称)。设备/解锁事件刷新时调用。</summary>
public void SetUnlocked(bool unlocked)
{
if (_lockedOverlay != null) _lockedOverlay.SetActive(!unlocked);
if (_group != null) _group.alpha = unlocked ? 1f : 0.4f;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 759137176b2cfdd47b53d655d10b170f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
using UnityEngine;
using BaseGames.Player;
namespace BaseGames.UI
{
/// <summary>
/// 能力总览数据表(策划编辑):每个 <see cref="AbilityType"/> → 名称/说明/图标。
/// <see cref="DataDrivenAbilityPanel"/> 据此列出全部能力 + 已解锁/未解锁态。
/// 改本表即增删/重排能力项、改标签/图标,零代码。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/UI/Ability Meta", fileName = "UI_AbilityMeta")]
public class AbilityMetaSO : ScriptableObject
{
[System.Serializable]
public struct Item
{
[Tooltip("此条目对应的能力位。")]
public AbilityType type;
[Tooltip("能力名标签本地化 KeyUI 表,如 ABL_DASH。")]
public string labelKey;
[Tooltip("能力说明本地化 Key可空。")]
public string descKey;
[Tooltip("能力图标(可空)。")]
public Sprite icon;
}
[SerializeField] private Item[] _items;
public Item[] Items => _items;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 381fcefcad0b4544c88188bf7e8d695e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -21,6 +21,7 @@
"BaseGames.Spells",
"BaseGames.Quest",
"BaseGames.Skills",
"BaseGames.Player",
"BaseGames.Inventory",
"Unity.Addressables",
"Unity.ResourceManager",

View File

@@ -0,0 +1,46 @@
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Localization;
namespace BaseGames.UI
{
/// <summary>
/// 图鉴单元格视图(仿 <see cref="AbilityCellView"/>):肖像 + 名称 + 已发现/未发现视觉。
/// 未发现时:名称显示 "???"BESTIARY_LOCKED 文案)、肖像剪影化、暗罩、整格暗化。
/// 由 <see cref="BestiaryPanel"/> 据 <see cref="BestiaryDatabaseSO"/> 实例化并 Bind。
/// </summary>
[DisallowMultipleComponent]
public class BestiaryCellView : MonoBehaviour
{
[SerializeField] private Image _icon;
[SerializeField] private LocalizedText _label;
[Tooltip("未发现时显示的暗罩 / 锁图标(可空)。")]
[SerializeField] private GameObject _lockedOverlay;
[Tooltip("整格暗化用(可空)。")]
[SerializeField] private CanvasGroup _group;
[Tooltip("点击选中此格以在详情区展示(可空)。")]
[SerializeField] private Button _selectButton;
private const string LockedKey = "BESTIARY_LOCKED";
private string _nameKey;
public Button SelectButton => _selectButton;
public void Bind(string nameKey, Sprite icon, bool discovered)
{
_nameKey = nameKey;
if (_icon != null) { _icon.sprite = icon; _icon.enabled = icon != null; }
SetDiscovered(discovered);
}
/// <summary>只更新发现态(保留图标/名称。EVT_BestiaryUpdated 刷新时调用。</summary>
public void SetDiscovered(bool discovered)
{
if (_label != null) _label.SetKey(discovered ? _nameKey : LockedKey);
if (_lockedOverlay != null) _lockedOverlay.SetActive(!discovered);
// 未发现:肖像剪影化(压暗);已发现:原色
if (_icon != null) _icon.color = discovered ? Color.white : new Color(0.04f, 0.04f, 0.06f, 1f);
if (_group != null) _group.alpha = discovered ? 1f : 0.55f;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7734d0120f3b75b41905933b325a823e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
using UnityEngine;
namespace BaseGames.UI
{
/// <summary>
/// 图鉴数据表(策划单表编辑,仿 <see cref="AbilityMetaSO"/>)。每条目对应一个敌人,
/// 以 <c>enemyId</c> 与 <c>EnemyBase._enemyId</c> 关联。自包含显示数据(名/图标/描述),
/// 不引用敌人程序集BaseGames.UI 不依赖 BaseGames.Enemies故显示数据放此处
/// 改本表即增删 / 重排敌人条目、改名/图标,零代码。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/UI/Bestiary Database", fileName = "UI_BestiaryDatabase")]
public class BestiaryDatabaseSO : ScriptableObject
{
public enum Tier { Normal, Elite, Boss }
[System.Serializable]
public struct Entry
{
[Tooltip("敌人唯一 ID须与 EnemyBase._enemyId 一致(如 Enemy_SpiderGuard。解锁靠此 id 匹配。")]
public string enemyId;
[Tooltip("敌人名本地化 KeyUI 表,如 ENM_E001_NAME。")]
public string displayNameKey;
[Tooltip("敌人描述 / 背景本地化 Key可空。")]
public string descKey;
[Tooltip("敌人肖像(可空)。")]
public Sprite icon;
[Tooltip("分级(普通 / 精英 / Boss用于分类与排序。")]
public Tier tier;
[Tooltip("所属区域 ID可空用于筛选。")]
public string region;
[Tooltip("解锁完整词条所需累计击杀数(<=1 即首杀全解锁)。")]
public int fullLoreKillCount;
[Tooltip("展示用最大生命策划手填0 = 不显示)。")]
public int displayMaxHP;
}
[SerializeField] private Entry[] _entries;
public Entry[] Entries => _entries;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d7e33770928593459417c4dab65ced5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,168 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Localization;
namespace BaseGames.UI
{
/// <summary>
/// 图鉴面板(数据驱动,仿 <see cref="DataDrivenAbilityPanel"/>)。据 <see cref="BestiaryDatabaseSO"/> 列出全部敌人,
/// 经 <see cref="IBestiaryService"/>(ServiceLocator) 读已发现态:已发现亮显,未发现 "???"+剪影+锁。
/// 订阅 EVT_BestiaryUpdated 实时刷新;点击格子在右侧详情区展示名/描述/数值/击杀数;显示完成度 X/Y。
/// 菜单态无游戏service=null下全部显示未发现。
/// </summary>
public class BestiaryPanel : MonoBehaviour, IFocusable
{
[Header("数据 / 单元格")]
[SerializeField] private BestiaryDatabaseSO _database;
[Tooltip("敌人格的父节点(通常挂 GridLayoutGroup。")]
[SerializeField] private Transform _container;
[SerializeField] private BestiaryCellView _cellPrefab;
[Header("头部 / 关闭")]
[Tooltip("完成度文本X / Y。")]
[SerializeField] private TMP_Text _completionText;
[SerializeField] private Button _btnClose;
[Header("详情区")]
[SerializeField] private Image _detailIcon;
[SerializeField] private LocalizedText _detailName;
[SerializeField] private TMP_Text _detailDesc;
[SerializeField] private TMP_Text _detailStats;
[Header("Event Channels")]
[Tooltip("图鉴更新广播payload=enemyId用于实时刷新。对应 EVT_BestiaryUpdated。")]
[SerializeField] private StringEventChannelSO _onBestiaryUpdated;
private readonly List<(BestiaryCellView view, BestiaryDatabaseSO.Entry entry)> _cells = new();
private readonly CompositeDisposable _subs = new();
private IBestiaryService _service;
private void Awake() => _btnClose?.onClick.AddListener(OnClose);
private void OnEnable()
{
_onBestiaryUpdated?.Subscribe(_ => RefreshStates()).AddTo(_subs);
_service = ServiceLocator.GetOrDefault<IBestiaryService>(); // 菜单态为 null → 全部未发现
BuildList();
ShowDetail(-1);
}
private void OnDisable() => _subs.Clear();
// ── 列表构建 ──────────────────────────────────────────────────────────
public void BuildList()
{
ClearList();
if (_database == null || _container == null || _cellPrefab == null) return;
var entries = _database.Entries;
if (entries == null) return;
for (int i = 0; i < entries.Length; i++)
{
var entry = entries[i];
var cell = Instantiate(_cellPrefab, _container);
cell.gameObject.SetActive(true);
cell.Bind(entry.displayNameKey, entry.icon, IsDiscovered(entry));
int idx = i;
if (cell.SelectButton != null)
cell.SelectButton.onClick.AddListener(() => ShowDetail(idx));
_cells.Add((cell, entry));
}
UpdateCompletion();
}
private void RefreshStates()
{
if (_service == null) _service = ServiceLocator.GetOrDefault<IBestiaryService>();
foreach (var (view, entry) in _cells)
if (view != null) view.SetDiscovered(IsDiscovered(entry));
UpdateCompletion();
}
private bool IsDiscovered(BestiaryDatabaseSO.Entry entry)
=> _service != null && _service.IsDiscovered(entry.enemyId);
private void UpdateCompletion()
{
if (_completionText == null) return;
int total = _database != null && _database.Entries != null ? _database.Entries.Length : 0;
int found = 0;
foreach (var (_, entry) in _cells) if (IsDiscovered(entry)) found++;
_completionText.text = $"{found} / {total}";
}
// ── 详情 ──────────────────────────────────────────────────────────────
private void ShowDetail(int index)
{
bool valid = index >= 0 && index < _cells.Count;
var entry = valid ? _cells[index].entry : default;
bool discovered = valid && IsDiscovered(entry);
if (_detailIcon != null)
{
_detailIcon.sprite = discovered ? entry.icon : null;
_detailIcon.enabled = discovered && entry.icon != null;
}
if (_detailName != null)
_detailName.SetKey(!valid ? "" : (discovered ? entry.displayNameKey : "BESTIARY_LOCKED"));
if (_detailDesc != null)
{
if (!valid) _detailDesc.text = "";
else if (!discovered) _detailDesc.text = LocalizationManager.Get("BESTIARY_LOCKED_DESC", LocalizationTable.UI);
else
{
string d = LocalizationManager.Get(entry.descKey, LocalizationTable.UI);
_detailDesc.text = string.IsNullOrEmpty(d) || d == entry.descKey ? "" : d;
}
}
if (_detailStats != null)
{
if (!valid || !discovered) { _detailStats.text = ""; }
else
{
int kills = _service != null ? _service.GetKillCount(entry.enemyId) : 0;
string hpLabel = LocalizationManager.Get("BESTIARY_HP", LocalizationTable.UI);
string killLabel = LocalizationManager.Get("BESTIARY_KILLS", LocalizationTable.UI);
var sb = new System.Text.StringBuilder();
if (entry.displayMaxHP > 0) sb.Append(hpLabel).Append(' ').Append(entry.displayMaxHP).Append(" ");
sb.Append(killLabel).Append(' ').Append(kills);
_detailStats.text = sb.ToString();
}
}
}
private void ClearList()
{
_cells.Clear();
if (_container == null) return;
for (int i = _container.childCount - 1; i >= 0; i--)
{
var c = _container.GetChild(i).gameObject;
if (Application.isPlaying) Destroy(c); else DestroyImmediate(c);
}
}
private void OnClose()
{
var uiMgr = ServiceLocator.GetOrDefault<UIManager>();
if (uiMgr != null) uiMgr.CloseTopPanel();
else gameObject.SetActive(false);
}
// ── IFocusable ────────────────────────────────────────────────────────
public void OnFocusRestored()
{
GameObject target = _cells.Count > 0 && _cells[0].view != null && _cells[0].view.SelectButton != null
? _cells[0].view.SelectButton.gameObject
: _btnClose != null ? _btnClose.gameObject : null;
if (target != null) EventSystem.current?.SetSelectedGameObject(target);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0fbb26cf6309a0749b96d6137a9b1b61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Player;
namespace BaseGames.UI
{
/// <summary>
/// 能力解锁总览面板(数据驱动)。据 <see cref="AbilityMetaSO"/> 列出全部能力 + 已解锁/未解锁态。
/// 在场景中找 <see cref="PlayerStats"/> 读 <see cref="PlayerStats.UnlockedAbilities"/>
/// 订阅 <see cref="AbilityTypeEventChannelSO"/>(EVT_AbilityUnlocked) 实时刷新解锁态。
/// 策划改 UI_AbilityMeta 即增删/重排能力、改标签图标;美术改 UI_AbilityCell / 面板预制件改样式。
/// </summary>
public class DataDrivenAbilityPanel : MonoBehaviour
{
[Header("数据 / 单元格")]
[SerializeField] private AbilityMetaSO _meta;
[Tooltip("能力格的父节点(通常挂 GridLayoutGroup。")]
[SerializeField] private Transform _container;
[SerializeField] private AbilityCellView _cellPrefab;
[Header("引用")]
[SerializeField] private Button _btnClose;
[Header("Event Channels")]
[Tooltip("能力解锁广播AbilityType payload用于实时刷新。对应 EVT_AbilityUnlocked。")]
[SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked;
private readonly List<(AbilityCellView view, AbilityType type)> _cells = new();
private readonly CompositeDisposable _subs = new();
private PlayerStats _stats;
private void Awake() => _btnClose?.onClick.AddListener(OnClose);
private void OnEnable()
{
_onAbilityUnlocked?.Subscribe(_ => RefreshStates()).AddTo(_subs);
_stats = FindObjectOfType<PlayerStats>(true); // 游戏内有玩家;菜单态为 null全部显示未解锁
BuildList();
}
private void OnDisable() => _subs.Clear();
/// <summary>据 AbilityMetaSO 重建能力格public 以便编辑器/测试)。</summary>
public void BuildList()
{
ClearList();
if (_meta == null || _container == null || _cellPrefab == null) return;
foreach (var item in _meta.Items)
{
var cell = Instantiate(_cellPrefab, _container);
cell.gameObject.SetActive(true);
cell.Bind(item.labelKey, item.icon, IsUnlocked(item.type));
_cells.Add((cell, item.type));
}
}
private void RefreshStates()
{
if (_stats == null) _stats = FindObjectOfType<PlayerStats>(true);
foreach (var (view, type) in _cells)
if (view != null) view.SetUnlocked(IsUnlocked(type));
}
private bool IsUnlocked(AbilityType type) => _stats != null && _stats.HasAbility(type);
private void ClearList()
{
_cells.Clear();
if (_container == null) return;
for (int i = _container.childCount - 1; i >= 0; i--)
{
var c = _container.GetChild(i).gameObject;
if (Application.isPlaying) Destroy(c); else DestroyImmediate(c);
}
}
private void OnClose()
{
var uiMgr = ServiceLocator.GetOrDefault<UIManager>();
if (uiMgr != null) uiMgr.CloseTopPanel();
else gameObject.SetActive(false);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba7e35964864d7d44b021a24a7213c52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -14,8 +14,9 @@ namespace BaseGames.UI
/// 只读面板:按形态展示各技能槽的技能资料(名称、描述、消耗、冷却),不含任何解锁/技能点交互。
/// 支持左右翻页浏览各形态,并高亮当前实际使用的形态。
///
/// 数据驱动:技能数据读 <see cref="FormSkillDatabaseSO"/>(与 SkillManager 共享的权威源),不再复制配置。
/// Inspector 必填:
/// _formSkillSets每个形态对应一条配置soulSkill + spirit1 + spirit2
/// _database FormSkillDatabaseSO与玩家 SkillManager 同一资产
/// _formTitleText — 显示当前形态标签名的文本
/// _prevFormBtn / _nextFormBtn — 翻页按钮
/// _activeFormIndicator — 高亮"当前形态"的标志(可选,如小圆点)
@@ -26,19 +27,6 @@ namespace BaseGames.UI
{
// ── 子数据结构 ────────────────────────────────────────────────────────
[System.Serializable]
public struct FormSkillConfig
{
[Tooltip("形态标题的本地化 Key格式如 \"Form_SkyWarden_Label\",由 LocalizationManager.Get(…, LocalizationTable.UI) 查找。")]
public string formLabelKey;
[Tooltip("天魂/地魂/命魂的「魂技」")]
public FormSkillSO soulSkill;
[Tooltip("灵技 1Y / Triangle / 手柄按钮)")]
public FormSkillSO spiritSkill1;
[Tooltip("灵技 2Shift+Y / L2+Triangle 等)")]
public FormSkillSO spiritSkill2;
}
[System.Serializable]
public struct SkillSlotView
{
@@ -58,9 +46,8 @@ namespace BaseGames.UI
// ── Inspector 字段 ────────────────────────────────────────────────────
[Header("形态技能配置")]
[Tooltip("每个元素对应一种形态的三项技能,顺序应与 FormType 枚举序号一致。")]
[SerializeField] private FormSkillConfig[] _formSkillSets;
[Header("形态技能数据库(权威源,与玩家 SkillManager 共享同一资产)")]
[SerializeField] private FormSkillDatabaseSO _database;
[Header("UI 导航")]
[SerializeField] private TMP_Text _formTitleText;
@@ -87,6 +74,8 @@ namespace BaseGames.UI
private int _activeIndex = 0; // 玩家当前实际使用的形态
private readonly CompositeDisposable _subs = new();
private FormSkillDatabaseSO.Entry[] Entries => _database != null ? _database.Entries : null;
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
@@ -122,15 +111,17 @@ namespace BaseGames.UI
private void PrevForm()
{
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
_viewIndex = (_viewIndex - 1 + _formSkillSets.Length) % _formSkillSets.Length;
var entries = Entries;
if (entries == null || entries.Length == 0) return;
_viewIndex = (_viewIndex - 1 + entries.Length) % entries.Length;
Refresh();
}
private void NextForm()
{
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
_viewIndex = (_viewIndex + 1) % _formSkillSets.Length;
var entries = Entries;
if (entries == null || entries.Length == 0) return;
_viewIndex = (_viewIndex + 1) % entries.Length;
Refresh();
}
@@ -138,10 +129,11 @@ namespace BaseGames.UI
private void Refresh()
{
if (_formSkillSets == null || _formSkillSets.Length == 0) return;
_viewIndex = Mathf.Clamp(_viewIndex, 0, _formSkillSets.Length - 1);
var entries = Entries;
if (entries == null || entries.Length == 0) return;
_viewIndex = Mathf.Clamp(_viewIndex, 0, entries.Length - 1);
var cfg = _formSkillSets[_viewIndex];
var cfg = entries[_viewIndex];
// 标题走本地化管道fallback 为 key 本身)
if (_formTitleText != null)
@@ -159,7 +151,7 @@ namespace BaseGames.UI
_activeFormIndicator.SetActive(_viewIndex == _activeIndex);
// 翻页按钮可见性(只有一种形态时隐藏)
bool multiForm = _formSkillSets.Length > 1;
bool multiForm = entries.Length > 1;
if (_prevFormBtn != null) _prevFormBtn.gameObject.SetActive(multiForm);
if (_nextFormBtn != null) _nextFormBtn.gameObject.SetActive(multiForm);

View File

@@ -118,6 +118,17 @@ namespace BaseGames.UI.Inventory
/// <summary>切换到指定 Tab。无效索引或与当前相同则忽略。</summary>
public void SelectTab(int index) => SelectTab(index, raise: true, animateEntry: false);
/// <summary>
/// 按内容根名(如 "Content_Map" / "Content_Inventory" / "Content_Quests")选中对应 Tab找不到则忽略。
/// 供"快速直达键"用与数字索引解耦Tab 重排后仍正确。
/// </summary>
public void SelectTabByContentName(string contentName)
{
if (_tabs == null || string.IsNullOrEmpty(contentName)) return;
for (int i = 0; i < _tabs.Length; i++)
if (_tabs[i].content != null && _tabs[i].content.name == contentName) { SelectTab(i); return; }
}
private void SelectTab(int index, bool raise, bool animateEntry)
{
if (_tabs == null || _tabs.Length == 0) return;

View File

@@ -0,0 +1,43 @@
using UnityEngine;
using BaseGames.Input;
namespace BaseGames.UI
{
/// <summary>
/// 挂在"游戏内全屏菜单"根节点(如 InventoryHubRoot
/// 打开OnEnable时冻结游戏Time.timeScale=0并切到 UI 输入关闭OnDisable时恢复时间并切回 Gameplay 输入。
///
/// 解决的 bug游戏内菜单统一背包屏打开时本不改 GameState导致 Gameplay 输入仍激活、
/// 按 Esc 触发的是"暂停"而非"关闭菜单",且关闭后状态错乱使 Esc 不再暂停。
/// 切到 UI 输入后:菜单内 Esc=关闭UINavigator Cancel关闭后 Esc=暂停Gameplay
/// 同时冻结时间符合"打开背包暂停游戏"的常规手感。
///
/// 仅作用于本菜单的开/关与暂停菜单PausedState的真实暂停相互独立、互不冲突。
/// </summary>
[DisallowMultipleComponent]
public class MenuPauseScope : MonoBehaviour
{
[SerializeField] private InputReaderSO _inputReader;
[Tooltip("打开时是否冻结游戏时间Time.timeScale=0。菜单动画须用 unscaledDeltaTime。")]
[SerializeField] private bool _freezeTime = true;
private float _prevTimeScale = 1f;
private void OnEnable()
{
if (_freezeTime)
{
_prevTimeScale = Time.timeScale;
Time.timeScale = 0f;
}
_inputReader?.EnableUIInput();
}
private void OnDisable()
{
if (_freezeTime)
Time.timeScale = _prevTimeScale; // 恢复打开前的时间倍率(若打开前已是 0 则维持暂停,正确)
_inputReader?.EnableGameplayInput();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09d80c6e088f83c4f8a1edadbf5455cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -82,6 +82,9 @@ namespace BaseGames.UI
case PauseMenuAction.OpenSettings:
_uiManager?.OpenPanel(PanelId.Settings);
break;
case PauseMenuAction.OpenPanel:
_uiManager?.OpenPanel(item.targetPanel);
break;
case PauseMenuAction.ReturnToMainMenu:
_uiManager?.CloseTopPanel();
_onSceneLoadRequest?.Raise(new SceneLoadRequest

View File

@@ -9,6 +9,7 @@ namespace BaseGames.UI
{
Resume, // 继续游戏(关闭暂停面板)
OpenSettings, // 打开设置面板
OpenPanel, // 打开任意已注册面板(用 targetPanel 指定,如 FormSkills/Abilities
ReturnToMainMenu, // 返回主菜单(场景加载)
Quit, // 退出游戏
RaiseEvent, // 触发 eventChannel万能扩展
@@ -34,6 +35,9 @@ namespace BaseGames.UI
[Tooltip("点击动作。")]
public PauseMenuAction action;
[Tooltip("OpenPanel 动作要打开的面板 ID如 FormSkills / Abilities / Map。")]
public PanelId targetPanel;
[Tooltip("ReturnToMainMenu 的目标场景 Addressable Key留空用默认主菜单。")]
public string sceneKey;

View File

@@ -21,6 +21,8 @@ namespace BaseGames.UI
CharmPanel,
SpellSelect,
Inventory,
FormSkills, // 形态技能一览FormSkillPanel
Abilities, // 能力解锁总览DataDrivenAbilityPanel
}
[DefaultExecutionOrder(+50)]
@@ -53,6 +55,8 @@ namespace BaseGames.UI
[SerializeField] private VoidEventChannelSO _onSpellSelectOpen;
[Tooltip("打开统一背包菜单InventoryHub。对应 EVT_InventoryOpen。")]
[SerializeField] private VoidEventChannelSO _onInventoryOpen;
[Tooltip("快速直达:打开统一屏并定位到地图 Tab。对应 EVT_QuickOpenMap。")]
[SerializeField] private VoidEventChannelSO _onQuickOpenMap;
// ── 面板栈:委托给统一的 UINavigator不再自管栈─────────────────────
private IUINavigator _navigator;
@@ -119,6 +123,7 @@ namespace BaseGames.UI
_onCharmPanelOpen?.Subscribe(OpenCharmPanel).AddTo(_subs);
_onSpellSelectOpen?.Subscribe(OpenSpellSelect).AddTo(_subs);
_onInventoryOpen?.Subscribe(OpenInventory).AddTo(_subs);
_onQuickOpenMap?.Subscribe(() => OpenInventoryAt("Content_Map")).AddTo(_subs);
// 取消键ESC / 手柄 B由 UINavigator 统一消费UIManager 不再订阅。
}
@@ -278,7 +283,32 @@ namespace BaseGames.UI
private void OpenMap() => OpenPanel(PanelId.Map);
private void OpenCharmPanel() => OpenPanel(PanelId.CharmPanel);
private void OpenSpellSelect() => OpenPanel(PanelId.SpellSelect);
private void OpenInventory() => OpenPanel(PanelId.Inventory);
/// <summary>背包键 toggle若统一背包屏正位于栈顶则关闭否则打开同键开/关)。</summary>
private void OpenInventory()
{
if (_panelRegistry.TryGetValue(PanelId.Inventory, out var inv)
&& Navigator?.Top != null && Navigator.Top.gameObject == inv)
Navigator.Pop();
else
OpenPanel(PanelId.Inventory);
}
/// <summary>
/// 快速直达:确保统一背包屏打开,并定位到指定内容根名对应的 Tab如 "Content_Map")。
/// 已打开则只切 Tab不关闭未打开则打开后切。按名定位与 Tab 顺序解耦。
/// </summary>
private void OpenInventoryAt(string contentName)
{
if (!_panelRegistry.TryGetValue(PanelId.Inventory, out var inv) || inv == null)
{
Debug.LogWarning("[UIManager] 统一背包屏PanelId.Inventory未注册快速直达失败。", this);
return;
}
bool hubTop = Navigator?.Top != null && Navigator.Top.gameObject == inv;
if (!hubTop) OpenPanel(PanelId.Inventory); // 激活时 OnEnable 同步运行
inv.GetComponent<Inventory.InventoryHubPanel>()?.SelectTabByContentName(contentName);
}
// ── 编辑器工具 ────────────────────────────────────────────────────────
[ContextMenu("验证面板注册表")]