using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using BaseGames.Core; using BaseGames.Localization; namespace BaseGames.UI.Settings { /// /// 数据驱动设置面板:据 生成控件行并绑定 。 /// /// 取代硬编码的 :策划改表即可增删 / 重排 / 改标签 / 分节, /// 无需改代码。每行据 自动选用 Slider / Toggle / Dropdown 行预制件, /// 并复用通用控件 / 。 /// /// 控件来源:行预制件由 SettingsPanelScaffold 生成(标签为 )。 /// 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 _spawned = new(); // ── 生命周期 ────────────────────────────────────────────────────────── protected override void OnPanelOpen() { _settings = GetService(); _loc = GetService(); Build(); } protected override void OnPanelClose() => Clear(); // ── 构建 ────────────────────────────────────────────────────────────── /// 据 schema 重建所有行。可在无 ISettingsService 时调用(仅生成行 + 标签,不绑定)。 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(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(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(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(true); if (ui == null) return; var data = _settings.Current; switch (key) { case SettingKey.TargetFPS: { var opts = new List { "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(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(LanguageNativeNames); int idx = _loc != null ? Array.IndexOf(LanguageOptions, _loc.CurrentLanguage) : 0; if (idx < 0) idx = 0; ui.Bind(opts, idx, i => GetService()?.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(true); if (sel != null) return sel.gameObject; } return null; } } }