【Unity】InputSystemを使って「溜め攻撃」を超シンプルに実装する方法

2024/04/052025/04/04ゲーム開発Unity

この記事では、Unityの新しい入力システム「Input System」を活用して、ゼルダの伝説の回転斬りのような「溜め攻撃」アクションを実装する方法を解説します。

Input Systemには長押しを検知する「Hold Interaction」もありますが、今回はあえてそれを使わず、ボタン入力の基本的なイベントである「押した瞬間 (started)」と「離した瞬間 (canceled)」の時刻差を利用するアプローチを紹介します。

従来のUpdate関数内で毎フレーム入力を監視する方法と比較して、Input Systemのイベント駆動型アプローチは、コードがシンプルになり、特に機能追加や変更に対する保守性が向上するメリットが期待できます。


この記事の内容

  1. 溜め攻撃の実装:Input System (Press) vs Update vs Hold Interaction
  2. Input SystemのインストールとInput Actionsアセットの作成
  3. Input Actions:左クリック(攻撃ボタン)アクションの定義
  4. スクリプト連携:PlayerInputとイベント処理スクリプト
  5. 溜め時間の計算ロジック:startedとcanceledの活用
  6. 中間演出の実装:コルーチンでInput Systemの弱点を補う
  7. まとめ:Pressイベント方式のメリット・デメリットと使い分け

溜め攻撃の実装:Input System (Press) vs Update vs Hold Interaction

溜め攻撃を実装する方法はいくつか考えられます。それぞれの特徴を見てみましょう。

  • 従来のUpdate関数による実装:
    • Update()内で毎フレーム入力をチェックし、ボタンが押されている時間を計測します。
    • 仕組みは直感的ですが、入力の種類や条件分岐が増えるとコードが複雑化しがちです。
  • Input System の Hold Interaction を使う実装:
    • Input ActionsアセットでInteractionを「Hold」に設定し、指定時間(Hold Time)長押しされるとperformedイベントが発生します。
    • 設定は簡単ですが、後述するように「短押し(通常攻撃)」と「長押し(溜め攻撃)」を同じボタンで使い分けたい場合に、実装がやや複雑になることがあります。
  • Input System の Press イベント (started/canceled) を使う実装(本記事のアプローチ):
    • ボタンが押された瞬間(started)と離された瞬間(canceled)のイベントを利用し、その間の時間を計測します。
    • イベント駆動でコードが整理されやすく、短押し・長押しの判定や溜め中の処理を柔軟に実装できます。

Q. なぜHold InteractionではなくPressイベント (started/canceled) を使うのか?

Hold Interactionは指定時間長押しされたことを検知するのに便利ですが、「**同じボタンで短押し(通常攻撃)と長押し(溜め攻撃)を使い分けたい**」場合や、「**溜めている最中に演出を入れたい**」場合には、少し扱いにくい側面があります。

  • 短押しと長押しの両立: Hold Interactionでは、長押しが成立してperformedが呼ばれる前に、短押し時の処理(通常攻撃など)をボタンを押した直後に実行させたい場合、工夫が必要です。例えば、「押した瞬間(started)に通常攻撃を出し、Holdが成立したらキャンセルする」といった制御や、別途短押し用のアクションを用意するなど、実装が複雑になりがちです。
  • 押下開始タイミングの活用: ボタンを押した瞬間にキャラクターが構えモーションに入ったり、溜めエフェクトを開始したりといった処理を即座に行いたい場合、Hold Interactionだけではタイミングを計りにくいことがあります。
  • 溜め中の制御: 溜めゲージを表示したり、一定時間ごとに溜めレベルが上がる演出を入れたりする場合、最終的にperformedイベントが発生するまで待つ必要があるHold Interactionでは、溜めている途中の細かな制御がしづらいです。

一方、本記事で紹介するPressイベント (started/canceled) を利用する方法では、

  • ボタンを押した瞬間のstartedイベントと、離した瞬間のcanceledイベントの時刻差を計測することで、**押下時間を正確に把握**できます。
  • これにより、「一定時間未満なら通常攻撃、一定時間以上なら溜め攻撃」といった判定をcanceledイベント内で簡単に行えます。
  • startedイベント発生時に溜め動作やエフェクトを開始し、canceledイベントで攻撃を発動、という流れを自然に実装できます。
  • (後述するコルーチンなどを組み合わせれば)溜めている最中の演出や状態変化も柔軟に組み込めます。

このように、短押し/長押しの判定や、溜め中の演出・状態管理を柔軟に行いたい場合には、started/canceledイベントを利用するアプローチが適していると言えます。


Input SystemのインストールとInput Actionsアセットの作成

まず、プロジェクトにInput Systemパッケージを導入し、入力を定義するInput Actionsアセットを作成します。

Input Systemパッケージのインストール

Unityエディタのメニューから操作します。

Package ManagerからInput Systemをインストールする手順
  1. Window > Package Manager を開きます。
  2. 左上のドロップダウンメニューで「Packages: Unity Registry」を選択します。
  3. リストから「Input System」を探し、「Install」ボタンをクリックします。
  4. インストール中にプロジェクト設定の変更を促すダイアログが出たら、「Yes」を選択してエディタを再起動します。

Input Actionsアセットファイルの作成

次に、入力アクションを定義するためのアセットファイルを作成します。

ProjectウィンドウでInput Actionsアセットを作成
  1. Projectウィンドウで右クリックし、Create > Input Actions を選択します。
  2. 作成されたアセットファイル(例: `PlayerInputActions.inputactions`)に分かりやすい名前を付けます。
  3. 作成したアセットファイルを選択し、Inspectorウィンドウで「Generate C# Class」にチェックを入れ、「Apply」ボタンを押します。
Generate C# Classにチェックを入れるとC#スクリプトが生成される

「Generate C# Class」にチェックを入れることで、このInput Actionsアセットに対応するC#クラスが自動生成され、スクリプトから入力イベントを扱いやすくなります。


Input Actions:左クリック(攻撃ボタン)アクションの定義

作成したInput Actionsアセットファイル(例: `PlayerInputActions.inputactions`)をダブルクリックして編集ウィンドウを開き、溜め攻撃に使用するアクションを定義します。

Input Actionsエディタで攻撃アクションを設定する
  1. Action Maps 列の「+」ボタンをクリックし、新しいAction Mapを作成します(例: `Gameplay`)。Action Mapは関連するアクションをまとめるグループです。
  2. Actions 列の「+」ボタンをクリックし、新しいActionを作成します(例: `AttackLeft`)。これが具体的な入力操作に対応します。
  3. 作成した`AttackLeft` Actionを選択し、右側のPropertiesパネルで以下を設定します。
    • Action Type:Button」を選択します。これは、押す/離すの単純な入力に適しています。
  4. `AttackLeft` Actionの下にある``を選択し、Propertiesパネルで以下を設定します。これが具体的な入力デバイスのボタンとの紐付け(Binding)です。
    • Path: プルダウンメニューから「Mouse」>「Left Button」を選択します。(ゲームパッドのボタンなどもここで設定可能)
  5. 編集が終わったら、ウィンドウ上部の「Save Asset」ボタンをクリックして変更を保存します。

これで、「マウスの左クリック」が`AttackLeft`という名前のアクションとして、スクリプトからイベントとして受け取れるようになりました。


スクリプト連携:PlayerInputとイベント処理スクリプト

定義したInput Actionsを実際にゲーム内で機能させるために、スクリプトとの連携を設定します。

1. シーン内に空のGameObjectを作成し、分かりやすい名前(例: `InputManager`)を付けます。

2. 作成した`InputManager` GameObjectに「Player Input」コンポーネントを追加します。

3. Player Inputコンポーネントの「Actions」フィールドに、先ほど作成したInput Actionsアセット(例: `PlayerInputActions.inputactions`)をドラッグ&ドロップで設定します。

4. 以下のC#スクリプト(例: `InputManager.cs`)を作成し、`InputManager` GameObjectにアタッチします。

using UnityEngine;
using UnityEngine.InputSystem;

// PlayerInputコンポーネントが必須であることを示す
[RequireComponent(typeof(PlayerInput))]
public class InputManager : MonoBehaviour
{
    // ボタンが押され始めた時刻を記録する変数
    private float buttonPressStartTime;
    // 溜め攻撃と判定する時間のしきい値(例: 1秒)
    private const float specialAttackThreshold = 1.0f;

    // PlayerInputコンポーネントから呼び出されるメソッド
    // メソッド名はInput Actionsで定義したアクション名(例: AttackLeft)に
    // "On" を付けたものにするか、後述のようにInspectorで手動設定する
    public void OnAttackLeft(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間 (started) の処理
        if (context.started)
        {
            Debug.Log("Attack button pressed (started)");
            // 押下開始時刻を記録
            buttonPressStartTime = Time.time;
            // ここで構えモーションや溜めエフェクト開始などの処理を入れることも可能
        }
        // ボタンが離された瞬間 (canceled) の処理
        else if (context.canceled)
        {
            Debug.Log("Attack button released (canceled)");
            // 押されていた時間を計算
            float pressDuration = Time.time - buttonPressStartTime;
            Debug.Log($"Press duration: {pressDuration} seconds");

            // 押下時間がしきい値を超えていたら溜め攻撃
            if (pressDuration > specialAttackThreshold)
            {
                PerformSpecialAttack(); // 溜め攻撃実行メソッド呼び出し
            }
            // しきい値未満なら通常攻撃
            else
            {
                PerformNormalAttack(); // 通常攻撃実行メソッド呼び出し
            }
        }
        // context.performed は Button タイプでは started とほぼ同じタイミングで呼ばれることが多い
        // Hold Interaction を使わない場合、主に started と canceled を使う
    }

    // 通常攻撃を実行する処理(中身は仮)
    private void PerformNormalAttack()
    {
        Debug.Log("Perform Normal Attack!");
        // ここに実際の通常攻撃ロジックを記述
    }

    // 溜め攻撃を実行する処理(中身は仮)
    private void PerformSpecialAttack()
    {
        Debug.Log("Perform Special Attack!");
        // ここに実際の溜め攻撃ロジックを記述
    }
}

5. `InputManager` GameObjectを選択し、InspectorウィンドウでPlayer Inputコンポーネントの「Behavior」を「Invoke Unity Events」に設定します。

6. 「Events」セクションが展開されるので、設定したAction Map名(例: `Gameplay`)を開き、その中のアクション名(例: `Attack Left`)に対応するイベント欄の「+」ボタンをクリックします。

7. イベント欄に`InputManager` GameObject自体をドラッグ&ドロップし、右側のドロップダウンメニューから「InputManager」>「OnAttackLeft (InputAction.CallbackContext)」を選択します。(スクリプトのメソッド名が `On[アクション名]` であれば自動で認識されることもあります)

これで、マウスの左ボタンがクリックされる(押される、または離される)たびに、`InputManager.cs`スクリプト内の`OnAttackLeft`メソッドが呼び出されるようになります。このイベント駆動の仕組みにより、`Update`関数を使うことなく入力処理を実現できます。


溜め時間の計算ロジック:startedとcanceledの活用

前述のスクリプト (`InputManager.cs`) 内の `OnAttackLeft` メソッドで行っている溜め時間の計算ロジックを詳しく見てみましょう。

  1. 押下開始 (context.started):
    • マウスの左ボタンが押された瞬間にこのブロックが実行されます。
    • 現在の時刻 (`Time.time`) を `buttonPressStartTime` 変数に記録します。これが溜め時間の計測開始点となります。
  2. 押下終了 (context.canceled):
    • 押されていた左ボタンが離された瞬間にこのブロックが実行されます。
    • 現在の時刻 (`Time.time`) から、記録しておいた押下開始時刻 (`buttonPressStartTime`) を引くことで、ボタンが押されていた時間 (`pressDuration`) を計算します。
    • 計算した `pressDuration` と、あらかじめ定義しておいた溜め攻撃のしきい値 (`specialAttackThreshold`) を比較します。
    • pressDuration がしきい値より長ければ `PerformSpecialAttack()` メソッドを、短ければ `PerformNormalAttack()` メソッドを呼び出します。

このように、Input Systemの `started` と `canceled` イベントを利用することで、ボタンが押されていた時間を正確に計測し、それに基づいて通常攻撃と溜め攻撃を振り分けることができます。`Update()` 関数内で毎フレーム時間を加算していく必要がないため、コードがシンプルになり、処理負荷も軽減される可能性があります。


中間演出の実装:コルーチンでInput Systemの弱点を補う

Input Systemのイベント駆動モデルは、「押した」「離した」といった瞬間のイベントを捉えるのは得意ですが、「**ボタンを押し続けている間の特定のタイミング**(例: 溜め攻撃が可能になる瞬間)」で何か処理を行いたい場合には、少し工夫が必要です。

例えば、「溜め時間がしきい値に達したらキャラクターを光らせる」「溜め完了のSEを鳴らす」といった演出を入れたい場合、`started` と `canceled` イベントだけでは、その「中間点」を直接検知できません。

この問題を解決する一般的な方法の一つが、Unityの「コルーチン (Coroutine)」を利用することです。ボタンが押された (`started`) 時点でコルーチンを開始し、一定時間(溜め攻撃のしきい値)が経過したら、溜め完了の合図(シグナル)を送る、という仕組みを作ります。

以下は、先ほどの `InputManager.cs` にコルーチンを追加し、溜め完了のシグナル(ここではDebugログ出力)を実装した例です。

using UnityEngine;
using UnityEngine.InputSystem;
using System.Collections; // コルーチンのために必要

[RequireComponent(typeof(PlayerInput))]
public class InputManager : MonoBehaviour
{
    private float buttonPressStartTime;
    private const float specialAttackThreshold = 1.0f;
    // 実行中のコルーチンを保持する変数
    private Coroutine chargeCheckCoroutine;
    // 溜め完了シグナルが送られたかどうかのフラグ
    private bool isChargeComplete = false;

    public void OnAttackLeft(InputAction.CallbackContext context)
    {
        if (context.started)
        {
            Debug.Log("Attack button pressed (started)");
            buttonPressStartTime = Time.time;
            isChargeComplete = false; // 溜め開始時にフラグをリセット

            // もし既にコルーチンが動いていたら停止する(連打対策)
            if (chargeCheckCoroutine != null)
            {
                StopCoroutine(chargeCheckCoroutine);
            }
            // 溜め時間監視コルーチンを開始
            chargeCheckCoroutine = StartCoroutine(ChargeTimerCoroutine());
        }
        else if (context.canceled)
        {
            Debug.Log("Attack button released (canceled)");
            // ボタンが離されたら、溜め時間監視コルーチンを停止
            if (chargeCheckCoroutine != null)
            {
                StopCoroutine(chargeCheckCoroutine);
                chargeCheckCoroutine = null; // 保持しているコルーチン参照をクリア
            }

            float pressDuration = Time.time - buttonPressStartTime;
            Debug.Log($"Press duration: {pressDuration} seconds");

            // 溜め完了フラグが立っていれば(=しきい値を超えていれば)溜め攻撃
            if (isChargeComplete) // または pressDuration > specialAttackThreshold でも判定可能
            {
                PerformSpecialAttack();
            }
            else
            {
                PerformNormalAttack();
            }

            // 攻撃実行後にフラグをリセット
            isChargeComplete = false;
        }
    }

    // 溜め時間を監視するコルーチン
    private IEnumerator ChargeTimerCoroutine()
    {
        // しきい値の時間だけ待機
        yield return new WaitForSeconds(specialAttackThreshold);

        // しきい値に到達したら(かつボタンがまだ押されている場合)
        // isChargeComplete フラグを立て、溜め完了の合図を送る
        // ※ context.ReadValue() > 0 などでボタンが押され続けているか確認する方がより厳密
        Debug.Log("Charge Complete threshold reached!");
        isChargeComplete = true;

        // ここで溜め完了エフェクト(光る、SE鳴らすなど)をトリガーする
        TriggerChargeCompleteEffect();

        chargeCheckCoroutine = null; // コルーチン終了
    }

    private void PerformNormalAttack()
    {
        Debug.Log("Perform Normal Attack!");
        // 通常攻撃ロジック
    }

    private void PerformSpecialAttack()
    {
        Debug.Log("Perform Special Attack!");
        // 溜め攻撃ロジック
    }

    private void TriggerChargeCompleteEffect()
    {
         Debug.Log("Play Charge Complete Effect!");
        // 溜め完了時の演出処理(エフェクト表示、SE再生など)
    }
}

このコードでは、ボタンが押されたら `ChargeTimerCoroutine` を開始し、`specialAttackThreshold` 秒後に `isChargeComplete` フラグを `true` にして、溜め完了演出メソッド `TriggerChargeCompleteEffect()` を呼び出します。ボタンが離された (`canceled`) 時点で、このフラグが `true` になっているかどうかで溜め攻撃か通常攻撃かを判断します。

このようにコルーチンを組み合わせることで、Input Systemのイベント駆動のメリットを活かしつつ、溜め攻撃における中間的なタイミングでの処理も実現できます。一見すると `Update` で実装するより複雑に感じるかもしれませんが、入力の種類が増えたり、他のアクションとの連携が必要になったりした場合、Input SystemのAction Mapやイベントによる責務分離がコード全体の整理に役立ちます。


まとめ:Pressイベント方式のメリット・デメリットと使い分け

UnityのInput SystemにおけるPressイベント (started / canceled) を利用することで、イベント駆動に基づいた溜め攻撃の実装が可能になることを見てきました。

メリット:

  • Update関数を使わずに済み、コードがイベント単位で整理されやすい。
  • 押下開始・終了のタイミングを正確に捉えられる。
  • 短押し/長押しの判定や、それに応じた処理の分岐が比較的容易。
  • Input Actionsアセットによる入力マッピング管理が直感的で、キーコンフィグなどへの拡張性が高い。

デメリット(考慮点):

  • 「押され続けている間の特定のタイミング」での処理(溜め完了演出など)には、コルーチンなどの補完的な仕組みが必要になる。
  • 単純な溜め時間計測だけなら、Update関数で実装する方がシンプルに感じる場合もある。

どちらを選ぶべきか?

最終的な実装方法は、プロジェクトの規模や要件、開発チームの好みによって異なります。

  • 小規模なプロジェクトやプロトタイプで、溜め攻撃以外の入力が少ない場合は、Update関数によるシンプルな実装でも十分かもしれません。
  • 中規模以上のプロジェクトで、多様な入力(ゲームパッド対応、キーコンフィグなど)や、他のアクションとの連携、将来的な拡張性・保守性を重視する場合は、Input SystemのPressイベント + コルーチン等による補完という組み合わせが有力な選択肢となるでしょう。Hold Interactionも選択肢ですが、本記事で解説したような柔軟性を求める場合はPressイベント方式が有利な場面があります。

Input Systemは学習コストが少しありますが、慣れれば強力な武器になります。ぜひ、ご自身のプロジェクトに合った方法で溜め攻撃の実装に挑戦してみてください。

この記事をシェアする