【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. 完整使用流程
- 创建配置:右键 → Create → Input Manager → Input Configuration
- 编辑绑定:Inspector 中添加按键绑定,配置事件类型和 UnityEvent
- 场景集成:添加 InputManager 组件,分配配置
- 事件绑定:在 UnityEvent 中拖入目标方法(无代码)
- 测试与调试:启用 Debug 模式,实时查看输入状态
- 热重载:编辑配置后调用 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 实现零代码绑定,编辑器扩展增强开发体验。如需特定输入类型(如手柄、触摸)或高级组合键支持,请提供更多需求!