Unity を触り始めると、つい何でもかんでも Update() に書きたくなりますよね。移動処理、ジャンプ、アニメーション、エフェクト管理、UI 更新…全部ひとつのスクリプトに押し込んでしまうと、あとから「ダッシュを追加したい」「入力方法を変えたい」といった時に地獄を見ます。

特に「ダッシュ」「回避」「特殊入力(2度押し、長押しなど)」は、1つの巨大なプレイヤースクリプトにベタ書きされがちです。そうすると、入力処理と移動処理とステータス管理が絡み合って、どこを触ればいいのか分からなくなってしまいます。

そこで今回は、「同じ方向キーを素早く2回押した時だけダッシュする」機能を、ひとつの責務に絞ったコンポーネントとして切り出します。その名も 「DoubleTapDash」コンポーネント です。

【Unity】2度押しで気持ちよく加速!「DoubleTapDash」コンポーネント

このコンポーネントは、

  • 左右(または任意の方向)の入力を監視
  • 同じ方向が短時間に2回押されたら「ダッシュ状態」にする
  • ダッシュ中は移動速度をブーストし、一定時間で元に戻す

という、「2度押し検出+ダッシュ制御」だけ を担当します。
移動そのものは Rigidbody を使ったシンプルな処理に分離し、「入力の解釈」と「移動」を分けて考えられる構成にしています。


フルコード:DoubleTapDash.cs


using UnityEngine;
using UnityEngine.InputSystem; // 新Input System用

/// <summary>  
/// 2度押しダッシュを制御するコンポーネント  
/// ・水平方向(左右)の入力を監視  
/// ・同じ方向が素早く2回押されたらダッシュ  
/// ・ダッシュ中は移動速度をブースト  
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class DoubleTapDash : MonoBehaviour
{
    // ====== 設定パラメータ(インスペクターから調整) ======

    [Header("Input Settings")]
    [SerializeField]
    private string horizontalActionName = "Move"; 
    // Input Actions の Action 名(Vector2 など)
    // 例:PlayerInput に "Move" (Vector2) がある前提で、
    //     x 成分を左右入力として扱います。

    [Header("Dash Detection")]
    [SerializeField, Tooltip("2度押しと判定する最大時間(秒)")]
    private float doubleTapThreshold = 0.25f;

    [SerializeField, Tooltip("ダッシュ可能な最小入力値(方向判定用)")]
    private float directionDeadZone = 0.5f;

    [Header("Dash Movement")]
    [SerializeField, Tooltip("通常時の移動速度")]
    private float walkSpeed = 4f;

    [SerializeField, Tooltip("ダッシュ中の移動速度")]
    private float dashSpeed = 8f;

    [SerializeField, Tooltip("ダッシュが続く時間(秒)")]
    private float dashDuration = 0.3f;

    [SerializeField, Tooltip("地面方向のレイヤーマスク(任意)")]
    private LayerMask groundLayer = ~0; // すべてのレイヤー

    [SerializeField, Tooltip("接地判定用のレイ長さ")]
    private float groundCheckDistance = 0.1f;

    [Header("Debug")]
    [SerializeField, Tooltip("デバッグログを出すかどうか")]
    private bool enableDebugLog = false;

    // ====== 内部状態 ======

    private Rigidbody rb;

    // 現在の水平方向入力(-1 ~ 1)
    private float currentHorizontalInput;

    // 直近の「1回目のタップ」があった方向(-1 or 1)
    private int lastTapDirection = 0;

    // 1回目のタップ時間
    private float lastTapTime = -999f;

    // ダッシュ状態管理
    private bool isDashing = false;
    private float dashEndTime = 0f;

    // 接地判定用
    private bool isGrounded = true;

    // ====== Unity ライフサイクル ======

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // 物理挙動の基本設定(任意)
        rb.constraints = RigidbodyConstraints.FreezeRotation;
    }

    private void Update()
    {
        // 入力の取得(新Input System前提)
        ReadHorizontalInput();

        // 接地判定(ジャンプなどと組み合わせる場合に備えて用意)
        CheckGrounded();

        // 2度押し検出とダッシュ状態更新
        UpdateDashState();
    }

    private void FixedUpdate()
    {
        // 物理ベースの移動処理
        ApplyMovement();
    }

    // ====== 入力処理 ======

    /// <summary>
    /// PlayerInput + InputAction から水平方向入力を取得します。
    /// </summary>
    private void ReadHorizontalInput()
    {
        // PlayerInput コンポーネントから現在のアクション値を取得
        var playerInput = GetComponent<PlayerInput>();
        if (playerInput == null)
        {
            // PlayerInput がない場合は早期リターン
            currentHorizontalInput = 0f;
            return;
        }

        // 指定した Action 名から Vector2 を取得し、x 成分を使用
        InputAction moveAction = playerInput.actions[horizontalActionName];

        if (moveAction == null)
        {
            currentHorizontalInput = 0f;
            return;
        }

        Vector2 moveValue = moveAction.ReadValue<Vector2>();
        currentHorizontalInput = moveValue.x;
    }

    // ====== ダッシュ検出と状態管理 ======

    /// <summary>
    /// 2度押し判定とダッシュ状態の更新を行います。
    /// </summary>
    private void UpdateDashState()
    {
        // 入力方向を -1 / 0 / 1 に丸める
        int inputDirection = 0;
        if (currentHorizontalInput > directionDeadZone)
        {
            inputDirection = 1;
        }
        else if (currentHorizontalInput < -directionDeadZone)
        {
            inputDirection = -1;
        }

        // ダッシュ中の時間管理
        if (isDashing && Time.time >= dashEndTime)
        {
            if (enableDebugLog)
            {
                Debug.Log("ダッシュ終了");
            }
            isDashing = false;
        }

        // 入力がゼロの場合は、タップ判定だけリセット気味に扱う
        if (inputDirection == 0)
        {
            return;
        }

        // 「入力が入った瞬間」を検出するために、前フレーム状態を管理してもよいが、
        // 今回は「方向が変わったタイミング」を1回目のタップとみなす簡易実装にします。

        // 1回目のタップがまだない、または方向が変わった
        if (lastTapDirection == 0 || inputDirection != lastTapDirection)
        {
            lastTapDirection = inputDirection;
            lastTapTime = Time.time;

            if (enableDebugLog)
            {
                Debug.Log($"1回目のタップ方向: {lastTapDirection}, 時刻: {lastTapTime}");
            }

            return;
        }

        // 同じ方向が続いている場合、時間差をチェックして 2度押し判定
        float timeSinceLastTap = Time.time - lastTapTime;
        if (timeSinceLastTap <= doubleTapThreshold)
        {
            // 2度押し成功 → ダッシュ開始
            StartDash(inputDirection);
        }

        // 2度押し判定が済んだので、次の 1回目として時間を更新
        lastTapTime = Time.time;
    }

    /// <summary>
    /// ダッシュ状態を開始します。
    /// </summary>
    /// <param name="direction">ダッシュ方向(-1 or 1)</param>
    private void StartDash(int direction)
    {
        isDashing = true;
        dashEndTime = Time.time + dashDuration;

        if (enableDebugLog)
        {
            Debug.Log($"ダッシュ開始! 方向: {direction}, 終了予定時刻: {dashEndTime}");
        }
    }

    // ====== 移動処理 ======

    /// <summary>
    /// Rigidbody による移動を適用します。
    /// ダッシュ中かどうかで速度を切り替えます。
    /// </summary>
    private void ApplyMovement()
    {
        float targetSpeed = isDashing ? dashSpeed : walkSpeed;

        // 現在の速度を取得し、水平方向だけ書き換える
        Vector3 velocity = rb.velocity;
        velocity.x = currentHorizontalInput * targetSpeed;

        rb.velocity = velocity;
    }

    // ====== 接地判定(任意) ======

    /// <summary>
    /// 簡易的な接地判定。必要に応じて使ってください。
    /// 今回のダッシュ処理では必須ではありませんが、
    /// 「空中ではダッシュ無効」などに拡張しやすくするために用意しています。
    /// </summary>
    private void CheckGrounded()
    {
        Ray ray = new Ray(transform.position, Vector3.down);
        isGrounded = Physics.Raycast(ray, groundCheckDistance, groundLayer);
    }

    // ====== 公開用プロパティ ======

    /// <summary>
    /// 現在ダッシュ中かどうかを外部から参照できます。
    /// (アニメーション制御などに利用)  
    /// </summary>
    public bool IsDashing => isDashing;

    /// <summary>
    /// 現在の入力方向(-1 ~ 1)  
    /// </summary>
    public float CurrentHorizontalInput => currentHorizontalInput;
}

使い方の手順

ここでは、2D横スクロールのプレイヤー を例に、DoubleTapDash コンポーネントの導入手順を説明します。

  1. プレイヤー用の GameObject を用意
    • ヒエラルキーで Right Click > 3D Object > Capsule などでプレイヤーの見た目を作成
    • Rigidbody コンポーネントを追加(Use Gravity ON, Constraints で X/Z 回転をFreeze)
    • PlayerInput コンポーネントを追加
  2. Input Actions(新Input System)の設定
    • Input System を有効化していない場合は、Project Settings > Player > Active Input Handling を「Input System Package」に設定
    • プロジェクト内で Input Actions アセットを作成し、Player マップに Move という Vector2 Action を追加
    • Move に対して、Keyboard の WASDArrow Keys などをバインド
    • PlayerInput の Actions にそのアセットを割り当て、Default MapPlayer に設定
    • DoubleTapDash の horizontalActionName"Move" にしておく(デフォルト値が “Move” なのでそのままでOK)
  3. DoubleTapDash コンポーネントをアタッチ
    • 上記の C# ファイルを DoubleTapDash.cs として Assets フォルダに保存
    • プレイヤーの GameObject を選択し、Add Component から DoubleTapDash を追加
    • walkSpeeddashSpeeddashDurationdoubleTapThreshold などを好みに合わせて調整
    • テスト時に挙動を確認したい場合は enableDebugLog にチェックを入れてログを表示
  4. シーンでテスト
    • 地面用に Plane などを配置し、Collider を付けておく
    • プレイヤーを地面の上に配置
    • 再生して、右キーを素早く2回押す → 右方向へダッシュ左キーを素早く2回押す → 左方向へダッシュ になるか確認

同じ仕組みで、

  • 敵キャラの「2度押しで突進攻撃」
  • 動く床の「2度押しで一気に加速」

などにも応用できます。入力元を AI やスクリプトからの値に差し替えれば、プレイヤー以外にも簡単に流用できますね。


メリットと応用

この DoubleTapDash コンポーネントを使うメリットは、主に次の3つです。

  • 責務がはっきり分かれる
    「2度押しの検出」と「ダッシュ中の速度変更」に責務を絞っているので、プレイヤーのメインスクリプトを肥大化させずに済みます。
  • プレハブ単位で挙動を差し替えやすい
    プレイヤーAは doubleTapThreshold = 0.2、プレイヤーBは 0.35 など、キャラごとの操作感 をプレハブのインスペクターだけで変えられます。
  • レベルデザインが柔軟になる
    ステージによって「このレベルはダッシュ禁止」「このレベルはダッシュ時間を長くする」などを、シーン内のプレイヤーのパラメータ変更だけで実現できます。スクリプトの中身をいじらずにバランス調整ができるのは大きなメリットですね。

さらに、IsDashing プロパティを外部から読めるようにしているので、

  • ダッシュ中だけアニメーションを変える
  • ダッシュ中だけエフェクト(残像やパーティクル)を出す
  • ダッシュ中だけスタミナを消費する

といった拡張も、別コンポーネントとして簡単に追加できます。

改造案:空中ではダッシュを禁止する

例えば「地上でだけダッシュできるようにしたい」場合は、StartDash の前に接地判定を挟むのがシンプルです。以下のようなメソッドを追加し、StartDash を呼ぶ前に使うとよいでしょう。


/// <summary>
/// 接地している時だけダッシュを許可するヘルパー関数の例。
/// </summary>
private void TryStartDash(int direction)
{
    // 空中ではダッシュ禁止にしたい場合
    if (!isGrounded)
    {
        if (enableDebugLog)
        {
            Debug.Log("空中のためダッシュ不可");
        }
        return;
    }

    StartDash(direction);
}

この場合は、UpdateDashState() 内の StartDash(inputDirection);TryStartDash(inputDirection); に差し替えるだけでOKです。
こうやって小さな関数を追加していくと、機能ごとに責務が分かれた読みやすいコードになっていきます。