mcpField
U
unity-input-correctness

unity-input-correctness

github.com/nice-wolf-studio开发4071 次安装6 周前更新
代码仓库

关于 unity-input-correctness

面向 Unity 新 Input System 的正确性模式技能,供使用该系统的开发者参考。内容聚焦常见 API 使用、输入生命周期、设备分配与重绑定问题,并提供规避模式。

主要功能

  1. 01区分触发与持续输入读取方式
  2. 02正确切换和启用 Action Map
  3. 03区分 Processor 与 Interaction
  4. 04处理 InputValue 生命周期
  5. 05支持多来源输入与设备分配
  6. 06保存重绑定并显示按键提示

适用场景

  1. 01排查按住按钮时动作只触发一次
  2. 02避免切换 Action Map 后回调失效
  3. 03实现多触控或本地多人输入
  4. 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 输入权限、客户端预测

其他资源

评论

支持 Markdown