Godot で「ダッシュ」実装しようとすると、ついプレイヤー用の巨大なスクリプトに全部を詰め込みがちですよね。
入力処理、移動、アニメーション、当たり判定…そこに「2度押しダッシュ」までねじ込むと、_physics_process() が if 文の塊になりがちです。

さらに、敵キャラや別のプレイヤーにも同じダッシュ挙動を入れたい時、
「Player.gd を継承して…」「DashPlayer.gd を作って…」と、継承ツリーがどんどん深くなっていきます。

そこで今回は、どのキャラクターにもポン付けできる「2度押しダッシュ専用コンポーネント」を作ってみましょう。
入力の2度押し判定と、ダッシュ中かどうかの状態管理だけをこのコンポーネントに任せ、
実際の移動処理は「ホスト側(プレイヤーなど)」が読むだけ、という分離構成にします。

【Godot 4】2度押しでスッと加速!「DoubleTapDash」コンポーネント

このコンポーネントは:

  • 左右 or 上下の同方向キーを素早く2回押したら「ダッシュ開始」
  • ダッシュ中かどうか、どの方向にダッシュしているかを外部に提供
  • ダッシュ開始/終了のシグナルも発行

という、入力~状態管理までを一手に引き受けます。
ホスト側は「通常速度か、ダッシュ速度か」を見るだけでOK、というシンプルな責務分担ですね。


フルコード:DoubleTapDash.gd


extends Node
class_name DoubleTapDash
## 2度押しダッシュ検出コンポーネント
##
## ・入力の2度押し判定を行い、ダッシュ状態を管理する
## ・実際の移動(velocity の更新)はホスト側(プレイヤーなど)が行う
##
## 想定:
## - 左右移動: "ui_left" / "ui_right"
## - 上下移動: "ui_up" / "ui_down"
## (InputMap で別のアクション名にしている場合は、export で差し替え可能)

signal dash_started(direction: Vector2)
signal dash_ended()

## --- 設定パラメータ(インスペクタから調整) ---

@export_category("Input")
## 2度押し対象のアクション名(水平)
@export var action_left: StringName = &"ui_left"
@export var action_right: StringName = &"ui_right"
## 2度押し対象のアクション名(垂直)
@export var action_up: StringName = &"ui_up"
@export var action_down: StringName = &"ui_down"

@export_category("Dash Timing")
## 1回目の入力から、2回目の入力まで許される最大時間(秒)
## これを短くすると「素早く2回押し」しか反応しなくなる
@export_range(0.05, 0.6, 0.01) var double_tap_max_interval: float = 0.25

## ダッシュ継続時間(秒)
@export_range(0.05, 2.0, 0.05) var dash_duration: float = 0.3

@export_category("Dash Behavior")
## ダッシュ方向を「押したキー方向」に限定するか
## true: 左右・上下の4方向のみ
## false: 斜め入力も許可(例:左+上を同時に2回)
@export var four_way_only: bool = true

## ダッシュ中の速度倍率(ホスト側で利用するための目安値)
## 実際の速度計算はホストスクリプトで行う想定
@export_range(1.1, 5.0, 0.1) var dash_speed_multiplier: float = 2.0

## 同時に別方向のダッシュを開始するのを許可するか
## false の場合、既にダッシュ中なら新しいダッシュは無視
@export var allow_dash_override: bool = false


## --- 内部状態 ---

var _last_tap_time_left: float = -1000.0
var _last_tap_time_right: float = -1000.0
var _last_tap_time_up: float = -1000.0
var _last_tap_time_down: float = -1000.0

var _dash_timer: float = 0.0
var _is_dashing: bool = false
var _dash_direction: Vector2 = Vector2.ZERO


func _ready() -> void:
    # 特に処理は不要だが、将来の拡張に備えておく
    pass


func _process(delta: float) -> void:
    _update_dash_state(delta)
    _check_double_tap_input()


## ダッシュ状態の更新(時間経過で終了させる)
func _update_dash_state(delta: float) -> void:
    if not _is_dashing:
        return

    _dash_timer -= delta
    if _dash_timer <= 0.0:
        _is_dashing = false
        _dash_direction = Vector2.ZERO
        dash_ended.emit()


## 入力を監視して2度押しを検出
func _check_double_tap_input() -> void:
    # 入力イベントは _input でも取れるが、
    # 「フレーム単位で押された瞬間だけを見る」ために is_action_just_pressed を使う。
    var now := Time.get_ticks_msec() / 1000.0

    # 左
    if Input.is_action_just_pressed(action_left):
        if now - _last_tap_time_left <= double_tap_max_interval:
            _request_dash(Vector2.LEFT)
        _last_tap_time_left = now

    # 右
    if Input.is_action_just_pressed(action_right):
        if now - _last_tap_time_right <= double_tap_max_interval:
            _request_dash(Vector2.RIGHT)
        _last_tap_time_right = now

    # 上
    if Input.is_action_just_pressed(action_up):
        if now - _last_tap_time_up <= double_tap_max_interval:
            _request_dash(Vector2.UP)
        _last_tap_time_up = now

    # 下
    if Input.is_action_just_pressed(action_down):
        if now - _last_tap_time_down <= double_tap_max_interval:
            _request_dash(Vector2.DOWN)
        _last_tap_time_down = now

    # four_way_only == false の場合の「斜め2度押し」をサポートしたいなら、
    # ここに複合入力の処理を追加するのもアリ。


## ダッシュ開始要求を処理
func _request_dash(direction: Vector2) -> void:
    if direction == Vector2.ZERO:
        return

    # 既にダッシュ中で、上書き禁止なら無視
    if _is_dashing and not allow_dash_override:
        return

    # four_way_only の場合は、方向を4方向に丸める
    if four_way_only:
        direction = _to_four_way(direction)

    _is_dashing = true
    _dash_timer = dash_duration
    _dash_direction = direction.normalized()
    dash_started.emit(_dash_direction)


## 任意のベクトルを「4方向」に丸める
func _to_four_way(dir: Vector2) -> Vector2:
    var result := Vector2.ZERO
    if absf(dir.x) > absf(dir.y):
        result.x = signf(dir.x)
    else:
        result.y = signf(dir.y)
    return result


## --- 外部から参照するためのAPI ---


## ダッシュ中かどうか
func is_dashing() -> bool:
    return _is_dashing


## 現在のダッシュ方向(ダッシュ中でない場合は Vector2.ZERO)
func get_dash_direction() -> Vector2:
    return _dash_direction


## 移動速度を決めるための倍率(目安)
## 例: base_speed * get_current_speed_multiplier()
func get_current_speed_multiplier() -> float:
    if _is_dashing:
        return dash_speed_multiplier
    return 1.0

使い方の手順

ここでは、横スクロールのプレイヤーに「左右2度押しでダッシュ」を付ける例で説明します。

シーン構成例(Player)

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DoubleTapDash (Node)  ← このコンポーネントをアタッチ

手順①:スクリプトをプロジェクトに追加

  1. res://components/DoubleTapDash.gd など、好きな場所に上記コードを保存します。
  2. Godot エディタで開くと、DoubleTapDash がスクリプトクラスとして認識されます。

手順②:Player シーンにコンポーネントを追加

  1. Player シーンを開きます。(CharacterBody2D ベースを想定)
  2. Player の子として Node を追加し、名前を DoubleTapDash にします。
  3. その Node に先ほどの DoubleTapDash.gd スクリプトをアタッチします。
  4. インスペクタで double_tap_max_intervaldash_duration をお好みで調整しましょう。

手順③:Player 側の移動スクリプトから利用する

プレイヤー側では、「通常移動」と「ダッシュ中の移動」で速度を切り替えるだけです。
DoubleTapDash は入力の2度押し検出と状態管理だけを担当します。


# Player.gd (例)
extends CharacterBody2D

@export var move_speed: float = 200.0

var _dash: DoubleTapDash


func _ready() -> void:
    # 子ノードから DoubleTapDash を取得
    _dash = $DoubleTapDash as DoubleTapDash

    # シグナルに接続して、アニメーションなどに使うこともできる
    _dash.dash_started.connect(_on_dash_started)
    _dash.dash_ended.connect(_on_dash_ended)


func _physics_process(delta: float) -> void:
    var input_dir := Vector2.ZERO
    input_dir.x = Input.get_axis("ui_left", "ui_right")
    input_dir.y = Input.get_axis("ui_up", "ui_down")

    # 基本の移動方向(正規化)
    if input_dir.length() > 1.0:
        input_dir = input_dir.normalized()

    # DoubleTapDash から現在の倍率をもらう
    var speed_multiplier := 1.0
    if _dash:
        speed_multiplier = _dash.get_current_speed_multiplier()

    var final_speed := move_speed * speed_multiplier

    velocity = input_dir * final_speed
    move_and_slide()


func _on_dash_started(direction: Vector2) -> void:
    # ここでアニメーションやエフェクトを開始すると気持ちいい
    # 例: アニメーションプレイヤーで "dash" を再生
    # $AnimationPlayer.play("dash")
    print("Dash started! dir = ", direction)


func _on_dash_ended() -> void:
    # ダッシュ終了時の処理(アニメーションを戻すなど)
    # $AnimationPlayer.play("idle")
    print("Dash ended")

このように、プレイヤー側は「倍率を掛けるだけ」でダッシュが成立します。
ダッシュ判定ロジックは全部 DoubleTapDash に押し込めているので、
別のキャラにも同じコンポーネントをポン付けするだけで再利用できます。

手順④:敵や動く床にも流用する

例えば「2度押しで急加速する敵」や、「プレイヤーの入力で加速する動く床」にも同じコンポーネントを使えます。

敵キャラのシーン構成例
DashEnemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DoubleTapDash (Node)

敵側のスクリプトで、プレイヤーと同じように get_current_speed_multiplier() を参照すれば、
「プレイヤーと同じダッシュ仕様」を簡単に共有できます。入力は AI 用のカスタムアクションにしても構いません。


メリットと応用

  • プレイヤーのスクリプトがスリム
    入力の2度押し判定ロジックを全部コンポーネントに追い出せるので、Player.gd は「移動」と「アニメーション」に集中できます。
  • シーン構造が浅いのに機能はリッチ
    「ダッシュ付きプレイヤー」「ダッシュ付き敵」「ダッシュ付き動く床」など、同じコンポーネントを複数のシーンにそのままアタッチできます。
  • 継承ツリーからの解放
    DashPlayer.gd / SuperDashPlayer.gd のような継承地獄ではなく、
    「ダッシュしたいシーンに DoubleTapDash を付けるだけ」という合成(Composition)志向の設計になります。
  • テストがしやすい
    ダッシュ判定だけを単体でテストしやすくなります。将来、判定ロジックを変える時も、ホスト側のコードにはほぼ手を入れずに済みます。

さらに、ダッシュの仕様を変えたくなった時(例: 無敵時間を付ける、スタミナを消費するなど)も、
このコンポーネントを改造するだけで、全キャラに一括反映できるのが大きなメリットですね。

改造案:スタミナ制限付きダッシュにする

例えば「スタミナが一定以上ある時だけダッシュできる」ようにするには、
以下のような関数を追加して、外部からスタミナ値を教えてもらう方式がシンプルです。


# DoubleTapDash.gd の一部に追加する例

@export_category("Stamina (Optional)")
@export var require_stamina: bool = false
@export var stamina_cost_per_dash: float = 20.0

var _current_stamina: float = 100.0
var _min_stamina_to_dash: float = 20.0


## ホスト側から現在のスタミナ値を通知してもらう
func update_stamina(value: float) -> void:
    _current_stamina = value


func _request_dash(direction: Vector2) -> void:
    if direction == Vector2.ZERO:
        return

    if _is_dashing and not allow_dash_override:
        return

    # スタミナチェック
    if require_stamina and _current_stamina < _min_stamina_to_dash:
        return

    if four_way_only:
        direction = _to_four_way(direction)

    _is_dashing = true
    _dash_timer = dash_duration
    _dash_direction = direction.normalized()
    dash_started.emit(_dash_direction)

    # ダッシュ開始時にスタミナを消費したい場合は、
    # ホスト側に「消費してね」とシグナルを飛ばすのもアリ。

ホスト側(プレイヤーなど)で update_stamina() を毎フレーム呼んであげれば、
「スタミナ制限付き2度押しダッシュ」があっさり実現できます。

こんな感じで、機能を追加したくなったらコンポーネントを育てていくと、
プロジェクト全体が「継承より合成」寄りの設計に寄っていきます。
ぜひ自分用のコンポーネントライブラリとして、DoubleTapDash を育ててみてください。