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) ← このコンポーネントをアタッチ
手順①:スクリプトをプロジェクトに追加
res://components/DoubleTapDash.gdなど、好きな場所に上記コードを保存します。- Godot エディタで開くと、
DoubleTapDashがスクリプトクラスとして認識されます。
手順②:Player シーンにコンポーネントを追加
- Player シーンを開きます。(
CharacterBody2Dベースを想定) - Player の子として
Nodeを追加し、名前をDoubleTapDashにします。 - その Node に先ほどの
DoubleTapDash.gdスクリプトをアタッチします。 - インスペクタで
double_tap_max_intervalやdash_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 を育ててみてください。
