Passion & UI
This commit is contained in:
293
Assets/Scripts/Settings/UI/KeyBindingPage.cs
Normal file
293
Assets/Scripts/Settings/UI/KeyBindingPage.cs
Normal 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
|
||||
{
|
||||
// 普通(非 composite)binding
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user