【Unity笔记】实现可视化配置的Unity按键输入管理器(按下/长按/松开事件 + UnityEvent绑定)

引言

Unity 输入管理器是处理复杂输入交互的核心组件,支持按下长按松开等多种事件类型,通过可视化配置和 UnityEvent 绑定实现零代码的输入处理。特别适用于游戏控制、UI 交互、调试工具等场景,支持新旧输入系统,具备热重载多平台适配编辑器可视化特性。

核心技术包括 Input System 集成、自定义 Inspector、ScriptableObject 配置和事件驱动架构。时间复杂度 O(n),n 为绑定事件数量,支持 Unity 2021.3+(新输入系统)或 2019.4+(旧输入系统)。

核心架构设计

1. 输入事件类型定义

事件类型触发条件适用场景
按下 (Pressed)按键瞬间触发跳跃、射击
长按 (Held)持续按住触发蓄力攻击、连续移动
松开 (Released)按键松开触发释放技能、停止移动
重复 (Repeated)长按后周期触发自动射击、连续输入

2. 配置数据结构

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using System;

[CreateAssetMenu(fileName = "InputConfig", menuName = "Input Manager/Input Configuration", order = 1)]
public class InputConfiguration : ScriptableObject
{
    [System.Serializable]
    public class InputBinding
    {
        [Header("按键绑定")]
        public Key keyCode = Key.Space;
        public string actionName;  // 新输入系统 Action 名

        [Header("触发设置")]
        public InputEventType eventType = InputEventType.Pressed;
        [Range(0.1f, 5f)]
        public float holdDuration = 0.5f;  // 长按检测时间
        [Range(0.1f, 1f)]
        public float repeatInterval = 0.1f;  // 重复触发间隔

        [Header("事件绑定")]
        public UnityEvent onPressed;
        public UnityEvent onHeld;
        public UnityEvent onReleased;
        public UnityEvent onRepeated;

        [NonSerialized] private bool isHeld;
        [NonSerialized] private float holdTimer;
        [NonSerialized] private float lastRepeatTime;

        public enum InputEventType
        {
            Pressed, Held, Released, Repeated
        }

        public Key Key => keyCode;
        public bool IsActive => onPressed != null || onHeld != null || onReleased != null || onRepeated != null;
    }

    [Header("输入绑定列表")]
    public InputBinding[] bindings;

    public void Initialize()
    {
        // 配置验证
        if (bindings == null) bindings = new InputBinding[0];

        for (int i = 0; i < bindings.Length; i++)
        {
            if (!bindings[i].IsActive)
            {
                Debug.LogWarning($"Empty binding at index {i}", this);
            }
        }
    }
}

核心输入管理器实现

3. 主输入管理器组件

using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections.Generic;

public class InputManager : MonoBehaviour
{
    public static InputManager Instance { get; private set; }

    [Header("配置")]
    public InputConfiguration inputConfig;
    public bool useNewInputSystem = true;

    [Header("调试")]
    public bool showDebugInfo = false;

    private Dictionary<Key, InputConfiguration.InputBinding> keyBindings = new Dictionary<Key, InputConfiguration.InputBinding>();
    private List<InputConfiguration.InputBinding> activeBindings = new List<InputConfiguration.InputBinding>();

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeInput();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void InitializeInput()
    {
        if (inputConfig != null)
        {
            inputConfig.Initialize();
            CacheBindings();
        }

        if (showDebugInfo)
        {
            Debug.Log($"InputManager initialized with {activeBindings.Count} active bindings");
        }
    }

    private void CacheBindings()
    {
        keyBindings.Clear();
        activeBindings.Clear();

        foreach (var binding in inputConfig.bindings)
        {
            if (binding.IsActive)
            {
                keyBindings[binding.Key] = binding;
                activeBindings.Add(binding);
            }
        }
    }

    void Update()
    {
        if (!useNewInputSystem)
        {
            HandleLegacyInput();
        }
        // 新输入系统在 InputSystem 回调中处理
    }

    #region Legacy Input System (Unity 2019.4+)
    private void HandleLegacyInput()
    {
        foreach (var kvp in keyBindings)
        {
            Key key = kvp.Key;
            var binding = kvp.Value;

            bool isKeyDown = Input.GetKeyDown(key);
            bool isKey = Input.GetKey(key);
            bool isKeyUp = Input.GetKeyUp(key);

            ProcessInputEvent(binding, isKeyDown, isKey, isKeyUp);
        }
    }
    #endregion

    #region New Input System Callbacks
    // 通过 Input Action Asset 配置这些回调
    public void OnActionPressed(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            string actionName = context.action.name;
            TriggerBindingByActionName(actionName, InputConfiguration.InputBinding.InputEventType.Pressed);
        }
    }

    public void OnActionHeld(InputAction.CallbackContext context)
    {
        if (context.canceled)
        {
            string actionName = context.action.name;
            TriggerBindingByActionName(actionName, InputConfiguration.InputBinding.InputEventType.Released);
        }
    }
    #endregion

    private void ProcessInputEvent(InputConfiguration.InputBinding binding, bool isDown, bool isHeld, bool isUp)
    {
        if (isDown)
        {
            binding.onPressed?.Invoke();
            binding.isHeld = true;
            binding.holdTimer = 0f;
        }

        if (isHeld && binding.isHeld)
        {
            binding.holdTimer += Time.deltaTime;

            // 长按检测
            if (binding.holdTimer >= binding.holdDuration)
            {
                binding.onHeld?.Invoke();

                // 重复触发
                if (binding.eventType == InputConfiguration.InputBinding.InputEventType.Repeated &&
                    Time.time - binding.lastRepeatTime >= binding.repeatInterval)
                {
                    binding.onRepeated?.Invoke();
                    binding.lastRepeatTime = Time.time;
                }
            }
        }

        if (isUp)
        {
            binding.onReleased?.Invoke();
            binding.isHeld = false;
            binding.holdTimer = 0f;
        }
    }

    private void TriggerBindingByActionName(string actionName, InputConfiguration.InputBinding.InputEventType eventType)
    {
        foreach (var binding in activeBindings)
        {
            if (binding.actionName == actionName && binding.eventType == eventType)
            {
                switch (eventType)
                {
                    case InputConfiguration.InputBinding.InputEventType.Pressed:
                        binding.onPressed?.Invoke();
                        break;
                    case InputConfiguration.InputBinding.InputEventType.Held:
                        binding.onHeld?.Invoke();
                        break;
                    case InputConfiguration.InputBinding.InputEventType.Released:
                        binding.onReleased?.Invoke();
                        break;
                }
            }
        }
    }

    /// <summary>
    /// 运行时重新加载配置(热重载)
    /// </summary>
    public void ReloadConfiguration()
    {
        CacheBindings();
        Debug.Log("Input configuration reloaded");
    }

    void OnDestroy()
    {
        if (Instance == this)
            Instance = null;
    }
}

编辑器可视化增强

4. 自定义 Inspector(配置编辑器)

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(InputConfiguration))]
public class InputConfigurationEditor : Editor
{
    private SerializedProperty bindingsProperty;

    private void OnEnable()
    {
        bindingsProperty = serializedObject.FindProperty("bindings");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.PropertyField(bindingsProperty);

        // 动态数组大小
        if (bindingsProperty.arraySize < 1)
        {
            if (GUILayout.Button("添加绑定"))
            {
                bindingsProperty.arraySize++;
            }
        }

        // 批量操作
        EditorGUILayout.Space();
        EditorGUILayout.LabelField("批量操作", EditorStyles.boldLabel);

        if (GUILayout.Button("验证所有绑定"))
        {
            ValidateAllBindings();
        }

        if (GUILayout.Button("清除无效绑定"))
        {
            RemoveInvalidBindings();
        }

        // 测试按钮
        EditorGUILayout.Space();
        if (GUILayout.Button("测试配置"))
        {
            TestConfiguration();
        }

        serializedObject.ApplyModifiedProperties();
    }

    private void ValidateAllBindings()
    {
        InputConfiguration config = target as InputConfiguration;
        int invalidCount = 0;

        for (int i = 0; i < config.bindings.Length; i++)
        {
            var binding = config.bindings[i];
            if (!binding.IsActive)
            {
                invalidCount++;
                Debug.LogWarning($"Binding {i} is empty", config);
            }

            // 检查重复按键
            for (int j = i + 1; j < config.bindings.Length; j++)
            {
                if (binding.Key == config.bindings[j].Key && binding.IsActive && config.bindings[j].IsActive)
                {
                    Debug.LogWarning($"Duplicate key binding: {binding.Key}", config);
                }
            }
        }

        Debug.Log($"Validation complete. Invalid bindings: {invalidCount}");
    }

    private void RemoveInvalidBindings()
    {
        InputConfiguration config = target as InputConfiguration;
        List<int> invalidIndices = new List<int>();

        for (int i = 0; i < config.bindings.Length; i++)
        {
            if (!config.bindings[i].IsActive)
            {
                invalidIndices.Add(i);
            }
        }

        // 从后往前删除,避免索引错乱
        for (int i = invalidIndices.Count - 1; i >= 0; i--)
        {
            bindingsProperty.DeleteArrayElementAtIndex(invalidIndices[i]);
        }

        serializedObject.ApplyModifiedProperties();
        Debug.Log($"Removed {invalidIndices.Count} invalid bindings");
    }

    private void TestConfiguration()
    {
        InputConfiguration config = target as InputConfiguration;
        InputManager manager = FindObjectOfType<InputManager>();

        if (manager != null)
        {
            manager.inputConfig = config;
            manager.ReloadConfiguration();
            Debug.Log("Configuration applied to InputManager");
        }
        else
        {
            Debug.LogError("InputManager not found in scene");
        }
    }

    // 自定义按键选择器
    public class KeySelectorPropertyDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.BeginProperty(position, label, property);

            // 显示当前按键
            string currentKey = property.enumDisplayNames[property.enumValueIndex];
            Rect keyRect = new Rect(position.x, position.y, position.width - 60, position.height);
            EditorGUI.LabelField(keyRect, label);

            // 按键选择按钮
            Rect buttonRect = new Rect(position.x + position.width - 50, position.y, 50, position.height);
            if (GUI.Button(buttonRect, "Pick"))
            {
                KeyPickerWindow.ShowWindow((Key key) => {
                    property.enumValueIndex = (int)key;
                    property.serializedObject.ApplyModifiedProperties();
                });
            }

            EditorGUI.EndProperty();
        }
    }
}
#endif

5. 按键选择器编辑器窗口

#if UNITY_EDITOR
public class KeyPickerWindow : EditorWindow
{
    private System.Action<Key> onKeySelected;
    private Key selectedKey = Key.Space;

    public static void ShowWindow(System.Action<Key> callback)
    {
        KeyPickerWindow window = GetWindow<KeyPickerWindow>("Key Picker");
        window.onKeySelected = callback;
        window.Show();
    }

    private void OnGUI()
    {
        GUILayout.Label("选择按键", EditorStyles.boldLabel);

        // 常用按键
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("Space", GUILayout.Width(80))) SelectKey(Key.Space);
        if (GUILayout.Button("Enter", GUILayout.Width(80))) SelectKey(Key.Return);
        if (GUILayout.Button("Esc", GUILayout.Width(80))) SelectKey(Key.Escape);
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("WASD", GUILayout.Width(80))) SelectKey(Key.W);
        if (GUILayout.Button("Arrow", GUILayout.Width(80))) SelectKey(Key.UpArrow);
        if (GUILayout.Button("Mouse0", GUILayout.Width(80))) SelectKey(Key.Mouse0);
        EditorGUILayout.EndHorizontal();

        // 字母键
        EditorGUILayout.LabelField("字母键:");
        for (int i = 0; i < 26; i++)
        {
            char letter = (char)('A' + i);
            Key key = (Key)System.Enum.Parse(typeof(Key), letter.ToString());

            if (GUILayout.Button(letter.ToString(), GUILayout.Width(40)))
            {
                SelectKey(key);
            }
        }

        GUILayout.Space(10);
        if (GUILayout.Button("确认选择", GUILayout.Height(30)))
        {
            onKeySelected?.Invoke(selectedKey);
            Close();
        }
    }

    private void SelectKey(Key key)
    {
        selectedKey = key;
    }
}
#endif

新输入系统集成

6. Input Action Asset 集成

using UnityEngine.InputSystem;
using UnityEngine;

public class NewInputSystemAdapter : MonoBehaviour
{
    [Header("Input Actions")]
    public InputActionAsset inputActions;

    private InputActionMap playerMap;
    private InputAction jumpAction;
    private InputAction fireAction;

    void Awake()
    {
        // 启用 Input Action Asset
        inputActions.Enable();

        playerMap = inputActions.FindActionMap("Player");
        jumpAction = playerMap.FindAction("Jump");
        fireAction = playerMap.FindAction("Fire");

        // 绑定回调
        jumpAction.performed += ctx => OnJumpPressed();
        fireAction.performed += ctx => OnFirePressed();
        fireAction.canceled += ctx => OnFireReleased();
    }

    private void OnJumpPressed()
    {
        // 触发配置中的 Jump 绑定
        TriggerActionEvent("Jump", InputConfiguration.InputBinding.InputEventType.Pressed);
    }

    private void OnFirePressed()
    {
        TriggerActionEvent("Fire", InputConfiguration.InputBinding.InputEventType.Pressed);
    }

    private void OnFireReleased()
    {
        TriggerActionEvent("Fire", InputConfiguration.InputBinding.InputEventType.Released);
    }

    private void TriggerActionEvent(string actionName, InputConfiguration.InputBinding.InputEventType eventType)
    {
        var manager = InputManager.Instance;
        if (manager != null)
        {
            // 转发到主管理器
            manager.TriggerBindingByActionName(actionName, eventType);
        }
    }

    void OnDestroy()
    {
        inputActions?.Disable();
    }
}

实际应用示例

7. 游戏控制示例

public class PlayerController : MonoBehaviour
{
    [Header("输入事件")]
    public UnityEvent onJump;
    public UnityEvent onShoot;
    public UnityEvent onMoveStart;
    public UnityEvent onMoveStop;

    void Start()
    {
        // 自动绑定到 InputManager 事件
        InputManager.Instance.inputConfig.bindings[0].onPressed.AddListener(() => {
            Debug.Log("Space pressed - Jump!");
            onJump?.Invoke();
        });

        // 长按射击
        InputManager.Instance.inputConfig.bindings[1].onHeld.AddListener(() => {
            Debug.Log("Mouse0 held - Continuous fire!");
            onShoot?.Invoke();
        });
    }
}

8. UI 交互示例

public class UIInputHandler : MonoBehaviour
{
    public void SetupUIBindings()
    {
        var config = Resources.Load<InputConfiguration>("UIInputConfig");
        InputManager.Instance.inputConfig = config;

        // ESC 打开菜单
        var escBinding = config.bindings.Find(b => b.Key == Key.Escape);
        escBinding.onPressed.AddListener(OpenMenu);

        // Tab 切换焦点
        var tabBinding = config.bindings.Find(b => b.Key == Key.Tab);
        tabBinding.onPressed.AddListener(SwitchFocus);
    }

    private void OpenMenu()
    {
        // 显示暂停菜单
        Time.timeScale = 0f;
        UIManager.Instance.ShowMenu();
    }
}

高级功能扩展

9. 输入组合与修饰键

[System.Serializable]
public class ComboInputBinding : InputConfiguration.InputBinding
{
    [Header("组合键")]
    public Key[] modifierKeys;  // Ctrl+Shift+Key 等
    public bool requireAllModifiers = true;

    public bool CheckModifiers()
    {
        int matchedCount = 0;

        foreach (var modKey in modifierKeys)
        {
            if (Input.GetKey(modKey))
                matchedCount++;
        }

        return requireAllModifiers ? matchedCount == modifierKeys.Length : matchedCount > 0;
    }
}

10. 运行时绑定系统

public class DynamicInputBinder : MonoBehaviour
{
    public void BindKeyToEvent(Key key, UnityEvent targetEvent, InputConfiguration.InputBinding.InputEventType eventType)
    {
        var manager = InputManager.Instance;
        var binding = manager.inputConfig.bindings.FirstOrDefault(b => b.Key == key);

        if (binding != null)
        {
            switch (eventType)
            {
                case InputConfiguration.InputBinding.InputEventType.Pressed:
                    binding.onPressed.AddListener(() => targetEvent?.Invoke());
                    break;
                case InputConfiguration.InputBinding.InputEventType.Released:
                    binding.onReleased.AddListener(() => targetEvent?.Invoke());
                    break;
            }

            manager.ReloadConfiguration();
        }
    }

    // 示例:动态绑定 R 键重载武器
    public void BindReloadWeapon()
    {
        BindKeyToEvent(Key.R, onReloadWeapon, InputConfiguration.InputBinding.InputEventType.Pressed);
    }
}

跨平台与优化

11. 平台特定映射

public static class PlatformInputMapper
{
    public static Key MapToPlatformKey(Key baseKey, RuntimePlatform platform)
    {
        switch (platform)
        {
            case RuntimePlatform.Android:
            case RuntimePlatform.IPhonePlayer:
                // 移动端映射到触摸区域
                return MapToTouchKey(baseKey);
            case RuntimePlatform.WindowsPlayer:
                // PC 标准按键
                return baseKey;
            default:
                return baseKey;
        }
    }

    private static Key MapToTouchKey(Key key)
    {
        return key switch
        {
            Key.Space => Key.Touch0,  // 跳跃按钮
            Key.Mouse0 => Key.Touch1, // 射击按钮
            _ => Key.None
        };
    }
}

12. 性能优化配置

public class OptimizedInputManager : InputManager
{
    [Header("性能优化")]
    public float updateInterval = 0.016f;  // 60FPS
    private float lastUpdateTime;

    void Update()
    {
        if (Time.time - lastUpdateTime < updateInterval)
            return;

        lastUpdateTime = Time.time;
        HandleOptimizedInput();
    }

    private void HandleOptimizedInput()
    {
        // 只检查活跃绑定,减少遍历
        foreach (var binding in activeBindings)
        {
            if (ShouldProcessBinding(binding))
            {
                ProcessInputEvent(binding, 
                    Input.GetKeyDown(binding.Key),
                    Input.GetKey(binding.Key),
                    Input.GetKeyUp(binding.Key));
            }
        }
    }

    private bool ShouldProcessBinding(InputConfiguration.InputBinding binding)
    {
        // 跳过无事件绑定的按键
        return binding.onPressed != null || binding.onHeld != null || binding.onReleased != null;
    }
}

使用工作流与最佳实践

13. 完整使用流程

  1. 创建配置:右键 → Create → Input Manager → Input Configuration
  2. 编辑绑定:Inspector 中添加按键绑定,配置事件类型和 UnityEvent
  3. 场景集成:添加 InputManager 组件,分配配置
  4. 事件绑定:在 UnityEvent 中拖入目标方法(无代码)
  5. 测试与调试:启用 Debug 模式,实时查看输入状态
  6. 热重载:编辑配置后调用 ReloadConfiguration()

14. 配置示例(Inspector)

InputConfiguration:
├── Binding 0:
│   ├── Key: Space
│   ├── Event Type: Pressed
│   └── OnPressed: [PlayerController.Jump()]
├── Binding 1:
│   ├── Key: Mouse0
│   ├── Event Type: Held
│   ├── Hold Duration: 0.5s
│   └── OnHeld: [WeaponController.Fire()]
└── Binding 2:
    ├── Key: R
    ├── Event Type: Pressed
    └── OnPressed: [Inventory.ReloadWeapon()]

调试与工具

15. 输入调试面板

#if UNITY_EDITOR
[CustomEditor(typeof(InputManager))]
public class InputManagerEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();

        InputManager manager = (InputManager)target;

        EditorGUILayout.Space();
        EditorGUILayout.LabelField("调试工具", EditorStyles.boldLabel);

        if (GUILayout.Button("打印所有绑定"))
        {
            foreach (var binding in manager.inputConfig.bindings)
            {
                Debug.Log($"Key: {binding.Key}, Active: {binding.IsActive}");
            }
        }

        if (GUILayout.Button("测试所有按键"))
        {
            SimulateAllKeys(manager);
        }
    }

    private void SimulateAllKeys(InputManager manager)
    {
        foreach (var kvp in manager.keyBindings)
        {
            var binding = kvp.Value;
            binding.onPressed?.Invoke();
            Debug.Log($"Simulated press: {kvp.Key}");
        }
    }
}
#endif

此输入管理器提供完整的可视化配置方案,支持多种输入事件和平台。通过 ScriptableObject 和 UnityEvent 实现零代码绑定,编辑器扩展增强开发体验。如需特定输入类型(如手柄、触摸)或高级组合键支持,请提供更多需求!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注