Files
Cielonos/Assets/Scripts/Settings/UI/KeyBindingPage.cs
SoulliesOfficial 6d7ebc5825 Passion & UI
2026-06-12 17:11:39 -04:00

294 lines
9.9 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 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();
}
}
}