Godot 4で「風の影響」を実装しようとすると、けっこう面倒ですよね。
- プレイヤーや敵のスクリプトに直接「風処理」を書き足すと、あちこちのコードに風ロジックが散らばる
- Area2Dに毎回似たようなスクリプトを書いて、コピペ地獄になりがち
- シーンごとに「このステージの風は右向き」「こっちは上向き」など、調整パラメータがバラバラになって混乱する
しかも、Godot標準のやり方だと、
- プレイヤー: Player.gd に「風対応コード」
- 敵: Enemy.gd に「風対応コード」
- 動く足場: MovingPlatform.gd に「風対応コード」
…というように、継承ベースでそれぞれに書き足していくパターンになりがちです。これはまさに「継承地獄」。
そこで今回は、「風を発生させる側」を完全にコンポーネント化して、どんなシーンにもポン付けできるようにする WindFan コンポーネントを用意しました。
Area2Dにアタッチするだけで、範囲内の対象に一定方向のエアフォース(風力)を与え続けることができます。風向きや強さはインスペクタから調整可能。複数並べてステージギミックにするのも簡単です。
【Godot 4】シーンにポン付けで風ギミック!「WindFan」コンポーネント
フルコード(GDScript / Godot 4)
extends Area2D
class_name WindFan
##
## WindFan.gd
## Area2D 内にいる対象へ、一定方向の「風力」を加えるコンポーネント。
## - 方向、強さ、対象フィルタをインスペクタから調整可能。
## - 物理オブジェクト(RigidBody2D)にも、自前の移動ロジック(CharacterBody2Dなど)にも対応しやすい設計。
##
@export_category("Wind Settings")
## 風の向き(単位ベクトルでなくてもOK。内部で正規化されます)
@export var direction: Vector2 = Vector2.RIGHT
## 風の強さ(1秒あたりに加える速度量)
## RigidBody2D には「力」として、CharacterBody2D 等には「速度加算」として扱います。
@export var strength: float = 300.0
## 風を与える対象のレイヤー(Godot の CollisionLayer と混同しないため、独自フィルタ用)
## 例: プレイヤーは 1, 敵は 2, 物理オブジェクトは 4 など
@export_flags_2d_physics var target_layers: int = 1
## 風を常にオンにするかどうか(スイッチで切り替えたい場合など)
@export var enabled: bool = true
@export_category("Debug")
## デバッグ用に、エディタ上で風向きを矢印で描画する
@export var debug_draw_gizmo: bool = true
## Area2D 内で現在「風の影響を受ける候補」として追跡しているノード
var _bodies: Array[Node] = []
func _ready() -> void:
## Area2D のシグナル接続(エディタで接続しなくても動くように)
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _physics_process(delta: float) -> void:
if not enabled:
return
if direction == Vector2.ZERO or strength == 0.0:
return
var dir := direction.normalized()
var force_vec := dir * strength
for body in _bodies:
if not is_instance_valid(body):
continue
if not _matches_target_layer(body):
continue
# RigidBody2D の場合は「力」として加える
if body is RigidBody2D:
_apply_force_to_rigidbody(body, force_vec, delta)
# CharacterBody2D / Kinematic 系は「速度加算」として扱う
elif body is CharacterBody2D:
_apply_force_to_character(body, force_vec, delta)
# それ以外はカスタムプロパティを期待する(velocity や linear_velocity など)
else:
_apply_force_to_custom(body, force_vec, delta)
func _on_body_entered(body: Node) -> void:
if body in _bodies:
return
_bodies.append(body)
func _on_body_exited(body: Node) -> void:
_bodies.erase(body)
func _matches_target_layer(body: Node) -> bool:
## ここでは「body.layers」がある前提(RigidBody2D / CharacterBody2D など)で簡易チェック
## もし別の仕組みでフィルタしたい場合は、ここを書き換えればOK。
if not body.has_method("get_collision_layer_value") and not body.has_method("get_collision_layer"):
# 物理オブジェクト以外は、とりあえず許可しておくパターン
return true
var layer_bits: int = 0
# Godot 4 の RigidBody2D / CharacterBody2D は physics layers を持つ
if body.has_method("get_collision_layer"):
layer_bits = body.get_collision_layer()
elif "collision_layer" in body:
layer_bits = body.collision_layer
return (layer_bits & target_layers) != 0
func _apply_force_to_rigidbody(body: RigidBody2D, force_vec: Vector2, delta: float) -> void:
# RigidBody2D には「力」として加える。
# strength は「速度量」想定なので、質量や delta を考慮して力に変換してもよいが、
# ここではシンプルに「連続的な力」として apply_force を使う。
# Godot 4 では add_constant_force / apply_central_force などが使えます。
if not body.is_inside_tree():
return
body.apply_central_force(force_vec)
func _apply_force_to_character(body: CharacterBody2D, force_vec: Vector2, delta: float) -> void:
# CharacterBody2D は velocity プロパティを持つ前提。
# ここでは「風で徐々に加速する」イメージで速度を加算します。
if not body.is_inside_tree():
return
if not ("velocity" in body):
return
var v: Vector2 = body.velocity
v += force_vec * delta
body.velocity = v
func _apply_force_to_custom(body: Node, force_vec: Vector2, delta: float) -> void:
# 自前の移動ロジックを持つノード向け。
# velocity / linear_velocity / custom_velocity のいずれかを探して加算します。
if not body.is_inside_tree():
return
var applied := false
if "velocity" in body:
body.velocity += force_vec * delta
applied = true
elif "linear_velocity" in body:
body.linear_velocity += force_vec * delta
applied = true
elif "custom_velocity" in body:
body.custom_velocity += force_vec * delta
applied = true
# 何もプロパティが無い場合は、ユーザー側で対応してもらう。
# 例えば、body に apply_wind(Vector2 force) メソッドを用意しておき、
# ここから呼び出すようにするのもアリ。
if not applied and body.has_method("apply_wind"):
body.apply_wind(force_vec * delta)
func _draw() -> void:
if not debug_draw_gizmo:
return
if direction == Vector2.ZERO:
return
var dir := direction.normalized()
var length := 64.0
var to := dir * length
draw_line(Vector2.ZERO, to, Color.CYAN, 2.0)
# 矢印の先端
var left := to + dir.rotated(deg_to_rad(150.0)) * 16.0
var right := to + dir.rotated(deg_to_rad(-150.0)) * 16.0
draw_line(to, left, Color.CYAN, 2.0)
draw_line(to, right, Color.CYAN, 2.0)
func _process(_delta: float) -> void:
# エディタ上でも矢印を更新したい場合のために再描画
if Engine.is_editor_hint():
queue_redraw()
使い方の手順
基本的には「Area2D に WindFan をアタッチするだけ」です。以下の手順で進めましょう。
① コンポーネントスクリプトを用意する
- 上記の
WindFan.gdを新規スクリプトとして保存します。
例:res://components/WindFan.gd - Godot エディタを再読み込みすると、
class_name WindFanにより、ノードに直接アタッチ可能になります。
② シーンに扇風機(WindFan)を配置する
例として、右向きの風でプレイヤーを押し流すシンプルなステージを考えます。
MainLevel (Node2D)
├── Player (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
└── WindArea (Area2D)
├── CollisionShape2D
└── WindFan (script)
- WindArea (Area2D) ノードを追加します。
- CollisionShape2D を子に追加し、風が届く範囲(矩形やカプセルなど)を設定します。
- WindArea に
WindFan.gdをアタッチします。
(もしくは、WindArea 自体を削除して、WindFan(Area2D)ノードを直接追加してもOKです)
③ インスペクタで風向きと強さを調整する
WindFan を選択し、インスペクタから以下を設定します。
- direction: 例)
(1, 0)で右向き、(0, -1)で上向き - strength: 例)
300 ~ 800くらいから試すと良いです - target_layers: 風の影響を受けるオブジェクトのレイヤーを指定
– プレイヤーだけを飛ばしたいなら、プレイヤーの Collision Layer に対応するビットだけを ON にします。 - enabled: 最初から風を有効にするかどうか
- debug_draw_gizmo: オンにすると、エディタ上で風向きの矢印が表示されて便利です
④ 具体例:プレイヤー・敵・動く床をまとめて風で飛ばす
例えば、こんなシーン構成にしてみましょう:
Level01 (Node2D)
├── Player (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── Enemy (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── MovingPlatform (RigidBody2D or CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
└── WindTunnel (Area2D)
├── CollisionShape2D
└── WindFan (script)
- Player / Enemy / MovingPlatform それぞれに 衝突レイヤーを設定します。
例:- Player: Layer 1
- Enemy: Layer 2
- MovingPlatform: Layer 4
WindFan.target_layersを1 | 2 | 4(= 7)に設定すれば、全員が風の影響を受けるようになります。- もし「プレイヤーだけ飛ばしたい」なら、
target_layers = 1にすればOKです。
プレイヤーや敵のスクリプトには、風に関するコードは一切書く必要なしです。
「風エリアを配置してパラメータをいじるだけ」で、レベルデザインが完結するのがコンポーネント方式の強みですね。
メリットと応用
WindFan コンポーネントを使うメリットを整理してみましょう。
- シーン構造がシンプル
風ギミックはすべてWindFanノードとして表現されるので、
「このステージの風はどこ?」が一目でわかります。 - ロジックの重複がない
プレイヤー / 敵 / ギミックごとに「風処理」を書かなくてよく、
風の挙動を変えたくなってもWindFan.gdを1箇所いじるだけで全体に反映されます。 - レベルデザイナーが触りやすい
風の向きや強さを インスペクタから直感的に変更できるので、
コードを書かないメンバーでもバランス調整ができます。 - 継承に頼らない設計
風の影響を受けるオブジェクトは、特別な親クラスを継承する必要がありません。
既存のCharacterBody2DやRigidBody2Dにそのまま適用できます。
さらに、WindFan はちょっとした改造でいろいろ応用できます。
- 周期的に ON/OFF する「間欠風」
- プレイヤーがスイッチを押したら風向きが変わるギミック
- プレイヤーの位置に応じて風力が変わる「竜巻」
例えば、「一定間隔で風が出たり止まったりする」改造案はこんな感じです:
## WindFan.gd 内に追加する例
@export_category("Wind Toggle")
@export var toggle_interval: float = 0.0 # 0 のときは常時ON(トグル無効)
var _toggle_timer: float = 0.0
func _physics_process(delta: float) -> void:
# まずトグルの処理
if toggle_interval > 0.0:
_toggle_timer += delta
if _toggle_timer >= toggle_interval:
_toggle_timer = 0.0
enabled = not enabled
# 既存の風適用ロジックをそのまま利用
if not enabled:
return
if direction == Vector2.ZERO or strength == 0.0:
return
var dir := direction.normalized()
var force_vec := dir * strength
for body in _bodies:
if not is_instance_valid(body):
continue
if not _matches_target_layer(body):
continue
if body is RigidBody2D:
_apply_force_to_rigidbody(body, force_vec, delta)
elif body is CharacterBody2D:
_apply_force_to_character(body, force_vec, delta)
else:
_apply_force_to_custom(body, force_vec, delta)
toggle_interval に 1.0 を入れれば「1秒ごとに ON/OFF する扇風機」の完成です。
このように、風のロジックはすべて WindFan に閉じ込めておき、シーン側はただ「コンポーネントを置く」だけ、というスタイルにしておくと、後からの改造・実験がとても楽になりますね。
