【Godot 4】DashGhost (ダッシュ残像) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

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

手順②:プレイヤーシーンにアタッチする

  1. Player シーンを開く
  2. Player (CharacterBody2D) の子として Node を追加し、名前を DashGhost に変更
  3. その Node に DashGhost.gd をアタッチ

このとき、インスペクタで以下のように設定しておくと分かりやすいです。

  • source_root_path : ..(=親の Player)
  • source_sprite_path : ../Sprite2D
  • ghost_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"))

こういう「見た目の遊び」はコンポーネント側に閉じ込めておくと、
ゲーム全体の演出ポリシーを一括で変えやすくなります。
継承でガチガチに固めるよりも、コンポーネントをポン付けして合成するスタイルで、
気持ちよいダッシュ演出を量産していきましょう。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!