Godotでダッシュ演出を入れようとすると、けっこう面倒ですよね。
よくある実装パターンとしては…
- プレイヤーシーンを継承した「残像用プレイヤー」を別途作る
- ダッシュ処理の中に「残像生成ロジック」を直書きする
- アニメーションやスプライトの状態を都度コピーしてごにょごにょ…
こうなると、
- プレイヤーの実装を変えるたびに残像側もメンテが必要
- 敵や別キャラでも同じ演出を使いたいのに、コピペ地獄
- シーン階層がどんどん深くなって、何がどこで生成されているか分かりづらい
といった「継承+密結合」のつらみが出てきます。
そこで今回は、「ダッシュ残像」機能を 1 コンポーネントに閉じ込めて、
どんなキャラクターにもポン付けできる DashGhost コンポーネントを作ってみましょう。
【Godot 4】ダッシュが一気に“それっぽく”なる!「DashGhost」コンポーネント
この DashGhost コンポーネントは、
- ダッシュ中に一定間隔で「半透明の残像」を生成
- 元のスプライトのポーズ・フリップ・モジュレートをそのままコピー
- フェードアウトしながら自動で消える
という処理を、どのキャラにもアタッチするだけで実現します。
「ダッシュしているかどうか」は、親ノード側からフラグを渡すだけのシンプル設計です。
フルコード:DashGhost.gd
## DashGhost.gd
## ダッシュ中にスプライトの残像を生成するコンポーネント
## 任意のノードにアタッチして使うことを想定(親にSprite2D or AnimatedSprite2Dがある前提)
class_name DashGhost
extends Node
## --- 設定パラメータ(インスペクタから編集可能) ---
@export var enabled: bool = true:
set(value):
enabled = value
# 有効/無効を切り替えた瞬間にタイマーをリセットしておく
_time_accum = 0.0
## ダッシュ中に残像を生成する間隔(秒)
@export_range(0.01, 1.0, 0.01, "or_greater")
var spawn_interval: float = 0.06
## 残像が完全に消えるまでの時間(秒)
@export_range(0.05, 2.0, 0.05, "or_greater")
var ghost_lifetime: float = 0.4
## 残像の初期アルファ値(0.0 ~ 1.0)
@export_range(0.1, 1.0, 0.05)
var ghost_start_alpha: float = 0.7
## 残像の色を全体的に乗算する(未指定なら元スプライトの色をそのまま使用)
@export var ghost_tint: Color = Color(1, 1, 1, 1)
## 残像を生成する基準となるノードのパス
## 通常は Player / Enemy などの「キャラのルートノード」に向ける
@export_node_path("Node2D")
var source_root_path: NodePath
## 残像の見た目をコピーする元スプライトのパス
## Sprite2D or AnimatedSprite2D を想定
@export_node_path("Node2D")
var source_sprite_path: NodePath
## 残像を配置する親ノード(未設定なら source_root の親にぶら下げる)
@export_node_path("Node2D")
var ghost_parent_path: NodePath
## ダッシュ中かどうかを外部から制御するフラグ
## 親側のスクリプトから dash_ghost.is_dashing = true / false のように切り替える
var is_dashing: bool = false
## --- 内部用変数 ---
var _time_accum: float = 0.0
var _source_root: Node2D
var _source_sprite: Node2D
var _ghost_parent: Node2D
func _ready() -> void:
# 各参照ノードの解決
_resolve_nodes()
func _process(delta: float) -> void:
if not enabled:
return
if not is_instance_valid(_source_root) or not is_instance_valid(_source_sprite):
# 何らかの理由でノードが消えていたら再解決を試みる
_resolve_nodes()
if not is_instance_valid(_source_root) or not is_instance_valid(_source_sprite):
return
if not is_dashing:
# ダッシュしていない間はタイマーだけリセットしておく
_time_accum = 0.0
return
_time_accum += delta
if _time_accum >= spawn_interval:
_time_accum -= spawn_interval
_spawn_ghost()
## ノード参照を解決するヘルパー
func _resolve_nodes() -> void:
# source_root の解決
if source_root_path != NodePath(""):
_source_root = get_node_or_null(source_root_path)
else:
# 未指定なら、このコンポーネントの親を root とみなす
_source_root = get_parent() as Node2D
# source_sprite の解決
if source_sprite_path != NodePath(""):
_source_sprite = get_node_or_null(source_sprite_path)
else:
# 未指定なら、親の子ノードから Sprite2D / AnimatedSprite2D を探す
if _source_root:
for child in _source_root.get_children():
if child is Sprite2D or child is AnimatedSprite2D:
_source_sprite = child
break
# ghost_parent の解決
if ghost_parent_path != NodePath(""):
_ghost_parent = get_node_or_null(ghost_parent_path)
else:
# 未指定なら、source_root の親にぶら下げる(=キャラと同じ階層に並べる)
if _source_root and _source_root.get_parent() is Node2D:
_ghost_parent = _source_root.get_parent()
else:
_ghost_parent = _source_root
## 残像インスタンスを生成する
func _spawn_ghost() -> void:
if not is_instance_valid(_ghost_parent):
_ghost_parent = _source_root
if not is_instance_valid(_source_sprite) or not is_instance_valid(_source_root):
return
# Sprite2D / AnimatedSprite2D のどちらかに対応
if _source_sprite is Sprite2D:
_spawn_ghost_from_sprite2d(_source_sprite as Sprite2D)
elif _source_sprite is AnimatedSprite2D:
_spawn_ghost_from_animated_sprite2d(_source_sprite as AnimatedSprite2D)
func _spawn_ghost_from_sprite2d(src: Sprite2D) -> void:
var ghost := Sprite2D.new()
# テクスチャやフレームなどの見た目をコピー
ghost.texture = src.texture
ghost.centered = src.centered
ghost.offset = src.offset
ghost.hframes = src.hframes
ghost.vframes = src.vframes
ghost.frame = src.frame
ghost.flip_h = src.flip_h
ghost.flip_v = src.flip_v
ghost.region_enabled = src.region_enabled
ghost.region_rect = src.region_rect
# 位置・回転・スケールをキャラの root 基準でコピー
ghost.global_position = _source_root.global_position
ghost.global_rotation = _source_root.global_rotation
ghost.global_scale = _source_root.global_scale
# 色(モジュレート)をコピーして、アルファとティントを適用
var base_color := src.modulate
var color := base_color
# ghost_tint がデフォルト(1,1,1,1)でなければ乗算
color.r *= ghost_tint.r
color.g *= ghost_tint.g
color.b *= ghost_tint.b
color.a *= ghost_start_alpha
ghost.modulate = color
# 残像を親に追加
_ghost_parent.add_child(ghost)
# フェードアウト処理をアタッチ
_attach_fadeout(ghost)
func _spawn_ghost_from_animated_sprite2d(src: AnimatedSprite2D) -> void:
var ghost := Sprite2D.new()
# AnimatedSprite2D の現在のフレームをテクスチャとして取得
# Godot 4 では sprite_frames.get_frame_texture(animation, frame) で取得できる
if src.sprite_frames and src.sprite_frames.has_animation(src.animation):
var tex := src.sprite_frames.get_frame_texture(src.animation, src.frame)
ghost.texture = tex
ghost.centered = true # AnimatedSprite2D のデフォルトに合わせる
ghost.flip_h = src.flip_h
ghost.flip_v = src.flip_v
# 位置・回転・スケールをキャラの root 基準でコピー
ghost.global_position = _source_root.global_position
ghost.global_rotation = _source_root.global_rotation
ghost.global_scale = _source_root.global_scale
# 色(モジュレート)をコピーして、アルファとティントを適用
var base_color := src.modulate
var color := base_color
color.r *= ghost_tint.r
color.g *= ghost_tint.g
color.b *= ghost_tint.b
color.a *= ghost_start_alpha
ghost.modulate = color
_ghost_parent.add_child(ghost)
_attach_fadeout(ghost)
## 残像にフェードアウト処理を付与する
func _attach_fadeout(sprite: Sprite2D) -> void:
# Tween を使ってアルファを 0 まで下げ、その後 QueueFree する
var tween := create_tween()
tween.tween_property(
sprite,
"modulate:a",
0.0,
ghost_lifetime
).set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)
tween.tween_callback(Callable(sprite, "queue_free"))
使い方の手順
ここでは、典型的な「2D横スクロールのプレイヤー」にダッシュ残像を付ける例で説明します。
シーン構成例
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── DashGhost (Node)
手順①:コンポーネントスクリプトを用意する
上記の DashGhost.gd をプロジェクトに保存します。
例: res://components/DashGhost.gd
手順②:プレイヤーシーンにアタッチする
- Player シーンを開く
- Player (CharacterBody2D) の子として
Nodeを追加し、名前をDashGhostに変更 - その Node に
DashGhost.gdをアタッチ
このとき、インスペクタで以下のように設定しておくと分かりやすいです。
source_root_path:..(=親の Player)source_sprite_path:../Sprite2Dghost_parent_path: 空のまま(デフォルトで Player の親に配置される)
手順③:プレイヤー側から「ダッシュ中フラグ」を渡す
プレイヤーのスクリプトに「ダッシュ処理」がある前提で、
その状態を DashGhost に伝えるだけで OK です。
# Player.gd (例)
extends CharacterBody2D
@export var dash_speed: float = 600.0
@export var dash_time: float = 0.2
var _is_dashing: bool = false
var _dash_timer: float = 0.0
var _dash_ghost: DashGhost
func _ready() -> void:
_dash_ghost = $DashGhost
func _physics_process(delta: float) -> void:
var input_dir := Input.get_axis("ui_left", "ui_right")
var velocity := self.velocity
# ダッシュ開始(例:Shiftキー)
if Input.is_action_just_pressed("dash") and not _is_dashing:
_is_dashing = true
_dash_timer = dash_time
if _is_dashing:
_dash_timer -= delta
if _dash_timer <= 0.0:
_is_dashing = false
# ダッシュ中の移動処理(横方向のみの簡易例)
velocity.x = dash_speed * sign(input_dir if input_dir != 0 else 1)
else:
# 通常移動処理
velocity.x = 200.0 * input_dir
# ダッシュ状態を DashGhost に伝える
if _dash_ghost:
_dash_ghost.is_dashing = _is_dashing
self.velocity = velocity
move_and_slide()
ポイントは _dash_ghost.is_dashing = _is_dashing の 1 行だけです。
ダッシュ演出のロジックはすべて DashGhost 内に閉じ込められているので、
プレイヤー側は「ダッシュしてるかどうか」を教えるだけで済みます。
手順④:敵や別キャラにもポン付けする
敵キャラにも同じ演出を使いたい場合は、同じように DashGhost を子ノードとして追加し、
敵のスクリプトから is_dashing を切り替えるだけです。
Enemy (CharacterBody2D) ├── AnimatedSprite2D ├── CollisionShape2D └── DashGhost (Node)
# Enemy.gd (例)
extends CharacterBody2D
var _dash_ghost: DashGhost
var _is_dashing: bool = false
func _ready() -> void:
_dash_ghost = $DashGhost
func start_dash() -> void:
_is_dashing = true
func stop_dash() -> void:
_is_dashing = false
func _physics_process(delta: float) -> void:
# 何らかのロジックで _is_dashing を切り替える…
if _dash_ghost:
_dash_ghost.is_dashing = _is_dashing
こうしておくと、プレイヤーと敵で同じコンポーネントを共有できて、
将来「残像の色を変えたい」「生成間隔を調整したい」となっても、
コンポーネント側の修正だけで全キャラに反映されます。
メリットと応用
この DashGhost コンポーネントを使うメリットを整理すると:
- 継承不要:プレイヤー用・敵用の「残像付きクラス」を別々に作る必要がない
- シーン構造がシンプル:キャラの子に 1 ノード足すだけで完結
- 再利用性が高い:ダッシュ以外の「高速移動」「テレポート演出」にも流用できる
- デザイナーも調整しやすい:spawn_interval / ghost_lifetime / ghost_start_alpha をインスペクタから触るだけ
たとえば、ステージの「動く床」にも DashGhost を付けてやれば、
高速で動く床が残像を引いて、スピード感のあるギミックを簡単に作れます。
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D └── DashGhost (Node)
# MovingPlatform.gd (例)
extends Node2D
@export var speed: float = 300.0
var _dash_ghost: DashGhost
func _ready() -> void:
_dash_ghost = $DashGhost
func _process(delta: float) -> void:
position.x += speed * delta
# 一定速度以上なら残像ON、遅ければOFFみたいな使い方もできる
if _dash_ghost:
_dash_ghost.is_dashing = abs(speed) > 50.0
「ダッシュ」という名前は付いていますが、
要するに「高速で動いている間だけ残像を出すコンポーネント」なので、
スピード感を出したいオブジェクト全般に使い回せます。
改造案:トレイルっぽく色を変えながら消える
最後に、少しだけ「改造案」を。
フェードアウトのときに色も変化させて、
例えば青→紫→透明みたいなトレイルっぽい演出にするなら、
_attach_fadeout をこんな感じに差し替えられます。
func _attach_fadeout(sprite: Sprite2D) -> void:
var tween := create_tween()
# アルファを 0 へ
tween.tween_property(
sprite,
"modulate:a",
0.0,
ghost_lifetime
).set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_OUT)
# 色相を少しずつ変化させる(青っぽくする例)
var target_color := sprite.modulate
target_color.r *= 0.6
target_color.g *= 0.6
target_color.b *= 1.4
tween.parallel().tween_property(
sprite,
"modulate:rgb",
target_color.rgb,
ghost_lifetime
)
tween.tween_callback(Callable(sprite, "queue_free"))
こういう「見た目の遊び」はコンポーネント側に閉じ込めておくと、
ゲーム全体の演出ポリシーを一括で変えやすくなります。
継承でガチガチに固めるよりも、コンポーネントをポン付けして合成するスタイルで、
気持ちよいダッシュ演出を量産していきましょう。




