Passion & UI

This commit is contained in:
SoulliesOfficial
2026-06-12 17:11:39 -04:00
parent 7bc1e1722c
commit 6d7ebc5825
3444 changed files with 865284 additions and 463132 deletions

View File

@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using Cielonos.Settings;
using SLSUtilities.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace Cielonos.Settings.UI
{
/// <summary>
/// 键位绑定二级 UIPage。
/// <para>
/// 从 <see cref="InputActionAsset"/> 读取所有可绑定的 Action
/// 为每个 Action 生成一行 <see cref="SettingsEntryKeyBinding"/>。
/// Composite 类型的 Action如 WASD Move的每个 part 单独显示一行。
/// </para>
/// <para>
/// 此页面是 <see cref="UIPageBase"/>需手动关闭Back 按钮或 ESC
/// 关闭时自动将绑定覆盖保存到 <see cref="ControlsSettings.bindingOverridesJson"/>。
/// </para>
/// </summary>
public class KeyBindingPage : UIPageBase
{
[Header("Prefabs")]
[Tooltip("键位绑定条目预制体,需挂载 SettingsEntryKeyBinding。")]
[SerializeField] private GameObject keyBindingEntryPrefab;
[Tooltip("分组标题预制体(可选),用于分隔不同 ActionMap。")]
[SerializeField] private GameObject sectionHeaderPrefab;
[Header("Content")]
[Tooltip("条目的父容器,应挂载 VerticalLayoutGroup。")]
[SerializeField] private RectTransform contentContainer;
[Header("Buttons")]
[SerializeField] private Button resetAllButton;
[SerializeField] private Button backButton;
[Header("Input")]
[Tooltip("要显示的 InputActionAsset。为空时从 GameSettingsManager 上下文获取。")]
[SerializeField] private InputActionAsset inputActionAsset;
[Tooltip("要显示的 Control Scheme 名称。")]
[SerializeField] private string controlScheme = "KeyboardMouse";
private readonly List<SettingsEntryKeyBinding> activeEntries = new();
private readonly List<GameObject> spawnedObjects = new();
/// <summary>当任何绑定变更时触发(供 SettingsUIPage 监听)。</summary>
public event Action OnAnyBindingChanged;
// ──────────────────── 生命周期 ────────────────────
protected override void Start()
{
base.Start();
resetAllButton?.onClick.AddListener(OnResetAllClicked);
backButton?.onClick.AddListener(OnBackClicked);
}
/// <summary>
/// 使用指定的 InputActionAsset 打开键位绑定页面。
/// </summary>
public void Open(InputActionAsset asset)
{
inputActionAsset = asset;
Open();
}
protected override void OnPageOpened()
{
BuildEntries();
}
protected override void OnPageClosed()
{
SaveBindingOverrides();
ClearAll();
}
// ──────────────────── 条目生成 ────────────────────
private void BuildEntries()
{
ClearAll();
if (inputActionAsset == null)
{
Debug.LogError("[KeyBindingPage] InputActionAsset is not assigned.");
return;
}
foreach (InputActionMap map in inputActionAsset.actionMaps)
{
bool hasBindableAction = false;
foreach (InputAction action in map.actions)
{
if (HasBindableBindings(action))
{
hasBindableAction = true;
break;
}
}
if (!hasBindableAction) continue;
// 插入 ActionMap 标题
SpawnSectionHeader(FormatMapName(map.name));
foreach (InputAction action in map.actions)
{
SpawnEntriesForAction(action);
}
}
}
/// <summary>
/// 为一个 Action 生成绑定条目。
/// Composite binding 的每个 part 生成独立条目;普通 binding 生成一个条目。
/// </summary>
private void SpawnEntriesForAction(InputAction action)
{
var bindings = action.bindings;
for (int i = 0; i < bindings.Count; i++)
{
InputBinding binding = bindings[i];
// 跳过不匹配 controlScheme 的绑定
if (!IsBindingMatchingScheme(binding))
continue;
if (binding.isComposite)
{
// composite 的 header不生成条目但后续 parts 会单独生成
continue;
}
if (binding.isPartOfComposite)
{
// composite 的一个 part显示为 "ActionName - PartName"
string displayName = $"{FormatActionName(action.name)} - {FormatActionName(binding.name)}";
SpawnKeyBindingEntry(action, i, displayName);
}
else
{
// 普通(非 compositebinding
SpawnKeyBindingEntry(action, i, FormatActionName(action.name));
}
}
}
private void SpawnKeyBindingEntry(InputAction action, int bindingIndex, string displayName)
{
if (keyBindingEntryPrefab == null) return;
GameObject entryObj = Instantiate(keyBindingEntryPrefab, contentContainer);
var entry = entryObj.GetComponent<SettingsEntryKeyBinding>();
if (entry == null)
{
Debug.LogError("[KeyBindingPage] keyBindingEntryPrefab is missing SettingsEntryKeyBinding.");
Destroy(entryObj);
return;
}
entry.Initialize(action, bindingIndex, displayName);
entry.OnBindingChanged += OnEntryBindingChanged;
activeEntries.Add(entry);
spawnedObjects.Add(entryObj);
}
private void SpawnSectionHeader(string title)
{
if (sectionHeaderPrefab == null) return;
GameObject headerObj = Instantiate(sectionHeaderPrefab, contentContainer);
var headerText = headerObj.GetComponentInChildren<TMPro.TMP_Text>();
if (headerText != null)
headerText.text = title;
spawnedObjects.Add(headerObj);
}
// ──────────────────── 绑定变更 ────────────────────
private void OnEntryBindingChanged()
{
OnAnyBindingChanged?.Invoke();
}
/// <summary>
/// 将当前绑定覆盖序列化到 <see cref="ControlsSettings.bindingOverridesJson"/>
/// 并重新初始化 <see cref="InputBindingResolver"/>。
/// </summary>
private void SaveBindingOverrides()
{
if (inputActionAsset == null) return;
var manager = GameSettingsManager.Instance;
if (manager != null)
{
manager.controls.bindingOverridesJson = inputActionAsset.SaveBindingOverridesAsJson();
manager.Save();
}
// 重新初始化 glyph 解析器以反映新的绑定
InputBindingResolver.Initialize(inputActionAsset, controlScheme);
}
// ──────────────────── 按钮回调 ────────────────────
private void OnResetAllClicked()
{
if (inputActionAsset == null) return;
inputActionAsset.RemoveAllBindingOverrides();
// 重建条目以刷新显示
BuildEntries();
OnAnyBindingChanged?.Invoke();
}
private void OnBackClicked()
{
Close();
}
// ──────────────────── 工具方法 ────────────────────
private bool HasBindableBindings(InputAction action)
{
foreach (InputBinding binding in action.bindings)
{
if (binding.isComposite) continue;
if (IsBindingMatchingScheme(binding))
return true;
}
return false;
}
private bool IsBindingMatchingScheme(InputBinding binding)
{
if (string.IsNullOrEmpty(controlScheme)) return true;
if (string.IsNullOrEmpty(binding.groups)) return false;
return binding.groups.Contains(controlScheme);
}
private static string FormatActionName(string name)
{
if (string.IsNullOrEmpty(name)) return name;
var sb = new System.Text.StringBuilder(name.Length + 4);
sb.Append(char.ToUpper(name[0]));
for (int i = 1; i < name.Length; i++)
{
char c = name[i];
if (char.IsUpper(c) && i > 0 && char.IsLower(name[i - 1]))
sb.Append(' ');
sb.Append(c);
}
return sb.ToString();
}
private static string FormatMapName(string mapName)
{
return FormatActionName(mapName);
}
// ──────────────────── 清理 ────────────────────────
private void ClearAll()
{
foreach (var entry in activeEntries)
{
if (entry != null)
entry.OnBindingChanged -= OnEntryBindingChanged;
}
activeEntries.Clear();
foreach (var obj in spawnedObjects)
{
if (obj != null)
Destroy(obj);
}
spawnedObjects.Clear();
}
}
}