using System; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace Cielonos.Settings.UI { /// /// 键位绑定条目,用于 中的每一行。 /// /// 显示:[Action 名称] [当前按键文本] [重新绑定按钮] /// 点击重新绑定按钮后进入监听模式,捕获下一个按键输入并更新绑定。 /// /// public class SettingsEntryKeyBinding : MonoBehaviour { [SerializeField] private TMP_Text actionNameText; [SerializeField] private TMP_Text currentKeyText; [SerializeField] private Button rebindButton; [SerializeField] private TMP_Text rebindButtonText; [Header("Visuals")] [SerializeField] private string waitingText = "..."; [SerializeField] private GameObject waitingOverlay; private InputAction inputAction; private int bindingIndex; private InputActionRebindingExtensions.RebindingOperation rebindOperation; /// 绑定完成时触发。 public event Action OnBindingChanged; /// /// 初始化键位绑定条目。 /// /// 要绑定的 InputAction。 /// /// 要重新绑定的 binding 索引。 /// 对于非 composite 的 action,通常为 0。 /// /// 条目显示名称(可为 null,则使用 action 名)。 public void Initialize(InputAction action, int targetBindingIndex, string displayName = null) { inputAction = action; bindingIndex = targetBindingIndex; if (actionNameText != null) actionNameText.text = displayName ?? FormatActionName(action.name); if (rebindButton != null) rebindButton.onClick.AddListener(StartRebind); RefreshDisplay(); } /// /// 刷新当前按键的显示文本。 /// public void RefreshDisplay() { if (inputAction == null || currentKeyText == null) return; string displayString = inputAction.bindings[bindingIndex].ToDisplayString( InputBinding.DisplayStringOptions.DontUseShortDisplayNames); currentKeyText.text = displayString; } /// /// 开始交互式重新绑定。 /// private void StartRebind() { if (inputAction == null) return; // 显示等待提示 SetWaitingState(true); // 先禁用 action 以允许重新绑定 inputAction.Disable(); rebindOperation = inputAction.PerformInteractiveRebinding(bindingIndex) .WithControlsExcluding("/position") .WithControlsExcluding("/delta") .WithControlsExcluding("/position") .WithControlsExcluding("/delta") .WithCancelingThrough("/escape") .OnMatchWaitForAnother(0.1f) .OnComplete(operation => FinishRebind(true)) .OnCancel(operation => FinishRebind(false)) .Start(); } /// /// 重新绑定完成或取消后的处理。 /// private void FinishRebind(bool completed) { rebindOperation?.Dispose(); rebindOperation = null; inputAction.Enable(); SetWaitingState(false); RefreshDisplay(); if (completed) { OnBindingChanged?.Invoke(); } } /// /// 重置此条目的绑定到默认值。 /// public void ResetToDefault() { if (inputAction == null) return; inputAction.RemoveBindingOverride(bindingIndex); RefreshDisplay(); OnBindingChanged?.Invoke(); } private void SetWaitingState(bool isWaiting) { if (currentKeyText != null) currentKeyText.text = isWaiting ? waitingText : ""; if (rebindButton != null) rebindButton.interactable = !isWaiting; if (waitingOverlay != null) waitingOverlay.SetActive(isWaiting); } /// /// 将 ActionName (camelCase) 格式化为可读文本。 /// private static string FormatActionName(string actionName) { if (string.IsNullOrEmpty(actionName)) return actionName; var sb = new System.Text.StringBuilder(actionName.Length + 4); sb.Append(char.ToUpper(actionName[0])); for (int i = 1; i < actionName.Length; i++) { char c = actionName[i]; if (char.IsUpper(c) && i > 0 && char.IsLower(actionName[i - 1])) sb.Append(' '); sb.Append(c); } return sb.ToString(); } private void OnDestroy() { rebindOperation?.Dispose(); if (rebindButton != null) rebindButton.onClick.RemoveListener(StartRebind); } } }