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 をアタッチするだけ」です。以下の手順で進めましょう。

① コンポーネントスクリプトを用意する

  1. 上記の WindFan.gd を新規スクリプトとして保存します。
    例: res://components/WindFan.gd
  2. Godot エディタを再読み込みすると、class_name WindFan により、ノードに直接アタッチ可能になります。

② シーンに扇風機(WindFan)を配置する

例として、右向きの風でプレイヤーを押し流すシンプルなステージを考えます。

MainLevel (Node2D)
 ├── Player (CharacterBody2D)
 │    ├── Sprite2D
 │    └── CollisionShape2D
 └── WindArea (Area2D)
      ├── CollisionShape2D
      └── WindFan (script)
  1. WindArea (Area2D) ノードを追加します。
  2. CollisionShape2D を子に追加し、風が届く範囲(矩形やカプセルなど)を設定します。
  3. WindAreaWindFan.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_layers1 | 2 | 4(= 7)に設定すれば、全員が風の影響を受けるようになります。
  • もし「プレイヤーだけ飛ばしたい」なら、target_layers = 1 にすればOKです。

プレイヤーや敵のスクリプトには、風に関するコードは一切書く必要なしです。
「風エリアを配置してパラメータをいじるだけ」で、レベルデザインが完結するのがコンポーネント方式の強みですね。

メリットと応用

WindFan コンポーネントを使うメリットを整理してみましょう。

  • シーン構造がシンプル
    風ギミックはすべて WindFan ノードとして表現されるので、
    「このステージの風はどこ?」が一目でわかります。
  • ロジックの重複がない
    プレイヤー / 敵 / ギミックごとに「風処理」を書かなくてよく、
    風の挙動を変えたくなっても WindFan.gd を1箇所いじるだけで全体に反映されます。
  • レベルデザイナーが触りやすい
    風の向きや強さを インスペクタから直感的に変更できるので、
    コードを書かないメンバーでもバランス調整ができます。
  • 継承に頼らない設計
    風の影響を受けるオブジェクトは、特別な親クラスを継承する必要がありません。
    既存の CharacterBody2DRigidBody2D にそのまま適用できます。

さらに、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 に閉じ込めておき、シーン側はただ「コンポーネントを置く」だけ、というスタイルにしておくと、後からの改造・実験がとても楽になりますね。