167 lines
5.4 KiB
C#
167 lines
5.4 KiB
C#
|
|
using System;
|
|||
|
|
using TMPro;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using UnityEngine.InputSystem;
|
|||
|
|
using UnityEngine.UI;
|
|||
|
|
|
|||
|
|
namespace Cielonos.Settings.UI
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 键位绑定条目,用于 <see cref="KeyBindingPage"/> 中的每一行。
|
|||
|
|
/// <para>
|
|||
|
|
/// 显示:[Action 名称] [当前按键文本] [重新绑定按钮]
|
|||
|
|
/// 点击重新绑定按钮后进入监听模式,捕获下一个按键输入并更新绑定。
|
|||
|
|
/// </para>
|
|||
|
|
/// </summary>
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
/// <summary>绑定完成时触发。</summary>
|
|||
|
|
public event Action OnBindingChanged;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 初始化键位绑定条目。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="action">要绑定的 InputAction。</param>
|
|||
|
|
/// <param name="targetBindingIndex">
|
|||
|
|
/// 要重新绑定的 binding 索引。
|
|||
|
|
/// 对于非 composite 的 action,通常为 0。
|
|||
|
|
/// </param>
|
|||
|
|
/// <param name="displayName">条目显示名称(可为 null,则使用 action 名)。</param>
|
|||
|
|
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();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 刷新当前按键的显示文本。
|
|||
|
|
/// </summary>
|
|||
|
|
public void RefreshDisplay()
|
|||
|
|
{
|
|||
|
|
if (inputAction == null || currentKeyText == null) return;
|
|||
|
|
|
|||
|
|
string displayString = inputAction.bindings[bindingIndex].ToDisplayString(
|
|||
|
|
InputBinding.DisplayStringOptions.DontUseShortDisplayNames);
|
|||
|
|
|
|||
|
|
currentKeyText.text = displayString;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 开始交互式重新绑定。
|
|||
|
|
/// </summary>
|
|||
|
|
private void StartRebind()
|
|||
|
|
{
|
|||
|
|
if (inputAction == null) return;
|
|||
|
|
|
|||
|
|
// 显示等待提示
|
|||
|
|
SetWaitingState(true);
|
|||
|
|
|
|||
|
|
// 先禁用 action 以允许重新绑定
|
|||
|
|
inputAction.Disable();
|
|||
|
|
|
|||
|
|
rebindOperation = inputAction.PerformInteractiveRebinding(bindingIndex)
|
|||
|
|
.WithControlsExcluding("<Mouse>/position")
|
|||
|
|
.WithControlsExcluding("<Mouse>/delta")
|
|||
|
|
.WithControlsExcluding("<Pointer>/position")
|
|||
|
|
.WithControlsExcluding("<Pointer>/delta")
|
|||
|
|
.WithCancelingThrough("<Keyboard>/escape")
|
|||
|
|
.OnMatchWaitForAnother(0.1f)
|
|||
|
|
.OnComplete(operation => FinishRebind(true))
|
|||
|
|
.OnCancel(operation => FinishRebind(false))
|
|||
|
|
.Start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 重新绑定完成或取消后的处理。
|
|||
|
|
/// </summary>
|
|||
|
|
private void FinishRebind(bool completed)
|
|||
|
|
{
|
|||
|
|
rebindOperation?.Dispose();
|
|||
|
|
rebindOperation = null;
|
|||
|
|
|
|||
|
|
inputAction.Enable();
|
|||
|
|
SetWaitingState(false);
|
|||
|
|
RefreshDisplay();
|
|||
|
|
|
|||
|
|
if (completed)
|
|||
|
|
{
|
|||
|
|
OnBindingChanged?.Invoke();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 重置此条目的绑定到默认值。
|
|||
|
|
/// </summary>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 将 ActionName (camelCase) 格式化为可读文本。
|
|||
|
|
/// </summary>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|