Godot 4で物理系のギミックを作るとき、つい「プレイヤーシーンを継承して、風エリア用の処理を追加しよう」とか「風専用のArea2Dシーンを作って、そこに直接スクリプトを書いちゃおう」となりがちですよね。ですが、そのたびにプレイヤーや敵のスクリプトに条件分岐を足したり、風エリアごとに似たようなコードをコピペしたりすると、すぐに管理不能なスパゲッティ化が始まります。
とくに「風の通り道」のような、特定エリア内の物体に対して、一定方向に力をかけ続けるタイプのギミックは、プレイヤー側のコードに手を入れてしまいがちです。
- プレイヤーに
is_in_windフラグを追加する - 物理処理の中で「もし風エリアにいるなら…」と分岐を書く
- 敵や動く足場にも同じような処理をコピペする
こうなると、風の仕様を変えたいときに、プレイヤー・敵・オブジェクト全部を修正しないといけなくなります。
そこで今回は、「風の処理は風のコンポーネントに閉じ込める」という発想で、Area2Dにアタッチするだけで、範囲内の物体に継続的な力を与え続けるコンポーネント WindTunnel を用意しました。ノード階層をムダに深くしたり、既存スクリプトを継承で引きずり回したりせず、必要なシーンにペタッと貼るだけで風エリアを量産できます。
【Godot 4】フワッと押し流す風エリアをコンポーネント化!「WindTunnel」コンポーネント
このコンポーネントは、Area2D にアタッチして使います。エリアに入った物体(PhysicsBody2D や CharacterBody2D など)に対して、指定方向・指定強さの力を毎フレーム加え続ける仕組みです。
- 風の向き(角度 or ベクトル)
- 強さ(加速度 or 速度上書きのどちらで扱うか)
- 対象フィルタ(プレイヤーだけ / 敵だけ / RigidBody2Dだけ など)
などを、@export でインスペクタから調整可能にしてあります。
フルコード:WindTunnel.gd
extends Area2D
class_name WindTunnel
## WindTunnel (風の通り道)
## Area2D内にいる物体に、指定方向へ継続的な力を加え続けるコンポーネント。
##
## 使い方:
## - Area2D ノードにこのスクリプトをアタッチ
## - コリジョンレイヤー/マスクで「誰が風の影響を受けるか」を設定
## - export 変数で風向き・強さ・モードを調整
@export_category("Wind Settings")
## 風の向き(度数法)。0度=右、90度=下、-90度=上。
@export_range(-180.0, 180.0, 1.0, "radians_as_degrees")
var direction_degrees: float = 0.0:
set(value):
direction_degrees = value
_direction = Vector2.RIGHT.rotated(deg_to_rad(direction_degrees))
## 風の強さ。モードによって意味が変わる:
## - ACCELERATION: [pixel/sec^2] として加速度的に加える
## - VELOCITY_OVERRIDE: [pixel/sec] で目標速度として扱う
@export_range(0.0, 5000.0, 10.0)
var strength: float = 500.0
## 風の適用モード
enum WindMode {
ACCELERATION, ## 速度に加算していく(ふわっと加速)
VELOCITY_OVERRIDE, ## 風方向の速度を上書き(ベルトコンベア的)
}
@export var wind_mode: WindMode = WindMode.ACCELERATION
## CharacterBody2D に対しても適用するか?
@export var affect_character_bodies: bool = true
## RigidBody2D に対しても適用するか?
@export var affect_rigid_bodies: bool = true
## その他の PhysicsBody2D(例: 自作のKinematic系など)にも適用するか?
@export var affect_generic_bodies: bool = false
## 対象をグループ名でフィルタする場合。空配列なら全て対象。
## 例: ["player", "enemy"] とすると、そのどちらかのグループに属するノードだけが対象。
@export var required_groups: Array[StringName] = []
## デバッグ表示:エディタ上とゲーム中に風向きを矢印で描画する
@export var debug_draw: bool = true
## 内部用: 正規化された風向きベクトル
var _direction: Vector2 = Vector2.RIGHT
## エリア内にいる対象ノードを保持
var _bodies: Array[Node] = []
func _ready() -> void:
# 初期方向ベクトルを反映
_direction = Vector2.RIGHT.rotated(deg_to_rad(direction_degrees))
# すでにエリア内にいるボディも拾っておく(シーン読み込み直後など)
for body in get_overlapping_bodies():
_on_body_entered(body)
# シグナル接続(エディタから接続していなくても動くように)
if not body_entered.is_connected(_on_body_entered):
body_entered.connect(_on_body_entered)
if not body_exited.is_connected(_on_body_exited):
body_exited.connect(_on_body_exited)
func _physics_process(delta: float) -> void:
if _bodies.is_empty():
return
# フレームごとに、エリア内の対象に風の力を適用
for body in _bodies:
if not is_instance_valid(body):
continue
_apply_wind_to_body(body, delta)
func _on_body_entered(body: Node) -> void:
if not _is_body_affected(body):
return
if body in _bodies:
return
_bodies.append(body)
func _on_body_exited(body: Node) -> void:
_bodies.erase(body)
func _is_body_affected(body: Node) -> bool:
# グループフィルタ
if not required_groups.is_empty():
var ok := false
for group in required_groups:
if body.is_in_group(group):
ok = true
break
if not ok:
return false
# タイプフィルタ
if affect_character_bodies and body is CharacterBody2D:
return true
if affect_rigid_bodies and body is RigidBody2D:
return true
if affect_generic_bodies and body is PhysicsBody2D:
return true
return false
func _apply_wind_to_body(body: Node, delta: float) -> void:
match wind_mode:
WindMode.ACCELERATION:
_apply_as_acceleration(body, delta)
WindMode.VELOCITY_OVERRIDE:
_apply_as_velocity_override(body, delta)
func _apply_as_acceleration(body: Node, delta: float) -> void:
var accel := _direction * strength # [px/sec^2]
var dv := accel * delta # 速度変化量 [px/sec]
if body is CharacterBody2D:
# CharacterBody2D は velocity プロパティを直接操作
body.velocity += dv
elif body is RigidBody2D:
# RigidBody2D は加速度を「力」として適用する
# mass を考慮するなら body.mass * accel を力にしてもよい
body.apply_central_force(accel)
elif body is PhysicsBody2D:
# 他のカスタム物理ボディ用。velocity プロパティがある想定。
if "velocity" in body:
body.velocity += dv
func _apply_as_velocity_override(body: Node, delta: float) -> void:
var target_vel := _direction * strength
if body is CharacterBody2D:
# CharacterBody2D の velocity を、風向き成分だけ上書き
# 既存の他方向の速度は残したいので、風方向と直交方向に分解する
body.velocity = _override_velocity_along_direction(body.velocity, target_vel)
elif body is RigidBody2D:
# RigidBody2D は線形速度を直接上書き(ベルトコンベア的な挙動)
body.linear_velocity = _override_velocity_along_direction(body.linear_velocity, target_vel)
elif body is PhysicsBody2D:
if "velocity" in body:
body.velocity = _override_velocity_along_direction(body.velocity, target_vel)
func _override_velocity_along_direction(current: Vector2, target: Vector2) -> Vector2:
# current を「風方向」と「それに直交する成分」に分解し、
# 風方向の成分だけ target に置き換える。
if _direction == Vector2.ZERO:
return current
var dir := _direction.normalized()
var parallel_len := current.dot(dir) # current の風方向成分の長さ
var parallel := dir * parallel_len # 風方向成分
var perpendicular := current - parallel # 直交成分
var target_parallel := target.project(dir) # target の風方向成分
return perpendicular + target_parallel
func _draw() -> void:
if not debug_draw:
return
if _direction == Vector2.ZERO:
return
var arrow_len := 64.0
var start := Vector2.ZERO
var end := _direction.normalized() * arrow_len
# 本体の矢印線
draw_line(start, end, Color.CYAN, 2.0)
# 矢印の先端
var head_size := 8.0
var dir := (end - start).normalized()
var left := dir.rotated(deg_to_rad(150.0)) * head_size
var right := dir.rotated(deg_to_rad(-150.0)) * head_size
draw_line(end, end + left, Color.CYAN, 2.0)
draw_line(end, end + right, Color.CYAN, 2.0)
func _process(_delta: float) -> void:
# エディタ上でパラメータ変更したときにも矢印を更新したいので
if Engine.is_editor_hint():
queue_redraw()
使い方の手順
- WindTunnel.gd を用意
上のコードをそのままres://components/WindTunnel.gdなどに保存します。
class_name WindTunnelがついているので、スクリプトファイルをどこに置いても ノードのスクリプト候補に出てきます。 - 風エリア用の Area2D を作成
例として、横向きの風でプレイヤーを右に押し流す「風の通路」を作ってみましょう。WindArea (Area2D) ├── CollisionShape2D └── WindTunnel (スクリプトとしてアタッチ)–
WindAreaの CollisionShape2D を、風が吹く範囲に合わせて伸ばします。
–WindAreaに WindTunnel.gd をアタッチします(右クリック > Attach Script or インスペクタから)。 - インスペクタでパラメータを設定
WindArea(=WindTunnel付きArea2D)を選択し、インスペクタから:direction_degrees: 0 → 右向き、90 → 下向き、-90 → 上向きstrength: 500〜1500 くらいから調整すると分かりやすいですwind_mode:ACCELERATION: だんだん加速する「風」っぽい挙動VELOCITY_OVERRIDE: ベルトコンベア・エスカレーター的な一定速度
required_groups: 例として["player"]とすると、playerグループのノードだけが風の影響を受けます。
あわせて、WindArea のコリジョンレイヤー/マスクを、影響させたいボディと接触するように設定しておきましょう。
- プレイヤーや敵シーンにそのまま適用
風エリア自体はプレイヤーや敵の実装を一切知らないので、既存のシーン構造を壊さずに差し込めます。
例: 横スクロールアクションで、プレイヤーを右に押し流す風エリア:Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── (プレイヤー自前スクリプト)ステージ側:
Level01 (Node2D) ├── TileMap ├── Player (CharacterBody2D) └── WindArea_Right (Area2D) ├── CollisionShape2D └── WindTunnel (スクリプト)さらに、敵にも同じ風を効かせたい場合は、敵を
enemyグループに入れておき、
required_groups = ["player", "enemy"]のように設定すればOKです。
別の具体例
もう少しバリエーションを出してみます。
例1: 上向きの風でジャンプアシスト(上昇気流)
UpdraftArea (Area2D) ├── CollisionShape2D └── WindTunnel
direction_degrees = -90(上方向)strength = 800wind_mode = ACCELERATION
こうしておくと、プレイヤーがこのエリアに入ると、ふわっと上に持ち上げられるような挙動になります。ジャンプ台や熱気球の上昇気流っぽい表現に使えますね。
例2: ベルトコンベア的な動く床
動く床をわざわざ CharacterBody2D で実装しなくても、静止した床 + WindTunnel でそれっぽい表現が可能です。
ConveyorArea (Area2D) ├── CollisionShape2D # 床と同じ範囲 └── WindTunnel
direction_degrees = 0(右)strength = 250wind_mode = VELOCITY_OVERRIDE
こうしておくと、エリア上にいる CharacterBody2D の 横方向速度だけを一定値に保つので、ベルトコンベアの上に乗っているような動きになります。
シーン構成図まとめ
最後に、よくありそうな構成をもう一度まとめておきます。
# プレイヤー Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── PlayerController (Script) # 上昇気流エリア UpdraftArea (Area2D) ├── CollisionShape2D └── WindTunnel (Script) # ベルトコンベア的な風エリア ConveyorArea (Area2D) ├── CollisionShape2D └── WindTunnel (Script)
メリットと応用
この WindTunnel コンポーネントを使うと、風ギミックに関するロジックがすべて Area2D 側に閉じ込められます。
- プレイヤーや敵のスクリプトに「風エリア用の特別なコード」を書かなくてよい
- 風の強さ・向き・挙動を レベルデザイナーがインスペクタから調整できる
- 必要なのは Area2D + CollisionShape2D + WindTunnel だけなので、シーン構造がスッキリする
- 同じコンポーネントを ステージ間でコピペ or シーンインスタンスして使い回せる
「風の通り道」を増やしたくなっても、プレイヤーや敵には一切触らず、ステージ側に WindArea をポンポン置くだけで済みます。これがまさに「継承より合成」の強さですね。
改造案:時間で風向きをスイングさせる
例えば「一定周期で風向きが変わる扇風機」のようなギミックを作りたい場合は、WindTunnel に簡単な追加処理を入れるだけで実現できます。
# WindTunnel の中に追加する一例
@export_category("Wind Animation")
@export var auto_swing: bool = false
@export_range(0.0, 180.0, 1.0)
var swing_amplitude_degrees: float = 45.0
@export_range(0.1, 10.0, 0.1)
var swing_speed: float = 1.0
func _process(delta: float) -> void:
# 既存のデバッグ描画処理を残しつつ、風向きアニメも行う
if auto_swing:
var t := Time.get_ticks_msec() / 1000.0
var offset := sin(t * swing_speed) * swing_amplitude_degrees
direction_degrees = offset # セッターを通して _direction を更新
if Engine.is_editor_hint():
queue_redraw()
これで auto_swing = true にすると、左右に首振りする風エリアが簡単に作れます。WindTunnel 自体はあくまで「風をかけるコンポーネント」なので、こうしたアニメーションも別コンポーネントとして切り出してもいいですし、上のように軽く混ぜてもOKです。
このように、「風」という概念を 1 つのコンポーネントに閉じ込めておけば、仕様変更やギミック追加にも柔軟に対応できるようになります。ぜひ、自分のプロジェクトでも WindTunnel をベースにした風ギミックの合成を楽しんでみてください。




