UI系统组件

This commit is contained in:
2026-06-06 09:00:11 +08:00
parent fe4fd60083
commit d794b83ebe
107 changed files with 25690 additions and 476 deletions

View 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;
}
}
}