【Godot 4】WindTunnel (風の通り道) コンポーネントの作り方

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 4で物理系のギミックを作るとき、つい「プレイヤーシーンを継承して、風エリア用の処理を追加しよう」とか「風専用のArea2Dシーンを作って、そこに直接スクリプトを書いちゃおう」となりがちですよね。ですが、そのたびにプレイヤーや敵のスクリプトに条件分岐を足したり、風エリアごとに似たようなコードをコピペしたりすると、すぐに管理不能なスパゲッティ化が始まります。

とくに「風の通り道」のような、特定エリア内の物体に対して、一定方向に力をかけ続けるタイプのギミックは、プレイヤー側のコードに手を入れてしまいがちです。

  • プレイヤーに is_in_wind フラグを追加する
  • 物理処理の中で「もし風エリアにいるなら…」と分岐を書く
  • 敵や動く足場にも同じような処理をコピペする

こうなると、風の仕様を変えたいときに、プレイヤー・敵・オブジェクト全部を修正しないといけなくなります。

そこで今回は、「風の処理は風のコンポーネントに閉じ込める」という発想で、Area2Dにアタッチするだけで、範囲内の物体に継続的な力を与え続けるコンポーネント WindTunnel を用意しました。ノード階層をムダに深くしたり、既存スクリプトを継承で引きずり回したりせず、必要なシーンにペタッと貼るだけで風エリアを量産できます。

【Godot 4】フワッと押し流す風エリアをコンポーネント化!「WindTunnel」コンポーネント

このコンポーネントは、Area2D にアタッチして使います。エリアに入った物体(PhysicsBody2DCharacterBody2D など)に対して、指定方向・指定強さの力を毎フレーム加え続ける仕組みです。

  • 風の向き(角度 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()

使い方の手順

  1. WindTunnel.gd を用意
    上のコードをそのまま res://components/WindTunnel.gd などに保存します。
    class_name WindTunnel がついているので、スクリプトファイルをどこに置いても ノードのスクリプト候補に出てきます
  2. 風エリア用の Area2D を作成
    例として、横向きの風でプレイヤーを右に押し流す「風の通路」を作ってみましょう。
    WindArea (Area2D)
     ├── CollisionShape2D
     └── WindTunnel (スクリプトとしてアタッチ)
        

    WindAreaCollisionShape2D を、風が吹く範囲に合わせて伸ばします。
    WindAreaWindTunnel.gd をアタッチします(右クリック > Attach Script or インスペクタから)。

  3. インスペクタでパラメータを設定
    WindArea(=WindTunnel付きArea2D)を選択し、インスペクタから:
    • direction_degrees: 0 → 右向き、90 → 下向き、-90 → 上向き
    • strength: 500〜1500 くらいから調整すると分かりやすいです
    • wind_mode:
      • ACCELERATION: だんだん加速する「風」っぽい挙動
      • VELOCITY_OVERRIDE: ベルトコンベア・エスカレーター的な一定速度
    • required_groups: 例として ["player"] とすると、player グループのノードだけが風の影響を受けます。

    あわせて、WindArea のコリジョンレイヤー/マスクを、影響させたいボディと接触するように設定しておきましょう。

  4. プレイヤーや敵シーンにそのまま適用
    風エリア自体はプレイヤーや敵の実装を一切知らないので、既存のシーン構造を壊さずに差し込めます。
    例: 横スクロールアクションで、プレイヤーを右に押し流す風エリア:
    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 = 800
  • wind_mode = ACCELERATION

こうしておくと、プレイヤーがこのエリアに入ると、ふわっと上に持ち上げられるような挙動になります。ジャンプ台や熱気球の上昇気流っぽい表現に使えますね。

例2: ベルトコンベア的な動く床

動く床をわざわざ CharacterBody2D で実装しなくても、静止した床 + WindTunnel でそれっぽい表現が可能です。

ConveyorArea (Area2D)
 ├── CollisionShape2D  # 床と同じ範囲
 └── WindTunnel
  • direction_degrees = 0(右)
  • strength = 250
  • wind_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 をベースにした風ギミックの合成を楽しんでみてください。

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をコピーしました!