Files
Cielonos/Assets/Scripts/MainGame/UI/Common/SettingsPage/SettingsUIPage.cs
SoulliesOfficial 6d7ebc5825 Passion & UI
2026-06-12 17:11:39 -04:00

463 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 设置界面页面(单页滚动式 + 右侧面板)。
/// <para>
/// <b>左侧:</b>所有设置分类在同一页面中按顺序展示,每个分类前插入标题。
/// 通过反射读取各设置类的公共字段,按类型自动选择 Entry Prefab 实例化。
/// 同时支持手动添加的按钮条目(如"Key Bindings"按钮)。
/// </para>
/// <para>
/// <b>右侧:</b>包含两种面板状态:
/// <list type="number">
/// <item>说明面板(<see cref="descriptionPanel"/>):鼠标悬停条目时显示说明文本,离开后隐藏。</item>
/// <item>详情页面(如 <see cref="keyBindingPage"/>):点击按钮后打开的二级 UIPage
/// 打开期间悬停说明被抑制,需手动关闭。</item>
/// </list>
/// </para>
/// </summary>
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<SettingsEntryBase> activeFieldEntries = new();
private readonly List<SettingsEntryButton> activeButtonEntries = new();
private readonly List<GameObject> spawnedObjects = new();
/// <summary>是否有详情页面处于打开状态(抑制悬停说明)。</summary>
private bool isDetailPageOpen;
// ──────────────────── 分类定义 ────────────────────
/// <summary>
/// 分类定义显示名称、设置实例、Apply/Reset 回调、附加按钮列表。
/// </summary>
private struct SectionDefinition
{
public string displayName;
public object settingsInstance;
public Action applyAction;
public Action resetAction;
public List<ButtonDefinition> buttons;
}
/// <summary>
/// 手动添加的按钮条目定义。
/// </summary>
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();
}
// ──────────────────── 分类定义构建 ────────────────
/// <summary>
/// 构建所有分类定义。扩展时在此处追加。
/// </summary>
private List<SectionDefinition> BuildSectionDefinitions()
{
var manager = GameSettingsManager.Instance;
if (manager == null) return null;
return new List<SectionDefinition>
{
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<ButtonDefinition>
{
new()
{
label = "Key Bindings",
description = "Customize keyboard and mouse bindings for all game actions.",
onClick = OpenKeyBindingPage
}
}
}
};
}
// ──────────────────── 条目生成 ────────────────────
/// <summary>
/// 构建所有分类的标题、字段条目和按钮条目。
/// </summary>
private void BuildAllSections()
{
ClearAll();
var sections = BuildSectionDefinitions();
if (sections == null)
{
Debug.LogError("[SettingsUIPage] GameSettingsManager not found.");
return;
}
foreach (var section in sections)
{
BuildSection(section);
}
}
/// <summary>
/// 构建单个分类的标题、字段条目和按钮条目。
/// </summary>
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<SettingsIgnoreAttribute>() == 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<SettingsIgnoreAttribute>() != 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<SettingsEntryBase>();
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);
}
}
}
/// <summary>
/// 为字段条目注册悬停回调。
/// </summary>
private void RegisterFieldEntryHover(SettingsEntryBase entry)
{
entry.OnHoverEnter = HandleHoverEnter;
entry.OnHoverExit = HandleHoverExit;
}
/// <summary>
/// 实例化按钮条目。
/// </summary>
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<SettingsEntryButton>();
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);
}
/// <summary>
/// 实例化分类标题预制体。
/// </summary>
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<TMP_Text>();
if (headerText != null)
headerText.text = title;
spawnedObjects.Add(headerObj);
}
/// <summary>
/// 根据字段类型选择对应的预制体。
/// </summary>
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;
}
}
}
}