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 を付ける手順を見ていきましょう。
① コンポーネントスクリプトをプロジェクトに追加
res://components/WindHowl.gdなど、好きな場所に上記コードを保存します。- Godot エディタを再読み込みすると、スクリプトクラスとして
WindHowlがノード追加ダイアログに出てくるようになります。
② プレイヤーにアタッチする例(CharacterBody2D)
典型的な 2D アクションゲームのプレイヤー構成を想定します。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Camera2D └── WindHowl (Node)
- Player シーンを開く。
- Player ノードを右クリック →「子ノードを追加」→ 検索欄に「WindHowl」と入力して追加。
- インスペクタで WindHowl の設定を行う:
Target Settings > target_node:空のままで OK(親の Player を対象にする)velocity_source:"character_body"(デフォルト)Audio Settings > wind_stream:ホワイトノイズのループ音を割り当てmin_speed_threshold:例)150.0max_speed_threshold:例)800.0mute_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)
- FallingRock シーンに WindHowl を子ノードとして追加。
- 設定を以下のように変更:
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 プロジェクト全体の見通しがかなり良くなるので、ぜひ他のサウンド演出にも応用してみてください。




