Godot 4 でアクションゲームを書いていると、「ダッシュ」や「回避」を作りたくなる場面って多いですよね。
素直に CharacterBody2D / CharacterBody3D を継承して、_physics_process() の中で速度をいじって…とやりがちですが、
- プレイヤーと敵でダッシュ処理をコピペしてしまう
- 「通常移動」と「ダッシュ移動」が同じスクリプトにベタっと混ざってカオス
- 別のキャラだけ「瞬間移動ダッシュ」にしたいのに、共通基底クラスをいじるハメになる
みたいな「継承ベタ書きあるある」にハマりがちです。
そこで今回は、「進行方向に一定距離だけ 一瞬でワープ する」タイプのダッシュを、
どんなキャラにもポン付けできる コンポーネント として切り出してみましょう。
名前は TeleportDash。
キャラの移動ロジックとは独立させて、「今の向きにシュッと飛ぶ」機能だけを担当させます。
プレイヤーにも敵にも、動く床にも、好きなノードにアタッチするだけで瞬間移動ダッシュを追加できます。
【Godot 4】一瞬でスッと消えるダッシュ!「TeleportDash」コンポーネント
以下がフルコードです。
2D 用ですが、3D 版にしたい場合のポイントも後半で触れます。
## TeleportDash.gd
## 任意のノードにアタッチして「瞬間移動ダッシュ」を追加するコンポーネント
## 対象: 主に Node2D / CharacterBody2D / RigidBody2D など「position を持つノード」
class_name TeleportDash
extends Node
## --- 設定パラメータ(インスペクタで編集可能) ---
@export_range(0.0, 2000.0, 1.0, "or_greater")
var dash_distance: float = 120.0
## 1 回のダッシュで進む距離(ピクセル)。
## 例: 120 なら、今向いている方向に 120px 先へ瞬間移動。
@export_range(0.0, 5.0, 0.05, "or_greater")
var cooldown: float = 0.4
## ダッシュのクールタイム(秒)。
## 0 にすると連打可能だが、ゲームバランスが崩れやすいので注意。
@export var max_charges: int = 0
## 0 の場合は無制限にダッシュ可能。
## 1 以上を指定すると、その回数だけダッシュできる「回数制ダッシュ」になる。
@export var consume_on_start: bool = true
## true: ダッシュ開始時にチャージを消費する。
## false: ダッシュ終了時に消費する(途中キャンセルなどを実装したい場合向け)。
@export var require_grounded: bool = false
## true: 「is_on_floor() == true」のときだけダッシュを許可する。
## 主に CharacterBody2D と一緒に使うとき用。
@export var use_physics_safe_teleport: bool = true
## true: CharacterBody2D の「set_global_position()」ではなく
## 「global_position = ...」を直接書き換えるだけにする。
## 物理挙動との兼ね合いで挙動が変わるので、環境に応じて切り替えましょう。
@export var debug_draw: bool = false
## true にすると、エディタ実行時にダッシュ先の線を簡易表示します。
## レベルデザイン中の距離調整に便利です。
@export_group("入力")
@export var auto_listen_input: bool = true
## true: このコンポーネント自身が Input を監視してダッシュする。
## false: コードから「dash(direction)」を直接呼び出す想定(AI やスクリプト制御用)。
@export var dash_action_name: StringName = &"dash"
## InputMap に設定するアクション名。
## デフォルトでは「dash」アクションが押されたときに発動。
@export_group("向きの解決方法")
@export_enum("Use_Velocity", "Use_Facing_Vector", "Use_Input_Vector")
var direction_mode: int = 0
## 0: Use_Velocity - 親ノードの velocity から向きを推定(CharacterBody2D 前提)
## 1: Use_Facing_Vector - 親ノードの「右向き」や「forward」など、任意のベクトルを使う
## 2: Use_Input_Vector - 入力(左右・上下)から向きを決定
@export var facing_vector: Vector2 = Vector2.RIGHT
## direction_mode == Use_Facing_Vector のときに使う「基準向き」。
## 例: スプライトが右向きで描かれているなら Vector2.RIGHT のままで OK。
@export var input_axis_left: StringName = &"ui_left"
@export var input_axis_right: StringName = &"ui_right"
@export var input_axis_up: StringName = &"ui_up"
@export var input_axis_down: StringName = &"ui_down"
## direction_mode == Use_Input_Vector のときに使うアクション名。
## Godot デフォルトのカーソルキー入力を前提にしています。
@export_group("コリジョン&安全性")
@export var use_raycast_check: bool = true
## true: ダッシュ先に壁がある場合は、手前までしか移動しない。
## false: 壁の中でも容赦なくワープする(危険だが、テレポート系ギミックには便利)。
@export var collision_mask: int = 1
## RayCast2D の collision_mask 相当。
## どのレイヤーのコリジョンを「障害物」として扱うか。
@export var collision_margin: float = 2.0
## 壁にめり込まないように、ヒット位置から少し手前にずらす距離(ピクセル)。
@export_group("エフェクトフック")
@export var emit_signal_on_dash: bool = true
## true: ダッシュ時に「dash_started」「dash_finished」シグナルを発火する。
## パーティクルやサウンドを別ノードで管理したいときに便利。
@export var disable_movement_during_dash: bool = false
## true: ダッシュ中は、親ノードの移動系コンポーネントを無効化する想定。
## 実際の無効化処理は、このコンポーネントからシグナルを受けて行う。
## --- シグナル ---
signal dash_started(global_from: Vector2, global_to: Vector2)
signal dash_finished(global_from: Vector2, global_to: Vector2)
## --- 内部状態 ---
var _last_dash_time: float = -9999.0
var _current_charges: int = 0
var _is_dashing: bool = false
## キャッシュ
var _parent_2d: Node2D
var _parent_body: CharacterBody2D
func _ready() -> void:
_parent_2d = get_parent() as Node2D
_parent_body = get_parent() as CharacterBody2D
if _parent_2d == null:
push_warning("TeleportDash: 親ノードが Node2D ではありません。2D シーンでの使用を想定しています。")
# チャージ初期化
if max_charges > 0:
_current_charges = max_charges
else:
_current_charges = 0 # 無制限モード
func _process(delta: float) -> void:
if auto_listen_input and not Engine.is_editor_hint():
_handle_input()
if debug_draw and Engine.is_editor_hint() == false:
_debug_draw_line()
func _handle_input() -> void:
if Input.is_action_just_pressed(dash_action_name):
var dir := _resolve_direction()
dash(dir)
## 外部からも呼べる API
## direction がゼロベクトルの場合は自動で向きを解決する
func dash(direction: Vector2 = Vector2.ZERO) -> void:
if not _can_dash():
return
if direction == Vector2.ZERO:
direction = _resolve_direction()
if direction == Vector2.ZERO:
# どうしても向きが決められないときは何もしない
return
direction = direction.normalized()
var from_pos := _get_global_position()
var target_pos := from_pos + direction * dash_distance
# コリジョンチェック(RayCast 的な動き)
if use_raycast_check and _parent_2d != null:
var space_state := _parent_2d.get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(from_pos, target_pos, collision_mask)
var result := space_state.intersect_ray(query)
if result:
var hit_position: Vector2 = result.position
var safe_dir := (hit_position - from_pos).normalized()
target_pos = hit_position - safe_dir * collision_margin
# チャージ消費タイミング
if max_charges > 0 and consume_on_start:
_current_charges = max(_current_charges - 1, 0)
_is_dashing = true
var before := _get_global_position()
_set_global_position(target_pos)
_last_dash_time = Time.get_ticks_msec() / 1000.0
if emit_signal_on_dash:
dash_started.emit(before, target_pos)
# (今回は瞬間移動なので、すぐに終了扱いにしてしまう)
if max_charges > 0 and not consume_on_start:
_current_charges = max(_current_charges - 1, 0)
_is_dashing = false
if emit_signal_on_dash:
dash_finished.emit(before, target_pos)
func _can_dash() -> bool:
# クールタイムチェック
var now := Time.get_ticks_msec() / 1000.0
if now - _last_dash_time < cooldown:
return false
# チャージチェック
if max_charges > 0 and _current_charges <= 0:
return false
# 接地チェック
if require_grounded and _parent_body != null:
if not _parent_body.is_on_floor():
return false
return true
## 向きの決定ロジック
func _resolve_direction() -> Vector2:
match direction_mode:
0: # Use_Velocity
if _parent_body != null:
if _parent_body.velocity.length() > 0.01:
return _parent_body.velocity.normalized()
# 停止中は facing_vector を fallback として使う
return facing_vector.normalized()
else:
return facing_vector.normalized()
1: # Use_Facing_Vector
return facing_vector.normalized()
2: # Use_Input_Vector
var input_vec := Vector2.ZERO
if Input.is_action_pressed(input_axis_left):
input_vec.x -= 1.0
if Input.is_action_pressed(input_axis_right):
input_vec.x += 1.0
if Input.is_action_pressed(input_axis_up):
input_vec.y -= 1.0
if Input.is_action_pressed(input_axis_down):
input_vec.y += 1.0
if input_vec == Vector2.ZERO:
# 入力が無いときは facing_vector を fallback として使う
return facing_vector.normalized()
return input_vec.normalized()
_:
return facing_vector.normalized()
## 親ノードのグローバル座標を取得
func _get_global_position() -> Vector2:
if _parent_2d != null:
return _parent_2d.global_position
return Vector2.ZERO
## 親ノードのグローバル座標を設定
func _set_global_position(pos: Vector2) -> void:
if _parent_2d == null:
return
if use_physics_safe_teleport and _parent_body != null:
# CharacterBody2D の場合、瞬時移動でも物理エンジンと整合が取れるように
# グローバル座標を直接書き換えるだけにする
_parent_body.global_position = pos
else:
_parent_2d.global_position = pos
## デバッグ描画(シンプルなライン)
func _debug_draw_line() -> void:
if _parent_2d == null:
return
var dir := _resolve_direction()
if dir == Vector2.ZERO:
return
var from_pos := _get_global_position()
var to_pos := from_pos + dir.normalized() * dash_distance
var world := get_tree().root.get_viewport().get_canvas_item()
if world:
var ci := world
# 単純にラインを描きたいだけなので、draw_line を呼ぶための簡易 CanvasItem を使う
# 実際には、専用の DebugDraw ノードを用意した方がきれいです
※ _debug_draw_line() は環境によってはうまく描画されないことがあるので、
本番では別途 DebugDraw ノードなどに差し替えるのがおすすめです。
(デバッグ用途なので、ここでは割愛します)
使い方の手順
ここでは代表的な 3 パターンで使い方を見ていきます。
- プレイヤーに「瞬間移動ダッシュ」を追加する
- 敵 AI に「一定間隔でプレイヤー方向へ瞬間移動」を追加する
- 動く床に「一定方向へワープ移動」を追加する
① プレイヤーに TeleportDash をアタッチする
まずは 2D プラットフォーマーのプレイヤーを想定します。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── TeleportDash (Node)
手順:
TeleportDash.gdをres://components/TeleportDash.gdなどに保存。- プレイヤーシーンを開き、
Player(CharacterBody2D)の子としてNodeを追加。 - その
NodeにTeleportDash.gdをアタッチ。 - インスペクタで以下を設定:
dash_distance: 120〜200 あたりから調整cooldown: 0.3〜0.5 秒程度direction_mode:Use_Input_VectorかUse_Velocitydash_action_name:"dash"- InputMap に
dashアクションを追加(Shift / Space など)
これだけで、プレイヤーは「今入力している方向」または「移動している方向」に向かって、
一瞬で dash_distance 分だけワープするようになります。
地上でだけダッシュさせたい場合は require_grounded = true にしておきましょう。
② 敵 AI に「プレイヤー方向への瞬間移動」を追加する
今度は、敵が一定間隔でプレイヤーに向かって瞬間移動してくるパターンです。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── TeleportDash (Node) └── EnemyAI (Node or Script)
EnemyAI.gd から TeleportDash を直接呼び出してみます。
## EnemyAI.gd
extends Node
@export var target: Node2D
@onready var teleport_dash: TeleportDash = $TeleportDash
@export_range(0.1, 10.0, 0.1)
var dash_interval: float = 1.5
var _timer: float = 0.0
func _process(delta: float) -> void:
if target == null or teleport_dash == null:
return
_timer += delta
if _timer >= dash_interval:
_timer = 0.0
var dir := (target.global_position - teleport_dash.get_parent().global_position).normalized()
teleport_dash.dash(dir)
このように、敵側は「進行方向の計算」だけに集中し、
実際のワープ処理は TeleportDash に丸投げできます。
同じ敵 AI を 3D に移植したいときも、「どこまでワープするか」のロジックは共通で使いまわせますね。
③ 動く床に「一定方向へのワープ」を追加する
最後に、トラップ的な「ワープ床」を作る例です。
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D └── TeleportDash (Node)
MovingPlatform.gd 側で、一定間隔で TeleportDash を発火します。
## MovingPlatform.gd
extends Node2D
@onready var teleport_dash: TeleportDash = $TeleportDash
@export var interval: float = 2.0
@export var move_direction: Vector2 = Vector2.RIGHT
var _timer: float = 0.0
func _process(delta: float) -> void:
if teleport_dash == null:
return
_timer += delta
if _timer >= interval:
_timer = 0.0
teleport_dash.dash(move_direction)
このプラットフォームは、interval ごとに move_direction 方向へ dash_distance 分だけ瞬間移動します。
「ワープする足場」「瞬間移動するリフト」といったギミックを簡単に量産できますね。
メリットと応用
TeleportDash をコンポーネントとして切り出すことで、
- プレイヤーの移動ロジックとダッシュロジックを完全に分離できる
- プレイヤー、敵、ギミックなど、どのノードにも同じダッシュ機能をポン付けできる
- クールタイムや距離、入力モードを インスペクタから個別に調整できる
- シグナルでエフェクトやサウンドを 別コンポーネントに分離できる
といったメリットがあります。
「ダッシュを実装したいからとりあえず Player スクリプトに書き足す」ではなく、
「ダッシュという機能を 1 つの部品として設計する」ことで、
プロジェクトが大きくなっても 見通しの良い構成 を保ちやすくなります。
また、TeleportDash は「瞬間移動」という性質上、
- 敵の奇襲(プレイヤーの背後にワープ)
- ボス戦のフェーズ移行時の大ジャンプ代わり
- パズルギミックとしての「ワープ床」「ワープブロック」
など、アクションゲーム以外にも応用しやすいです。
改造案:残像エフェクトをシグナルで追加する
最後に、「ダッシュした瞬間に残像を出す」改造案を 1 つ。
TeleportDash のシグナルを拾うだけで、エフェクト用コンポーネントを簡単に追加できます。
## DashTrail.gd
## TeleportDash の dash_started シグナルを受けて、残像を生成するコンポーネント
extends Node
@export var teleport_dash_path: NodePath = ^"../TeleportDash"
@export var ghost_scene: PackedScene # 残像用のシーン(Sprite2D + FadeOut など)
var _teleport_dash: TeleportDash
func _ready() -> void:
_teleport_dash = get_node_or_null(teleport_dash_path) as TeleportDash
if _teleport_dash:
_teleport_dash.dash_started.connect(_on_dash_started)
func _on_dash_started(from_pos: Vector2, to_pos: Vector2) -> void:
if ghost_scene == null:
return
# 開始地点と終了地点の両方に残像を出す例
_spawn_ghost(from_pos)
_spawn_ghost(to_pos)
func _spawn_ghost(pos: Vector2) -> void:
var ghost := ghost_scene.instantiate() as Node2D
if ghost == null:
return
get_tree().current_scene.add_child(ghost)
ghost.global_position = pos
このように、「瞬間移動する」というコアなロジックは TeleportDash に閉じ込めておき、
ビジュアルやサウンドは別コンポーネントで自由に差し替える、という構成にしておくと、
後からの改造・調整がとても楽になります。
継承でゴリゴリ書き足していくのではなく、
「瞬間移動したいノードに TeleportDash をアタッチするだけ」という発想で、
コンポーネント指向の Godot ライフを楽しんでいきましょう。
