UI系统组件
This commit is contained in:
214
Assets/_Game/Scripts/UI/Settings/DataDrivenSettingsPanel.cs
Normal file
214
Assets/_Game/Scripts/UI/Settings/DataDrivenSettingsPanel.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.UI.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据驱动设置面板:据 <see cref="SettingsSchemaSO"/> 生成控件行并绑定 <see cref="ISettingsService"/>。
|
||||
///
|
||||
/// 取代硬编码的 <see cref="BaseGames.UI.SettingsPanelController"/>:策划改表即可增删 / 重排 / 改标签 / 分节,
|
||||
/// 无需改代码。每行据 <see cref="SettingKey"/> 自动选用 Slider / Toggle / Dropdown 行预制件,
|
||||
/// 并复用通用控件 <see cref="UISlider"/> / <see cref="UIDropdown"/>。
|
||||
///
|
||||
/// 控件来源:行预制件由 <c>SettingsPanelScaffold</c> 生成(标签为 <see cref="LocalizedText"/>)。
|
||||
/// </summary>
|
||||
public class DataDrivenSettingsPanel : UIPanelBase
|
||||
{
|
||||
public enum ControlKind { Slider, Toggle, Dropdown }
|
||||
|
||||
[Header("数据表")]
|
||||
[SerializeField] private SettingsSchemaSO _schema;
|
||||
|
||||
[Header("行布局")]
|
||||
[Tooltip("生成的行的父节点(通常挂 VerticalLayoutGroup)。")]
|
||||
[SerializeField] private Transform _container;
|
||||
[SerializeField] private GameObject _headerPrefab;
|
||||
[SerializeField] private GameObject _sliderRowPrefab;
|
||||
[SerializeField] private GameObject _toggleRowPrefab;
|
||||
[SerializeField] private GameObject _dropdownRowPrefab;
|
||||
|
||||
// FPS 下拉的取值集合(与 SettingsPanelController 一致)
|
||||
private static readonly int[] FpsOptions = { 30, 60, 120, -1 };
|
||||
private static readonly Language[] LanguageOptions =
|
||||
{ Language.ChineseSimplified, Language.English, Language.Japanese, Language.Korean };
|
||||
private static readonly string[] LanguageNativeNames =
|
||||
{ "简体中文", "English", "日本語", "한국어" };
|
||||
|
||||
private ISettingsService _settings;
|
||||
private ILocalizationService _loc;
|
||||
private readonly List<GameObject> _spawned = new();
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
protected override void OnPanelOpen()
|
||||
{
|
||||
_settings = GetService<ISettingsService>();
|
||||
_loc = GetService<ILocalizationService>();
|
||||
Build();
|
||||
}
|
||||
|
||||
protected override void OnPanelClose() => Clear();
|
||||
|
||||
// ── 构建 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>据 schema 重建所有行。可在无 ISettingsService 时调用(仅生成行 + 标签,不绑定)。</summary>
|
||||
public void Build()
|
||||
{
|
||||
Clear();
|
||||
if (_schema == null || _container == null) return;
|
||||
|
||||
foreach (var item in _schema.Items)
|
||||
{
|
||||
GameObject prefab = item.isHeader ? _headerPrefab : PrefabFor(KindOf(item.key));
|
||||
if (prefab == null) continue;
|
||||
|
||||
var go = Instantiate(prefab, _container);
|
||||
_spawned.Add(go);
|
||||
|
||||
SetLabel(go, item.labelKey);
|
||||
if (!item.isHeader && _settings != null)
|
||||
BindControl(go, item.key);
|
||||
}
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
foreach (var go in _spawned) if (go != null) Destroy(go);
|
||||
_spawned.Clear();
|
||||
}
|
||||
|
||||
private GameObject PrefabFor(ControlKind kind) => kind switch
|
||||
{
|
||||
ControlKind.Slider => _sliderRowPrefab,
|
||||
ControlKind.Toggle => _toggleRowPrefab,
|
||||
ControlKind.Dropdown => _dropdownRowPrefab,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static void SetLabel(GameObject row, string labelKey)
|
||||
{
|
||||
var loc = row.GetComponentInChildren<LocalizedText>(true);
|
||||
if (loc != null) loc.SetKey(labelKey);
|
||||
}
|
||||
|
||||
// ── 控件类型映射 ──────────────────────────────────────────────────────
|
||||
public static ControlKind KindOf(SettingKey key) => key switch
|
||||
{
|
||||
SettingKey.MasterVolume or SettingKey.BGMVolume or SettingKey.SFXVolume
|
||||
or SettingKey.AmbientVolume or SettingKey.UIScale => ControlKind.Slider,
|
||||
SettingKey.VSync or SettingKey.ScreenShake => ControlKind.Toggle,
|
||||
SettingKey.TargetFPS or SettingKey.ColorblindMode or SettingKey.Language => ControlKind.Dropdown,
|
||||
_ => ControlKind.Slider,
|
||||
};
|
||||
|
||||
// ── 绑定派发 ──────────────────────────────────────────────────────────
|
||||
private void BindControl(GameObject row, SettingKey key)
|
||||
{
|
||||
switch (KindOf(key))
|
||||
{
|
||||
case ControlKind.Slider: BindSlider(row, key); break;
|
||||
case ControlKind.Toggle: BindToggle(row, key); break;
|
||||
case ControlKind.Dropdown: BindDropdown(row, key); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void BindSlider(GameObject row, SettingKey key)
|
||||
{
|
||||
var ui = row.GetComponentInChildren<UISlider>(true);
|
||||
if (ui == null) return;
|
||||
var data = _settings.Current;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case SettingKey.MasterVolume:
|
||||
ui.Bind(0f, 1f, data.MasterVolume, v => _settings.SetMasterVolume(v), Percent); break;
|
||||
case SettingKey.BGMVolume:
|
||||
ui.Bind(0f, 1f, data.BGMVolume, v => _settings.SetBGMVolume(v), Percent); break;
|
||||
case SettingKey.SFXVolume:
|
||||
ui.Bind(0f, 1f, data.SFXVolume, v => _settings.SetSFXVolume(v), Percent); break;
|
||||
case SettingKey.AmbientVolume:
|
||||
ui.Bind(0f, 1f, data.AmbientVolume, v => _settings.SetAmbientVolume(v), Percent); break;
|
||||
case SettingKey.UIScale:
|
||||
ui.Bind(0.8f, 1.5f, data.UIScale, v => _settings.SetUIScale(v), Percent); break;
|
||||
}
|
||||
}
|
||||
|
||||
private void BindToggle(GameObject row, SettingKey key)
|
||||
{
|
||||
var toggle = row.GetComponentInChildren<Toggle>(true);
|
||||
if (toggle == null) return;
|
||||
var data = _settings.Current;
|
||||
toggle.onValueChanged.RemoveAllListeners();
|
||||
switch (key)
|
||||
{
|
||||
case SettingKey.VSync:
|
||||
toggle.isOn = data.VSync;
|
||||
toggle.onValueChanged.AddListener(v => _settings.SetVSync(v));
|
||||
break;
|
||||
case SettingKey.ScreenShake:
|
||||
toggle.isOn = data.ScreenShakeEnabled;
|
||||
toggle.onValueChanged.AddListener(v => _settings.SetScreenShakeEnabled(v));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void BindDropdown(GameObject row, SettingKey key)
|
||||
{
|
||||
var ui = row.GetComponentInChildren<UIDropdown>(true);
|
||||
if (ui == null) return;
|
||||
var data = _settings.Current;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case SettingKey.TargetFPS:
|
||||
{
|
||||
var opts = new List<string> { "30", "60", "120", Loc("SET_FPS_UNLIMITED", "无限") };
|
||||
int idx = Array.IndexOf(FpsOptions, data.TargetFPS); if (idx < 0) idx = 1;
|
||||
ui.Bind(opts, idx, i => _settings.SetTargetFrameRate(FpsOptions[Mathf.Clamp(i, 0, FpsOptions.Length - 1)]));
|
||||
break;
|
||||
}
|
||||
case SettingKey.ColorblindMode:
|
||||
{
|
||||
int count = Enum.GetValues(typeof(ColorblindMode)).Length;
|
||||
var opts = new List<string>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
opts.Add(Loc($"SET_COLORBLIND_{i}", ((ColorblindMode)i).ToString()));
|
||||
ui.Bind(opts, (int)data.ColorblindMode,
|
||||
i => _settings.SetColorblindMode((ColorblindMode)Mathf.Clamp(i, 0, count - 1)));
|
||||
break;
|
||||
}
|
||||
case SettingKey.Language:
|
||||
{
|
||||
var opts = new List<string>(LanguageNativeNames);
|
||||
int idx = _loc != null ? Array.IndexOf(LanguageOptions, _loc.CurrentLanguage) : 0;
|
||||
if (idx < 0) idx = 0;
|
||||
ui.Bind(opts, idx, i =>
|
||||
GetService<ILocalizationService>()?.SetLanguage(LanguageOptions[Mathf.Clamp(i, 0, LanguageOptions.Length - 1)]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 工具 ──────────────────────────────────────────────────────────────
|
||||
private static string Percent(float v) => Mathf.RoundToInt(v * 100f) + "%";
|
||||
|
||||
private string Loc(string key, string fallback)
|
||||
{
|
||||
if (_loc != null && _loc.TryGet(key, out var v, LocalizationTable.UI)) return v;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
protected override GameObject ResolveFirstSelected()
|
||||
{
|
||||
foreach (var go in _spawned)
|
||||
{
|
||||
if (go == null) continue;
|
||||
var sel = go.GetComponentInChildren<Selectable>(true);
|
||||
if (sel != null) return sel.gameObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7cf26a2f7cc5bb49b573bbe9f5614b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/_Game/Scripts/UI/Settings/SettingKey.cs
Normal file
24
Assets/_Game/Scripts/UI/Settings/SettingKey.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace BaseGames.UI.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// 可在设置面板中数据驱动绑定的设置项标识。
|
||||
/// 与 <see cref="BaseGames.Core.ISettingsService"/> 的 typed get/set 一一对应;
|
||||
/// 控件类型与绑定逻辑由 <see cref="DataDrivenSettingsPanel"/> 的派发器据此决定。
|
||||
///
|
||||
/// 新增"全新"设置项需:在此加枚举 + ISettingsService 加方法 + 派发器加分支(代码);
|
||||
/// 但增删 / 重排 / 改标签 / 分节既有设置项是纯数据(改 <see cref="SettingsSchemaSO"/>)。
|
||||
/// </summary>
|
||||
public enum SettingKey
|
||||
{
|
||||
MasterVolume,
|
||||
BGMVolume,
|
||||
SFXVolume,
|
||||
AmbientVolume,
|
||||
VSync,
|
||||
TargetFPS,
|
||||
UIScale,
|
||||
ColorblindMode,
|
||||
ScreenShake,
|
||||
Language,
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Settings/SettingKey.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Settings/SettingKey.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5daec1dcdf7f7884ca90b234d3ac0f75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Assets/_Game/Scripts/UI/Settings/SettingsSchemaSO.cs
Normal file
31
Assets/_Game/Scripts/UI/Settings/SettingsSchemaSO.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.UI.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置面板的数据驱动表(策划编辑)。
|
||||
/// 按顺序列出设置项与分节标题;<see cref="DataDrivenSettingsPanel"/> 据此生成对应控件行。
|
||||
/// 策划可在 Inspector 增删、重排、改标签、加分节,无需改代码或重编译。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/UI/Settings Schema", fileName = "UI_SettingsSchema")]
|
||||
public class SettingsSchemaSO : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public struct Item
|
||||
{
|
||||
[Tooltip("勾选则为分节标题行(仅显示 labelKey,不绑定控件)。")]
|
||||
public bool isHeader;
|
||||
|
||||
[Tooltip("标签本地化 Key(UI 表)。")]
|
||||
public string labelKey;
|
||||
|
||||
[Tooltip("绑定的设置项(isHeader 为 true 时忽略)。")]
|
||||
public SettingKey key;
|
||||
}
|
||||
|
||||
[SerializeField] private Item[] _items;
|
||||
|
||||
public Item[] Items => _items;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/UI/Settings/SettingsSchemaSO.cs.meta
Normal file
11
Assets/_Game/Scripts/UI/Settings/SettingsSchemaSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0edaa79306d53d74da12f1f0cb03ea48
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user