Godot 4でキャラの足元に「土煙エフェクト」を付けたいとき、よくある実装はこんな感じですよね。
- プレイヤーシーンに
Particles2DやGPUParticles2Dを直付けする - アニメーションごとに
AnimationPlayerからパーティクルのemittingをオンオフする - もしくはプレイヤースクリプト内で「歩き中なら土煙ON、止まったらOFF」みたいな状態管理を書く
動きは作れるんですが、だんだんこうなっていきます。
- プレイヤースクリプトが「移動」と「アニメーション」と「エフェクト制御」でゴチャつく
- 敵キャラにも同じ土煙ロジックをコピペして、あとから修正が大変
- ノード階層の中に「Dust」「StepEffect」「WalkSmoke」など似たようなノードが散らばる
そこで今回は、「歩行アニメーションのフレームに合わせて足元から土煙を出す」機能を、完全に独立したコンポーネントとして切り出します。
プレイヤーでも敵でも、動く床でも、「足元からホコリが出そうなもの」にペタッと貼るだけで済むようにしていきましょう。
【Godot 4】歩くたびにホコリが舞う!「FootDust」コンポーネント
今回の FootDust コンポーネントの思想はシンプルです。
- アニメーション側から「このフレームで足が地面についたよ」と通知する
- FootDust はその通知を受けて、足元にパーティクル(またはシーン)を生成する
- プレイヤー本体は「移動」と「入力」に集中できる
つまり、継承ではなく「合成」です。
歩行ロジックは Player.gd に任せて、土煙は FootDust.gd に丸投げしましょう。
フルコード: FootDust.gd
extends Node2D
class_name FootDust
## 足元の土煙エフェクトを管理するコンポーネント
##
## 想定の使い方:
## - 親ノードは Player / Enemy / MovingPlatform など何でもOK
## - AnimationPlayer の足踏みフレームから `foot_dust.emit_left()` / `emit_right()` を呼ぶ
## - または、コードから `_on_landed()` などのタイミングで呼んでもよい
@export_category("Dust Scene / Particles")
## 生成する土煙シーン。
## PackedScene なら、任意のシーン(Particles2D / GPUParticles2D / Spriteアニメなど)を使えます。
@export var dust_scene: PackedScene
## 生成した土煙をどこにぶら下げるか。
## デフォルトはこの FootDust 自身にぶら下げます。
@export var parent_for_dust: NodePath
@export_category("Foot Positions")
## 左足のローカル座標(親ノード基準)
@export var left_foot_offset: Vector2 = Vector2(-8, 16)
## 右足のローカル座標(親ノード基準)
@export var right_foot_offset: Vector2 = Vector2(8, 16)
@export_category("Emission Settings")
## 連続で土煙を出さないようにするクールタイム(秒)
@export_range(0.0, 1.0, 0.01) var cooldown_sec: float = 0.08
## キャラの移動速度がこの値以下なら土煙を出さない(静止時に無駄に出ないように)
@export var min_speed_to_emit: float = 10.0
## X方向の速度を親から自動取得するかどうか。
## true の場合、親が CharacterBody2D / RigidBody2D などで velocity.x を見る。
@export var auto_read_horizontal_speed: bool = true
@export_category("Auto Cleanup")
## 生成した土煙ノードを自動的に削除するまでの秒数。
## 0 なら「シーン側の設定に任せる」(自動削除しない)。
@export_range(0.0, 10.0, 0.1) var auto_free_after: float = 1.5
@export_category("Debug")
## デバッグ用に足位置を表示するか
@export var debug_draw_feet: bool = false
## 内部状態
var _can_emit_left: bool = true
var _can_emit_right: bool = true
var _dust_parent: Node = null
func _ready() -> void:
# 土煙をぶら下げる親ノードを決定
if parent_for_dust != NodePath():
_dust_parent = get_node_or_null(parent_for_dust)
if _dust_parent == null:
# 指定がなければ FootDust 自身を使う
_dust_parent = self
# 親が移動系ノードでなくても動くように、特別なセットアップはしない
func _process(_delta: float) -> void:
# デバッグ表示用
if debug_draw_feet:
queue_redraw()
func _draw() -> void:
if not debug_draw_feet:
return
# 足の位置を視覚的に確認するためのガイド
draw_circle(left_foot_offset, 2.0, Color(0, 1, 0, 0.8))
draw_circle(right_foot_offset, 2.0, Color(0, 0, 1, 0.8))
draw_line(left_foot_offset, right_foot_offset, Color(1, 1, 0, 0.5), 1.0)
## 親の「現在の水平速度」を推定する。
## CharacterBody2D / RigidBody2D / Node2D + velocity_x プロパティをざっくり見る。
func _get_parent_horizontal_speed() -> float:
if not auto_read_horizontal_speed:
return min_speed_to_emit # チェックを無効化したいので閾値以上を返す
var p := get_parent()
if p == null:
return 0.0
# CharacterBody2D / RigidBody2D など Godot 標準の velocity を見る
if "velocity" in p:
var v = p.velocity
if typeof(v) == TYPE_VECTOR2:
return abs(v.x)
# カスタムな velocity_x プロパティを持っている場合
if "velocity_x" in p:
return abs(p.velocity_x)
# Node2D の position から速度を計算する…などもできるが、
# シンプルさ優先でここでは諦める
return 0.0
## 実際に土煙を生成する共通処理
func _spawn_dust_at(local_pos: Vector2) -> void:
if dust_scene == null:
push_warning("FootDust: 'dust_scene' が設定されていません。土煙を生成できません。")
return
# 親の速度が遅すぎる場合は土煙を出さない
var speed := _get_parent_horizontal_speed()
if speed < min_speed_to_emit:
return
var dust = dust_scene.instantiate()
if not (dust is Node2D):
push_warning("FootDust: dust_scene は Node2D 派生シーンを想定しています。位置指定がずれる可能性があります。")
# 親ノードのグローバル座標 + ローカルオフセットで足元の位置を決定
var parent_node := get_parent() as Node2D
if parent_node != null and dust is Node2D:
var world_pos := parent_node.to_global(local_pos)
(dust as Node2D).global_position = world_pos
_dust_parent.add_child(dust)
# Particles2D / GPUParticles2D の場合は emitting をトリガー
if "emitting" in dust:
dust.emitting = false
dust.emitting = true
# Auto free
if auto_free_after > 0.0:
dust.call_deferred("set_process", true)
var timer := get_tree().create_timer(auto_free_after)
# Timer が終わったら dust を削除
timer.timeout.connect(func():
if is_instance_valid(dust):
dust.queue_free()
)
## 左足の接地フレームで呼ぶ
func emit_left() -> void:
if not _can_emit_left:
return
_can_emit_left = false
_spawn_dust_at(left_foot_offset)
_start_cooldown(true)
## 右足の接地フレームで呼ぶ
func emit_right() -> void:
if not _can_emit_right:
return
_can_emit_right = false
_spawn_dust_at(right_foot_offset)
_start_cooldown(false)
## 左右共通のクールダウン処理
func _start_cooldown(is_left: bool) -> void:
if cooldown_sec <= 0.0:
# クールダウンが 0 の場合は即座に再度許可
if is_left:
_can_emit_left = true
else:
_can_emit_right = true
return
var timer := get_tree().create_timer(cooldown_sec)
timer.timeout.connect(func():
if is_left:
_can_emit_left = true
else:
_can_emit_right = true)
## スクリプト側から「どっちの足か気にせず」土煙を出したいとき用
## 例: 着地時に一発だけ出す、など
func emit_any() -> void:
# 左右交互に出す程度のシンプルな実装
if _can_emit_left:
emit_left()
elif _can_emit_right:
emit_right()
else:
# 両方クールダウン中なら何もしない
pass
使い方の手順
ここでは代表的な例として「2D横スクロールのプレイヤー」に FootDust を付ける手順を説明します。敵キャラや動く床などでも基本は同じです。
手順①: 土煙用シーン(Dust.tscn)を作る
- 新規シーンを作成し、ルートに
GPUParticles2Dを追加します。 - 名前を
Dustにして、以下のようにざっくり設定します(好みでOK)。
amount: 20lifetime: 0.4one_shot: Onemitting: Off(初期状態で止めておく)- Process Material で速度・重力・スケールなどを調整
シーンを res://effects/Dust.tscn などに保存します。
手順②: プレイヤーシーンに FootDust ノードを追加
例として、こんなプレイヤー構成を想定します。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AnimationPlayer └── FootDust (Node2D)
Playerシーンを開き、ルート(CharacterBody2D)の子としてNode2Dを追加し、名前をFootDustに変更します。- その
FootDustノードに、先ほどのFootDust.gdスクリプトをアタッチします。 - インスペクタで以下を設定します。
dust_scene: さきほど作ったDust.tscnを指定left_foot_offset/right_foot_offset: Sprite の足の位置に合わせて調整auto_read_horizontal_speed: Player がCharacterBody2DならそのままでOKdebug_draw_feet: 足位置の確認をしたいときは On にしておく
手順③: AnimationPlayer から FootDust を呼び出す
歩行アニメーションの中で「足が地面に着いたフレーム」に、アニメーション通知(Call Method Track)を仕込みます。
AnimationPlayerを選択し、「walk」などの歩きアニメを開きます。- トラック追加ボタンから「呼び出しトラック(Call Method Track)」を追加し、対象ノードに
FootDustノードを指定します。 - 左足が接地するフレームにキーを追加し、メソッド名に
emit_leftを指定します。 - 右足が接地するフレームにキーを追加し、メソッド名に
emit_rightを指定します。
これで、アニメーションが再生されるたびに FootDust コンポーネントが呼ばれ、左右の足元に土煙が出るようになります。
手順④: 敵キャラや動く床にも再利用する
同じ FootDust コンポーネントは、敵キャラや動く床にもそのまま流用できます。
例: 敵キャラ
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AnimationPlayer └── FootDust (Node2D)
例: 動く床(踏むとホコリが舞う床)
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D ├── Tween └── FootDust (Node2D)
- 敵キャラ: 歩行アニメのフレームから
emit_left/emit_rightを呼ぶ。 - 動く床: 「端に着いたとき」「プレイヤーが乗ったとき」などのタイミングで、コードから
foot_dust.emit_any()を呼ぶ。
どちらもロジックは FootDust に閉じ込められているので、本体スクリプトはほとんど汚れません。
メリットと応用
この FootDust コンポーネントを使うメリットを整理しておきましょう。
- シーン構造がスッキリ
土煙の設定やクールダウン、速度判定などは FootDust にまとまっているので、Player.gd / Enemy.gd は「移動」と「AI」に集中できます。 - 再利用性が高い
FootDust.gd と Dust.tscn をプロジェクト内で共有すれば、どのキャラにも同じ土煙表現を簡単に適用できます。
あとから「ちょっと土煙を大きくしよう」となっても、Dust.tscn をいじるだけで全キャラに反映されます。 - アニメーションとの結合度が低い
AnimationPlayer 側は「このフレームで emit_left を呼ぶ」だけ。
土煙の見た目やクールダウンの調整は FootDust 側で完結しているので、アニメーションを作り直してもロジックを触る必要がありません。 - コンポーネント指向での拡張がしやすい
「土煙の代わりに水しぶき」「雪煙」「火花」など、Dust.tscn を差し替えるだけでバリエーションを作れます。
さらに、FootDust 自体もコンポーネントなので、ちょっとした改造も簡単です。
例えば「ダッシュ中は土煙を大きくしたい」という場合、_spawn_dust_at 内で親の速度を見てスケールを変えるだけでOKです。
改造案: 速く動いているほど土煙を大きくする
以下のような関数を FootDust に追加して、_spawn_dust_at の中から呼ぶと、速度に応じてスケールを変えられます。
## 親の速度に応じて土煙のスケールを調整する例
func _scale_dust_by_speed(dust: Node) -> void:
var speed := _get_parent_horizontal_speed()
# 0 ~ 300 くらいの速度を 1.0 ~ 2.0 のスケールにマッピング
var t := clamp(speed / 300.0, 0.0, 1.0)
var scale_factor := lerp(1.0, 2.0, t)
if dust is Node2D:
(dust as Node2D).scale = Vector2.ONE * scale_factor
そして _spawn_dust_at の中で、インスタンス生成後に
_scale_dust_by_speed(dust)
を呼べば、歩きよりダッシュのほうが土煙が派手になる、という表現も簡単に実現できます。
こうやって「エフェクトは FootDust に」「移動は Player に」と役割を分けておくと、あとからどんどんコンポーネントを足していっても破綻しにくくなります。
継承より合成、どんどん進めていきましょう。
