215 lines
9.7 KiB
C#
215 lines
9.7 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|