using System; using System.Collections.Generic; using System.Reflection; using Cielonos.MainGame; using Cielonos.Settings; using Cielonos.Settings.UI; using SLSUtilities.UI; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace Cielonos.UI { /// /// 设置界面页面(单页滚动式 + 右侧面板)。 /// /// 左侧:所有设置分类在同一页面中按顺序展示,每个分类前插入标题。 /// 通过反射读取各设置类的公共字段,按类型自动选择 Entry Prefab 实例化。 /// 同时支持手动添加的按钮条目(如"Key Bindings"按钮)。 /// /// /// 右侧:包含两种面板状态: /// /// 说明面板():鼠标悬停条目时显示说明文本,离开后隐藏。 /// 详情页面(如 ):点击按钮后打开的二级 UIPage, /// 打开期间悬停说明被抑制,需手动关闭。 /// /// /// public class SettingsUIPage : UIPageBase { // ──────────────────── Entry Prefabs ────────────────── [Header("Entry Prefabs")] [Tooltip("bool 字段使用的 Toggle 条目预制体,需挂载 SettingsEntryToggle。")] [SerializeField] private GameObject toggleEntryPrefab; [Tooltip("带 [Range] 的 int 字段使用的 Slider 条目预制体,需挂载 SettingsEntrySlider。")] [SerializeField] private GameObject sliderEntryPrefab; [Tooltip("enum 字段使用的 Dropdown 条目预制体,需挂载 SettingsEntryDropdown。")] [SerializeField] private GameObject dropdownEntryPrefab; [Tooltip("按钮条目预制体,需挂载 SettingsEntryButton。")] [SerializeField] private GameObject buttonEntryPrefab; // ──────────────────── Section Header ────────────────── [Header("Section Header")] [Tooltip("分类标题预制体,需包含 TMP_Text 组件。")] [SerializeField] private GameObject sectionHeaderPrefab; // ──────────────────── Left Content ──────────────────── [Header("Left Content")] [Tooltip("条目的父容器,应挂载 VerticalLayoutGroup 和 ContentSizeFitter。")] [SerializeField] private RectTransform contentContainer; // ──────────────────── Right Panel ───────────────────── [Header("Right Panel - Description")] [Tooltip("右侧悬停说明面板,显示条目的标题和说明文本。")] [SerializeField] private SettingsDescriptionPanel descriptionPanel; [Header("Right Panel - Detail Pages")] [Tooltip("键位绑定二级页面。")] [SerializeField] private KeyBindingPage keyBindingPage; // ──────────────────── Input ─────────────────────────── [Header("Input")] [Tooltip("用于键位绑定页面的 InputActionAsset。为空时从 Player 获取。")] [SerializeField] private InputActionAsset inputActionAsset; // ──────────────────── 底部按钮 ──────────────────── [Header("Bottom Buttons")] [SerializeField] private Button resetButton; [SerializeField] private Button backButton; // ──────────────────── 运行时状态 ────────────────── private readonly List activeFieldEntries = new(); private readonly List activeButtonEntries = new(); private readonly List spawnedObjects = new(); /// 是否有详情页面处于打开状态(抑制悬停说明)。 private bool isDetailPageOpen; // ──────────────────── 分类定义 ──────────────────── /// /// 分类定义:显示名称、设置实例、Apply/Reset 回调、附加按钮列表。 /// private struct SectionDefinition { public string displayName; public object settingsInstance; public Action applyAction; public Action resetAction; public List buttons; } /// /// 手动添加的按钮条目定义。 /// private struct ButtonDefinition { public string label; public string description; public Action onClick; } // ──────────────────── 生命周期 ──────────────────── protected override void Start() { base.Start(); resetButton?.onClick.AddListener(OnResetClicked); backButton?.onClick.AddListener(OnBackClicked); // 监听详情页面的开关状态 if (keyBindingPage != null) { keyBindingPage.PageOpened += OnDetailPageOpened; keyBindingPage.PageClosed += OnDetailPageClosed; } } protected override void OnPageOpened() { BuildAllSections(); } protected override void OnPageClosed() { // 先关闭可能打开的详情页面 if (isDetailPageOpen && keyBindingPage != null && keyBindingPage.IsOpen) keyBindingPage.Close(); ClearAll(); descriptionPanel?.Hide(); GameSettingsManager.Instance?.Save(); } // ──────────────────── 详情页面状态 ──────────────── private void OnDetailPageOpened() { isDetailPageOpen = true; descriptionPanel?.Hide(); } private void OnDetailPageClosed() { isDetailPageOpen = false; } // ──────────────────── 悬停回调 ──────────────────── private void HandleHoverEnter(string title, string description) { if (isDetailPageOpen) return; descriptionPanel?.Show(title, description); } private void HandleHoverExit() { if (isDetailPageOpen) return; descriptionPanel?.Hide(); } // ──────────────────── 分类定义构建 ──────────────── /// /// 构建所有分类定义。扩展时在此处追加。 /// private List BuildSectionDefinitions() { var manager = GameSettingsManager.Instance; if (manager == null) return null; return new List { new() { displayName = "Gameplay", settingsInstance = manager.gameplay, applyAction = manager.ApplyGameplay, resetAction = manager.ResetGameplayToDefault }, new() { displayName = "Graphics", settingsInstance = manager.graphics, applyAction = manager.ApplyGraphics, resetAction = manager.ResetGraphicsToDefault }, new() { displayName = "Sound", settingsInstance = manager.sound, applyAction = manager.ApplySound, resetAction = manager.ResetSoundToDefault }, new() { displayName = "Controls", settingsInstance = manager.controls, applyAction = manager.ApplyControls, resetAction = manager.ResetControlsToDefault, buttons = new List { new() { label = "Key Bindings", description = "Customize keyboard and mouse bindings for all game actions.", onClick = OpenKeyBindingPage } } } }; } // ──────────────────── 条目生成 ──────────────────── /// /// 构建所有分类的标题、字段条目和按钮条目。 /// private void BuildAllSections() { ClearAll(); var sections = BuildSectionDefinitions(); if (sections == null) { Debug.LogError("[SettingsUIPage] GameSettingsManager not found."); return; } foreach (var section in sections) { BuildSection(section); } } /// /// 构建单个分类的标题、字段条目和按钮条目。 /// private void BuildSection(SectionDefinition section) { FieldInfo[] fields = section.settingsInstance.GetType() .GetFields(BindingFlags.Public | BindingFlags.Instance); // 预扫描:确认该分类至少有一个可显示的字段或按钮 bool hasVisibleContent = section.buttons is { Count: > 0 }; if (!hasVisibleContent) { foreach (FieldInfo field in fields) { if (field.GetCustomAttribute() == null && ResolvePrefab(field) != null) { hasVisibleContent = true; break; } } } if (!hasVisibleContent) return; // 插入分类标题 SpawnSectionHeader(section.displayName); // 生成字段条目 Action applyAction = section.applyAction; foreach (FieldInfo field in fields) { if (field.GetCustomAttribute() != null) continue; GameObject prefab = ResolvePrefab(field); if (prefab == null) { Debug.LogWarning( $"[SettingsUIPage] No matching prefab for field '{field.Name}' " + $"(type: {field.FieldType.Name}). Skipping."); continue; } GameObject entryObj = Instantiate(prefab, contentContainer); var entry = entryObj.GetComponent(); if (entry == null) { Debug.LogError( $"[SettingsUIPage] Prefab for field '{field.Name}' " + "is missing a SettingsEntryBase component."); Destroy(entryObj); continue; } entry.Initialize(section.settingsInstance, field, () => applyAction?.Invoke()); RegisterFieldEntryHover(entry); activeFieldEntries.Add(entry); spawnedObjects.Add(entryObj); } // 生成按钮条目 if (section.buttons != null) { foreach (var btnDef in section.buttons) { SpawnButtonEntry(btnDef); } } } /// /// 为字段条目注册悬停回调。 /// private void RegisterFieldEntryHover(SettingsEntryBase entry) { entry.OnHoverEnter = HandleHoverEnter; entry.OnHoverExit = HandleHoverExit; } /// /// 实例化按钮条目。 /// private void SpawnButtonEntry(ButtonDefinition def) { if (buttonEntryPrefab == null) { Debug.LogWarning("[SettingsUIPage] buttonEntryPrefab is not assigned."); return; } GameObject entryObj = Instantiate(buttonEntryPrefab, contentContainer); var buttonEntry = entryObj.GetComponent(); if (buttonEntry == null) { Debug.LogError("[SettingsUIPage] buttonEntryPrefab is missing SettingsEntryButton."); Destroy(entryObj); return; } buttonEntry.Initialize(def.label, def.description, def.onClick); buttonEntry.OnHoverEnter = HandleHoverEnter; buttonEntry.OnHoverExit = HandleHoverExit; activeButtonEntries.Add(buttonEntry); spawnedObjects.Add(entryObj); } /// /// 实例化分类标题预制体。 /// private void SpawnSectionHeader(string title) { if (sectionHeaderPrefab == null) { Debug.LogWarning("[SettingsUIPage] sectionHeaderPrefab is not assigned."); return; } GameObject headerObj = Instantiate(sectionHeaderPrefab, contentContainer); var headerText = headerObj.GetComponentInChildren(); if (headerText != null) headerText.text = title; spawnedObjects.Add(headerObj); } /// /// 根据字段类型选择对应的预制体。 /// private GameObject ResolvePrefab(FieldInfo field) { Type fieldType = field.FieldType; if (fieldType == typeof(bool)) return toggleEntryPrefab; if (fieldType == typeof(int)) return sliderEntryPrefab; if (fieldType.IsEnum) return dropdownEntryPrefab; return null; } // ──────────────────── 详情页面入口 ──────────────── private void OpenKeyBindingPage() { if (keyBindingPage == null) { Debug.LogWarning("[SettingsUIPage] keyBindingPage is not assigned."); return; } // 优先使用配置的 InputActionAsset,否则尝试从 Player 获取 InputActionAsset asset = inputActionAsset; if (asset == null) { var player = MainGameManager.Player; if (player != null) asset = player.inputSc.inputActions.asset; } if (asset != null) { keyBindingPage.Open(asset); } else { Debug.LogError("[SettingsUIPage] No InputActionAsset available for KeyBindingPage."); } } // ──────────────────── 底部按钮回调 ──────────────── private void OnResetClicked() { GameSettingsManager.Instance?.ResetAllToDefault(); BuildAllSections(); } private void OnBackClicked() { Close(); } // ──────────────────── 清理 ──────────────────────── private void ClearAll() { foreach (var obj in spawnedObjects) { if (obj != null) Destroy(obj); } activeFieldEntries.Clear(); activeButtonEntries.Clear(); spawnedObjects.Clear(); } private void OnDestroy() { if (keyBindingPage != null) { keyBindingPage.PageOpened -= OnDetailPageOpened; keyBindingPage.PageClosed -= OnDetailPageClosed; } } } }