mcpField
U
unity-input-correctness

unity-input-correctness

github.com/nice-wolf-studio開発4071 回インストール6 週間前に更新
リポジトリ

unity-input-correctness について

Unity の新 Input System を使う開発者向けに、正しい実装パターンをまとめたスキルです。アクションの読み取り、入力値の有効期間、デバイス割り当て、リバインドの注意点を扱います。

主な機能

  1. 01単発入力と押し続ける入力の読み取りを選択
  2. 02アクションマップの切り替えと有効化
  3. 03プロセッサーとインタラクションを区別
  4. 04InputValue のコールバック内の有効期間に対応
  5. 05複数入力ソースとデバイス割り当てを扱う
  6. 06リバインドの保存とボタン表示

活用シーン

  1. 01押している間に一度しか動かないアクションを調査
  2. 02マップ切り替え後のコールバック停止を防止
  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

詳細

Input System(New)-- 正確性のパターン

前提スキル: unity-input(Input System API、アクション、バインディング、PlayerInput コンポーネント)

これらのパターンは、Input System でよく発生するバグを対象としています。アクションの種類に合わない読み取り方法、旧 API と新 API の混在、リバインドの消失、マルチプレイヤーでのデバイス割り当ての誤処理です。


パターン:入力の読み取り -- triggered と IsPressed と WasPressedThisFrame

WHEN:実行時にボタン/アクションの状態を読み取る場合

WRONG(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!

RIGHT:

// 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

GOTCHA:.triggered は Interactions(Hold、Tap など)に従い、インタラクションが完了したときに発火します。.WasPressedThisFrame() は、インタラクションに関係なく、生の押下を検出して発火します。.IsPressed() は、押下しきい値を超えて作動している間、毎フレーム true を返します。インタラクションのない Button 型アクションでは、.triggered == .WasPressedThisFrame() です。Value 型アクションでは、値がゼロから非ゼロに変化したときに .triggered が発火します。


パターン:アクションマップの切り替え

WHEN:アクションマップを切り替える場合(例:Gameplay -> UI -> Vehicle)

WRONG(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

RIGHT:

// 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

GOTCHA:PlayerInput.SwitchCurrentActionMap を使うと、切り替え前のマップは完全に無効になります。切り替え前のマップから取得してキャッシュした InputAction 参照は、再度有効にするまでコールバックを発火しなくなります。Pause など、すべてのマップで動作させる必要のあるアクションは、常に有効にしておく別のマップに配置するか、手動で有効/無効を切り替えてください。


パターン:プロセッサーとインタラクションの混同

WHEN:デッドゾーンを適用したり、入力値を変更したりする場合

WRONG(Claude のデフォルト):

// Adding a deadzone as an Interaction (Interactions modify TIMING, not values)
// In .inputactions: Action > Interactions > "Deadzone" -- this doesn't exist as an interaction

RIGHT:

// 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)" });

GOTCHA:プロセッサーは値を変換します(デッドゾーン、正規化、スケーリング、反転)。インタラクションは started/performed/canceled が発火するタイミングを変更します(Press、Hold、Tap、SlowTap、MultiTap)。これらを混同すると、デッドゾーンが適用されない(プロセッサーがない)か、コールバックのタイミングが適切でなくなる(不要なインタラクションを追加している)結果になります。


パターン:SendMessages/BroadcastMessages における InputValue の有効期間

WHEN:動作モードを SendMessages または BroadcastMessages にして PlayerInput を使用する場合

WRONG(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
}

RIGHT:

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);
}

GOTCHA:InputValue は、コールバック間で再利用されるラッパーです。内部データが有効なのは、コールバックの実行中のみです。コールバック内で .Get<T>() を使って必ずコピーし、その結果を保存してください。これは SendMessages モードと BroadcastMessages モードに適用されます。UnityEvents モードと C# Events モードでは InputValue は使われず、同じ有効期間の制約がある InputAction.CallbackContext が渡されます。


パターン:複数の入力ソースに対する PassThrough と Value

WHEN:複数の入力ソースから同時に入力を処理する場合(マルチタッチ、複数のゲームパッドなど)

WRONG(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

RIGHT:

// 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>();
}

GOTCHA:Button:押下/解放時に発火し、float の 0 または 1 を返します。Value:値が変化したときに発火し、最も大きな値のソースを選択します(曖昧さの解消)。PassThrough:曖昧さの解消を行わず、すべてのソースからの変化ごとに発火します。ほとんどのゲームプレイ入力では、Value が適切です。デバイスまたは指ごとの追跡が必要な場合にのみ PassThrough を使ってください。


パターン:アクションの有効化/無効化の範囲

WHEN:個々のアクションとアクションマップ全体を有効/無効にする場合

WRONG(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

RIGHT:

// 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;

GOTCHA:アクションを含むマップが「無効」でも、そのアクションを有効にすることはでき、アクション自体は引き続き動作します。ただし、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 の割り当てによって処理され、各プレイヤーには異なるビューポート rect のカメラが割り当てられます。


パターン:コントロールスキームの自動切り替え

タイミング:プレイヤーがゲーム中にキーボードとゲームパッドを切り替えるとき

誤り(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 と新 API が競合し、両方のバックエンドを有効にする必要が生じる場合がある新しい Input System に完全に移行し、using UnityEngine.Input を削除する
action.Enable() を呼び出さないアクションが何もせず、エラーも発生しない値を読み取る前に、アクションマップまたは個々のアクションを有効にする
.ReadValue<T>() を誤った型 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 の入力権限、クライアント予測

追加リソース

コメント