关于 unity-input-correctness
面向 Unity 新 Input System 的正确性模式技能,供使用该系统的开发者参考。内容聚焦常见 API 使用、输入生命周期、设备分配与重绑定问题,并提供规避模式。
主要功能
- 01区分触发与持续输入读取方式
- 02正确切换和启用 Action Map
- 03区分 Processor 与 Interaction
- 04处理 InputValue 生命周期
- 05支持多来源输入与设备分配
- 06保存重绑定并显示按键提示
适用场景
- 01排查按住按钮时动作只触发一次
- 02避免切换 Action Map 后回调失效
- 03实现多触控或本地多人输入
- 04显示当前控制方案的按键提示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-input-correctness安装后在 Claude Code 中输入/unity-input-correctness确认已加载。
附带文件references/rebinding-and-multiplayer.md
详细介绍
输入系统(新)——正确性模式
前置技能:
unity-input(Input System API、操作、绑定、PlayerInput 组件)
这些模式针对最常见的 Input System 错误:操作类型与读取方法不匹配、混用新旧 API、重新绑定丢失,以及多人设备分配处理不当。
模式:读取输入——triggered、IsPressed 与 WasPressedThisFrame
何时:在运行时读取按钮/操作状态
错误(Claude 默认):
// Using .triggered for continuous input (only fires once per press)
if (fireAction.triggered)
rb.AddForce(Vector3.forward * force); // Only fires one frame, not while held
// Using .IsPressed() for one-shot actions (fires every frame while held)
if (jumpAction.IsPressed())
Jump(); // Jumps every frame the button is held!
正确:
// One-shot actions (jump, interact, fire single bullet):
if (jumpAction.WasPressedThisFrame()) // True for exactly ONE frame
Jump();
// Or use .triggered (same as WasPressedThisFrame for Button actions with default interaction)
if (jumpAction.triggered)
Jump();
// Continuous actions (sprint, aim, hold to charge):
if (sprintAction.IsPressed()) // True every frame while held
moveSpeed = sprintSpeed;
// Value reading (stick, mouse delta):
Vector2 moveInput = moveAction.ReadValue<Vector2>(); // Continuous value
注意:.triggered 遵循交互(Hold、Tap 等),会在交互完成时触发。.WasPressedThisFrame() 无论是否设置交互,都会在原始按下时触发。只要按下程度超过按下阈值,.IsPressed() 就会在每一帧返回 true。对于未设置交互的 Button 类型操作,.triggered == .WasPressedThisFrame()。对于 Value 类型操作,当值从零变为非零时,.triggered 会触发。
模式:切换操作映射
何时:在操作映射之间切换(例如 Gameplay -> UI -> Vehicle)
错误(Claude 默认):
// Forgetting that SwitchCurrentActionMap disables the previous map
playerInput.SwitchCurrentActionMap("UI");
// All "Gameplay" actions are now DISABLED -- callbacks won't fire
// If you cached Gameplay actions, they silently stop working
正确:
// Option 1: Via PlayerInput (handles enable/disable automatically)
playerInput.SwitchCurrentActionMap("UI");
// Previous map disabled, new map enabled
// Option 2: Manual enable/disable (more control)
gameplayActions.Disable();
uiActions.Enable();
// Option 3: Keep both maps active simultaneously
// (useful for universal actions like Pause)
gameplayActions.Enable();
pauseActions.Enable(); // Both active at once
注意:使用 PlayerInput.SwitchCurrentActionMap 时,先前的映射会被完全禁用。先前映射中缓存的任何 InputAction 引用都会停止触发回调,直到重新启用。如果需要某些操作(如 Pause)在所有映射中都可用,请将它们放在一个保持启用的独立映射中,或使用手动启用/禁用的方式。
模式:混淆处理器与交互
何时:应用死区或修改输入值
错误(Claude 默认):
// Adding a deadzone as an Interaction (Interactions modify TIMING, not values)
// In .inputactions: Action > Interactions > "Deadzone" -- this doesn't exist as an interaction
正确:
// Deadzones are PROCESSORS -- they modify the input VALUE
// Set in .inputactions: Binding > Processors > "Stick Deadzone" or "Axis Deadzone"
// Processors modify the value stream: Raw Input -> Processor Chain -> Final Value
// Common processors:
// StickDeadzone -- applies radial deadzone to Vector2 (sticks)
// AxisDeadzone -- applies linear deadzone to float (triggers)
// Normalize -- normalizes Vector2 to 0-1 range
// Invert -- negates the value
// Scale -- multiplies by a factor
// Clamp -- clamps to min/max range
// Runtime processor override (if needed):
moveAction.ApplyBindingOverride(new InputBinding { overrideProcessors = "StickDeadzone(min=0.2,max=0.9)" });
注意:处理器会转换值(死区、规范化、缩放、反转)。交互会改变 started/performed/canceled 的触发时机(Press、Hold、Tap、SlowTap、MultiTap)。混淆两者会导致死区无效(缺少处理器),或回调时机错误(添加了不必要的交互)。
模式:SendMessages/BroadcastMessages 中 InputValue 的生命周期
何时:在 SendMessages 或 BroadcastMessages 行为模式下使用 PlayerInput
错误(Claude 默认):
private InputValue _cachedInput; // Storing the reference
void OnMove(InputValue value)
{
_cachedInput = value; // WRONG: InputValue is pooled and recycled
}
void Update()
{
Vector2 dir = _cachedInput.Get<Vector2>(); // May return stale or corrupt data
}
正确:
private Vector2 _moveInput;
void OnMove(InputValue value)
{
// Copy the value immediately -- InputValue is only valid during the callback
_moveInput = value.Get<Vector2>();
}
void Update()
{
transform.Translate(_moveInput * speed * Time.deltaTime);
}
注意:InputValue 是一个会在回调之间重复使用的包装器。其内部数据仅在回调调用期间有效。始终在回调中使用 .Get<T>() 复制数据,并保存结果。这适用于 SendMessages 和 BroadcastMessages 模式。UnityEvents 和 C# Events 模式不使用 InputValue——它们传递 InputAction.CallbackContext,后者也有相同的生命周期限制。
模式:多来源输入的 PassThrough 与 Value
何时:处理来自多个同时来源的输入(多点触控、多个游戏手柄)
错误(Claude 默认):
// Using "Value" action type for multi-touch
// Value type performs disambiguation -- picks the input with highest magnitude
// You only see ONE touch, even if multiple fingers are on screen
正确:
// Use "PassThrough" action type for all-source input
// PassThrough does NOT disambiguate -- every input source triggers the action
// In .inputactions file: Set Action Type = "Pass Through"
// This is essential for:
// - Multi-touch (each finger fires separately)
// - Multiple gamepads sending the same action
// - Combining keyboard + mouse simultaneously
// Read which device triggered it:
void OnAction(InputAction.CallbackContext ctx)
{
var device = ctx.control.device;
var value = ctx.ReadValue<float>();
}
注意:Button:按下/释放时触发,返回浮点值 0 或 1。Value:值变化时触发,并选择幅度最大的来源(消歧)。PassThrough:每个来源发生的每次变化都会触发,不进行消歧。对于大多数游戏输入,Value 是正确的选择。只有在需要按设备或手指分别跟踪时,才使用 PassThrough。
模式:操作的启用/禁用范围
何时:启用/禁用单个操作或整个操作映射
错误(Claude 默认):
// Enabling an action without enabling its map
fireAction.Enable(); // Works, BUT...
// If the map was disabled, this implicitly enables JUST this action
// Other actions in the same map remain disabled
正确:
// Preferred: Enable/disable at the MAP level
playerActions.Enable(); // Enables all actions in the map
playerActions.Disable(); // Disables all actions
// Individual action enable/disable (advanced use only):
fireAction.Enable(); // Enables this action even if map is disabled
fireAction.Disable(); // Disables only this action
// Check state:
bool mapEnabled = playerActions.enabled;
bool actionEnabled = fireAction.enabled;
注意:操作可以在其所属映射处于“禁用”状态时保持启用——该操作仍然有效。但这会造成令人困惑的状态:map.enabled 返回 false,而 action.enabled 返回 true。最佳做法:始终在映射级别启用/禁用。仅在特殊情况下按操作启用/禁用,例如在重新装填时暂时禁用开火操作。
模式:设备特定按钮提示
适用场景:向玩家显示控制提示(例如“按 X 进行交互”)
错误(Claude 默认):
// Hardcoded button names
promptText.text = "Press A to Jump";
// Wrong on keyboard (should be "Space"), PS5 (should be "Cross"), etc.
正确:
// Get the display name for the current binding
InputAction jumpAction = inputActions.FindAction("Jump");
// Get display string for the active control scheme
string displayName = jumpAction.GetBindingDisplayString(
InputBinding.DisplayStringOptions.DontOmitDevice);
promptText.text = $"Press {displayName} to Jump";
// For a specific control scheme:
int bindingIndex = jumpAction.GetBindingIndex(
InputBinding.MaskByGroup("Gamepad"));
if (bindingIndex >= 0)
{
string gamepadPrompt = jumpAction.GetBindingDisplayString(bindingIndex);
// Returns "Button South" or device-specific name
}
注意:GetBindingDisplayString() 会返回人类可读的名称。不带参数时,它会返回第一个绑定对应的字符串。使用绑定掩码或索引来指定特定的控制方案。若要完整支持图标,需要自定义 InputBindingComposite 或资源,将控制路径映射到精灵图/图标引用——Unity 未提供内置的图标映射功能。
模式:本地多人游戏设备分配
适用场景:在同一台机器上支持多名玩家使用各自的控制器
错误(Claude 默认):
// Both players reading from the same static device reference
Vector2 p1Move = Gamepad.current.leftStick.ReadValue();
Vector2 p2Move = Gamepad.current.leftStick.ReadValue(); // Same gamepad!
正确:
// Use PlayerInputManager for automatic device assignment
// 1. Add PlayerInputManager component to a manager object
// 2. Set Join Behavior (e.g., JoinPlayersWhenButtonIsPressed)
// 3. Set Player Prefab (must have PlayerInput component)
// PlayerInputManager automatically assigns unique devices to each player
// In the player script:
public class PlayerController : MonoBehaviour
{
private PlayerInput _playerInput;
private InputAction _moveAction;
void Awake()
{
_playerInput = GetComponent<PlayerInput>();
_moveAction = _playerInput.actions["Move"];
}
void Update()
{
// Each PlayerInput instance reads from its ASSIGNED device only
Vector2 move = _moveAction.ReadValue<Vector2>();
transform.Translate(move * speed * Time.deltaTime);
}
}
// Listen for join/leave events:
void OnEnable()
{
PlayerInputManager.instance.onPlayerJoined += OnPlayerJoined;
PlayerInputManager.instance.onPlayerLeft += OnPlayerLeft;
}
注意:Gamepad.current 返回最近使用的游戏手柄,而不是特定玩家的游戏手柄。多人游戏中,始终通过管理设备分配的 PlayerInput 组件读取输入。PlayerInputManager.instance.maxPlayerCount 用于限制玩家人数。分屏通过分配 PlayerInput.camera 处理——每名玩家都会获得一台具有不同视口矩形的摄像机。
模式:控制方案自动切换
适用场景:玩家在游戏过程中切换键盘和游戏手柄
错误(Claude 默认):
// Assuming the control scheme is fixed after startup
// UI shows keyboard prompts even after player picks up a gamepad
正确:
public class ControlSchemeHandler : MonoBehaviour
{
private PlayerInput _playerInput;
void OnEnable()
{
_playerInput = GetComponent<PlayerInput>();
_playerInput.controlsChangedEvent.AddListener(OnControlsChanged);
// Initialize with current scheme
UpdatePrompts(_playerInput.currentControlScheme);
}
void OnDisable()
{
_playerInput.controlsChangedEvent.RemoveListener(OnControlsChanged);
}
void OnControlsChanged(PlayerInput input)
{
UpdatePrompts(input.currentControlScheme);
}
void UpdatePrompts(string schemeName)
{
bool isGamepad = schemeName == "Gamepad";
// Update UI prompts, button icons, etc.
promptIcon.sprite = isGamepad ? gamepadSprite : keyboardSprite;
}
}
注意:PlayerInput 检测到来自不同设备类型的输入时,会自动切换控制方案。每次切换都会触发 controlsChangedEvent。currentControlScheme 返回与你的 .inputactions 控制方案名称匹配的名称字符串。切换会在下一次输入事件发生时进行,而不是在设备连接时立即进行。请在同时插入两种设备的情况下进行测试。
反模式速查
| 反模式 | 问题 | 修复方法 |
|---|---|---|
将 Input.GetKey 与新 Input System 混用 | 新旧 API 冲突;可能需要同时启用两种后端 | 完全迁移到新 Input System;移除 using UnityEngine.Input |
未调用 action.Enable() | 操作不会执行,且不会报错 | 读取前启用操作映射或单个操作 |
使用错误的类型 T 调用 .ReadValue<T>() | 会静默返回默认值 | 让 T 与操作的 Control Type 匹配(摇杆用 Vector2,按钮用 float) |
忘记释放 PerformInteractiveRebinding | 内存泄漏 | .Start() 完成或取消后,务必调用 .Dispose() |
使用旧版 OnGUI 处理输入 | IMGUI 与 Input System 混用 | 使用 UI Toolkit 或 Input System 回调 |
| 未保存重新绑定的覆盖设置 | 玩家重启后会丢失自定义绑定 | 使用 SaveBindingOverridesAsJson 保存,并在 Awake 中加载 |
相关技能
- unity-input —— Input System API 参考、操作类型、绑定语法、设备访问
- unity-ui —— UI Toolkit 输入处理、导航事件
- unity-multiplayer —— Netcode 输入权限、客户端预测