【Godot 4】WindHowl (風切り音) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Godot 4 でアクションゲームやプラットフォーマーを作っていると、

  • 落下スピードに合わせて風切り音を鳴らしたい
  • でもプレイヤーや敵のスクリプトはすでにパンパンで、これ以上ロジックを足したくない
  • 各キャラごとに「落下処理+サウンド処理」を継承で量産すると、あとから仕様変更が地獄

……みたいな状況になりがちですね。

Godot の「継承ベース」で書いていると、

  • Player.gd に物理・入力・アニメーション・サウンドが全部入り
  • 敵キャラは BaseCharacter.gd を継承してさらにごちゃごちゃ
  • 風切り音のロジックを直したいだけなのに、全キャラのスクリプトに手を入れる羽目になる

こういう「肥大化クラス問題」を避けるために、「落下速度に応じた風切り音」を完全に独立したコンポーネントに切り出してしまいましょう。キャラ側は「自分の速度を持っているだけ」、サウンドは「速度を見て音量を変えるだけ」。この2つを疎結合にしておけば、どのキャラにも簡単に「風切り音」を後付けできます。

そこで登場するのが、今回のコンポーネント「WindHowl」です。

【Godot 4】落下が怖くなるサウンド演出!「WindHowl」コンポーネント

「WindHowl」は、

  • 対象ノードの「落下速度(縦方向の速度)」を監視
  • その速度に応じて、ホワイトノイズ(風切り音)の音量を自動でフェード
  • 閾値以下では無音、最大速度付近で最大音量

というシンプルなコンポーネントです。

キャラ本体のスクリプトを一切いじらず、シーンにペタっと貼るだけで風切り音が追加できます。「継承より合成」らしい、気持ちいい設計ですね。


WindHowl コンポーネントのフルコード


extends Node
class_name WindHowl
## WindHowl (風切り音) コンポーネント
##
## 対象ノードの落下速度に応じて、ホワイトノイズの音量を自動調整する。
## - 対象は CharacterBody2D / RigidBody2D / Node2D などを想定
## - "垂直速度" の取得方法を export で切り替え可能
## - AudioStreamPlayer2D を内部で自動生成 or 既存ノードを利用

@export_category("Target Settings")
## 風切り音の対象となるノード。
## 未指定の場合は、自分の親ノードを対象とする。
@export var target_node: NodePath

## 対象の垂直速度をどこから取得するかを指定する。
## - "character_body" : CharacterBody2D/3D の velocity.y
## - "rigid_body"     : RigidBody2D/3D の linear_velocity.y
## - "node2d"         : Node2D の position.y のフレーム間差分
@export_enum("character_body", "rigid_body", "node2d")
var velocity_source: String = "character_body"

@export_category("Audio Settings")
## 既存の AudioStreamPlayer2D を使いたい場合に指定する。
## 未指定なら、自動で子ノードとして生成する。
@export var audio_player_path: NodePath

## 風切り音として再生する AudioStream。
## ホワイトノイズ系のループ音を指定するとよい。
@export var wind_stream: AudioStream

## 最小音量(dB)。速度が閾値以下のとき、この値 or ミュート付近になる。
@export_range(-80.0, 0.0, 0.1)
var min_volume_db: float = -40.0

## 最大音量(dB)。最大速度以上のとき、この値にクリップされる。
@export_range(-40.0, 6.0, 0.1)
var max_volume_db: float = 0.0

@export_category("Speed Mapping")
## この速度以下では風切り音をほぼ鳴らさない(絶対値)。
## 例: 100.0 → |vy| < 100 なら無音に近い。
@export_range(0.0, 5000.0, 1.0)
var min_speed_threshold: float = 150.0

## この速度以上で最大音量になる(絶対値)。
## 例: 800.0 → |vy| >= 800 なら max_volume_db。
@export_range(1.0, 10000.0, 1.0)
var max_speed_threshold: float = 800.0

## 音量の追従スピード。1.0 で即時、0.1 でかなりゆっくり。
@export_range(0.01, 1.0, 0.01)
var volume_lerp_factor: float = 0.2

@export_category("Misc")
## 上昇中(vy < 0)のときも風切り音を鳴らすか。
## false の場合、落下中(vy > 0)のみ有効。
@export var enable_on_rise: bool = true

## 対象が "地面にいる" 状態を表すプロパティ名。
## CharacterBody2D の is_on_floor() を見る場合は空文字にしておき、
## 別途カスタムで管理している場合はそのブールプロパティ名を指定する。
@export var grounded_property_name: String = ""

## 地面にいるときは強制的にミュートするか。
@export var mute_when_grounded: bool = true

## デバッグ用に、現在の推定速度を表示するか。
@export var debug_print_speed: bool = false


var _target: Node
var _audio: AudioStreamPlayer2D
var _last_position_y: float
var _current_volume_db: float


func _ready() -> void:
    # 対象ノードの解決
    if target_node != NodePath():
        _target = get_node_or_null(target_node)
    else:
        _target = get_parent()

    if _target == null:
        push_warning("WindHowl: target_node が見つかりません。親ノードを対象にするか、正しい NodePath を設定してください。")
        set_process(false)
        return

    # AudioStreamPlayer2D の取得 or 自動生成
    if audio_player_path != NodePath():
        _audio = get_node_or_null(audio_player_path) as AudioStreamPlayer2D
    else:
        _audio = AudioStreamPlayer2D.new()
        _audio.name = "WindHowlAudio"
        add_child(_audio)

    if _audio == null:
        push_warning("WindHowl: AudioStreamPlayer2D が取得できませんでした。audio_player_path を確認してください。")
        set_process(false)
        return

    if wind_stream != null:
        _audio.stream = wind_stream
    else:
        push_warning("WindHowl: wind_stream が設定されていません。ホワイトノイズ系の AudioStream を指定してください。")

    _audio.autoplay = false
    _audio.bus = "Master"  # 必要に応じてバス名を変更
    _audio.volume_db = min_volume_db
    _current_volume_db = min_volume_db

    # Node2D から速度を推定する場合、初期位置を記録
    if velocity_source == "node2d" and _target is Node2D:
        _last_position_y = (_target as Node2D).position.y

    # 常に処理
    set_process(true)


func _process(delta: float) -> void:
    if _target == null or _audio == null:
        return

    var vertical_speed := _get_vertical_speed(delta)
    if debug_print_speed:
        print("WindHowl speed: ", vertical_speed)

    # 上昇中の音を無効にする設定
    if not enable_on_rise and vertical_speed < 0.0:
        _set_target_volume(min_volume_db)
        _update_audio()
        return

    # 地面にいるときはミュートする設定
    if mute_when_grounded and _is_grounded(_target):
        _set_target_volume(min_volume_db)
        _update_audio()
        return

    var abs_speed := absf(vertical_speed)

    # 閾値以下は無音に近くする
    if abs_speed <= min_speed_threshold:
        _set_target_volume(min_volume_db)
    else:
        # min_speed_threshold ~ max_speed_threshold を 0.0 ~ 1.0 に正規化
        var t := (abs_speed - min_speed_threshold) / max(0.001, (max_speed_threshold - min_speed_threshold))
        t = clamp(t, 0.0, 1.0)
        # 線形補間で音量を決定
        var target_volume := lerp(min_volume_db, max_volume_db, t)
        _set_target_volume(target_volume)

    _update_audio()


func _get_vertical_speed(delta: float) -> float:
    # 垂直速度の取得方法をソースごとに分岐
    match velocity_source:
        "character_body":
            # CharacterBody2D / CharacterBody3D を想定
            if "velocity" in _target:
                var v = _target.get("velocity")
                # 2D: Vector2, 3D: Vector3 を想定
                if v is Vector2:
                    return v.y
                elif v is Vector3:
                    return v.y
        "rigid_body":
            # RigidBody2D / RigidBody3D を想定
            if "linear_velocity" in _target:
                var lv = _target.get("linear_velocity")
                if lv is Vector2:
                    return lv.y
                elif lv is Vector3:
                    return lv.y
        "node2d":
            # Node2D の位置差分から速度を推定
            if _target is Node2D and delta > 0.0:
                var node2d := _target as Node2D
                var current_y := node2d.position.y
                var vy := (current_y - _last_position_y) / delta
                _last_position_y = current_y
                return vy

    # 取得できなかった場合は 0 とみなす
    return 0.0


func _is_grounded(target: Node) -> bool:
    # grounded_property_name が設定されていれば、それを参照する
    if grounded_property_name != "" and grounded_property_name in target:
        var v = target.get(grounded_property_name)
        if v is bool:
            return v

    # CharacterBody 系なら is_on_floor() を試す
    if target.has_method("is_on_floor"):
        return target.call("is_on_floor")

    return false


func _set_target_volume(target_db: float) -> void:
    # volume_lerp_factor に応じて、なめらかに音量を変化させる
    _current_volume_db = lerp(_current_volume_db, target_db, volume_lerp_factor)


func _update_audio() -> void:
    _audio.volume_db = _current_volume_db

    # 音量がかなり低いときは、止めておいてもよい
    if _current_volume_db <= min_volume_db + 1.0:
        if _audio.playing:
            _audio.stop()
    else:
        if not _audio.playing and _audio.stream != null:
            _audio.play()

使い方の手順

ここからは、実際に「プレイヤー」「敵」「落下する岩」などに WindHowl を付ける手順を見ていきましょう。

① コンポーネントスクリプトをプロジェクトに追加

  1. res://components/WindHowl.gd など、好きな場所に上記コードを保存します。
  2. Godot エディタを再読み込みすると、スクリプトクラスとして WindHowl がノード追加ダイアログに出てくるようになります。

② プレイヤーにアタッチする例(CharacterBody2D)

典型的な 2D アクションゲームのプレイヤー構成を想定します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Camera2D
 └── WindHowl (Node)
  1. Player シーンを開く。
  2. Player ノードを右クリック →「子ノードを追加」→ 検索欄に「WindHowl」と入力して追加。
  3. インスペクタで WindHowl の設定を行う:
    • Target Settings > target_node:空のままで OK(親の Player を対象にする)
    • velocity_source"character_body"(デフォルト)
    • Audio Settings > wind_stream:ホワイトノイズのループ音を割り当て
    • min_speed_threshold:例)150.0
    • max_speed_threshold:例)800.0
    • mute_when_grounded:true(地面では無音)

プレイヤーが落下すると、Player.velocity.y の値に応じて自動的に風切り音がフェードイン/アウトします。プレイヤースクリプトには一切手を入れていません。

③ 敵キャラに使い回す例(共通ベース不要)

例えば、空から落ちてくる敵「FallingEnemy」を作るとします。

FallingEnemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WindHowl (Node)
  • プレイヤーと同じ WindHowl をそのままアタッチできます。
  • もし敵だけ「もっと大げさな風切り音」にしたいなら、max_volume_db を +3 dB に上げるなど、個別に調整可能。
  • 落下開始のロジックは敵のスクリプト側で完結させておき、WindHowl は「速度だけを見て音を変える」ことに専念します。

継承ベースで BaseFallingCharacter.gd を作ってそこにサウンド処理を入れる……というパターンと比べると、かなりスッキリしますね。

④ 動く足場や落下する岩にも簡単適用

今度は、落下する岩(RigidBody2D)に風切り音を付けてみます。

FallingRock (RigidBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WindHowl (Node)
  1. FallingRock シーンに WindHowl を子ノードとして追加。
  2. 設定を以下のように変更:
    • velocity_source"rigid_body"
    • mute_when_grounded:false(地面にぶつかるまで鳴らしっぱなしにしたい場合)

RigidBody2D の linear_velocity.y を自動で見に行くので、物理挙動に合わせて風切り音が変化します。こちらも岩のスクリプトには一切ロジック追加不要です。

おまけ:Node2D の単純な「落下演出」にも

もし Node2D ベースで手動移動しているオブジェクトなら、位置差分から速度を近似できます。

MovingPlatform (Node2D)
 ├── Sprite2D
 └── WindHowl (Node)
  • velocity_source"node2d" に設定。
  • WindHowl が毎フレーム position.y の差分から速度を推定してくれます。

メリットと応用

WindHowl コンポーネントを導入することで、次のようなメリットがあります。

  • シーン構造がシンプル
    各キャラは「移動」「攻撃」「アニメーション」「サウンド」などを、それぞれ独立したコンポーネントとして子ノードに分割できます。
    「プレイヤー=巨大な God クラス」から卒業できます。
  • 再利用性が高い
    プレイヤー・敵・ギミック(落下する岩、落ちる足場など)に同じ WindHowl を貼るだけで、同じロジックを共有できます。
    「落下速度の計算」「音量マッピング」のような共通処理を一箇所に集約できるので、バランス調整も楽です。
  • 仕様変更に強い
    「風切り音は落下中だけじゃなく、上昇中(ジャンプ中)も鳴らしたい」
    「最大音量に達する速度をもっと遅くしたい」
    といった要望が出ても、WindHowl.gd を1ファイル直すだけで全キャラに反映されます。
  • レベルデザインがやりやすい
    レベルデザイナーが「この敵はもっと怖い感じにしたい」と思ったら、シーン上で max_volume_db だけいじれば OK。
    スクリプトを書かなくても、サウンド演出のチューニングができます。

まさに「継承より合成」のお手本のようなコンポーネントですね。

改造案:高度によるピッチ変化を足す

応用として、「落下速度」だけでなく「高度(地面からの距離)」によってピッチを変えると、よりスリリングな演出ができます。例えば、地面に近づくほどピッチを下げる処理を追加してみましょう。

以下は WindHowl に追加できる、シンプルな改造用関数例です(高さは外部から渡す想定)。


## 高度に応じてピッチを変える簡易改造例
## - current_height: 現在の高度(例: 0 = 地面、1000 = 高い位置)
## - max_height: 想定される最大高度
@export_range(0.5, 2.0, 0.01)
var max_pitch_scale: float = 1.2
@export_range(0.1, 1.0, 0.01)
var min_pitch_scale: float = 0.8

func update_pitch_by_height(current_height: float, max_height: float) -> void:
    if _audio == null:
        return
    var t := clamp(current_height / max_height, 0.0, 1.0)
    # 高いほどピッチ高く、地面に近いほどピッチ低く
    var pitch := lerp(min_pitch_scale, max_pitch_scale, t)
    _audio.pitch_scale = pitch

例えばプレイヤー側で「地面からの高さ」を計算して、この update_pitch_by_height() を呼ぶようにすれば、「高いところから落ちるときは高い風音、地面に近づくと低く重い音」みたいな演出も簡単に実現できます。

こうやって小さな機能をどんどんコンポーネント化していくと、Godot プロジェクト全体の見通しがかなり良くなるので、ぜひ他のサウンド演出にも応用してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!