using System; using System.Collections.Generic; using Cielonos.Settings; using SLSUtilities.UI; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace Cielonos.Settings.UI { /// /// 键位绑定二级 UIPage。 /// /// 从 读取所有可绑定的 Action, /// 为每个 Action 生成一行 。 /// Composite 类型的 Action(如 WASD Move)的每个 part 单独显示一行。 /// /// /// 此页面是 ,需手动关闭(Back 按钮或 ESC)。 /// 关闭时自动将绑定覆盖保存到 。 /// /// 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 activeEntries = new(); private readonly List spawnedObjects = new(); /// 当任何绑定变更时触发(供 SettingsUIPage 监听)。 public event Action OnAnyBindingChanged; // ──────────────────── 生命周期 ──────────────────── protected override void Start() { base.Start(); resetAllButton?.onClick.AddListener(OnResetAllClicked); backButton?.onClick.AddListener(OnBackClicked); } /// /// 使用指定的 InputActionAsset 打开键位绑定页面。 /// 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); } } } /// /// 为一个 Action 生成绑定条目。 /// Composite binding 的每个 part 生成独立条目;普通 binding 生成一个条目。 /// 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(); 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(); if (headerText != null) headerText.text = title; spawnedObjects.Add(headerObj); } // ──────────────────── 绑定变更 ──────────────────── private void OnEntryBindingChanged() { OnAnyBindingChanged?.Invoke(); } /// /// 将当前绑定覆盖序列化到 , /// 并重新初始化 。 /// 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(); } } }