Godot 4でアクションゲームを作っていると、「HPが減ったときにだけ音をこもらせたい(ローパス)」って演出、やりたくなりますよね。
でも素直にやろうとすると…
- プレイヤーシーンのスクリプトが「HP管理」「移動」「入力」「エフェクト」「音の演出」などで巨大化
- AudioBusやEffectの操作コードが、HPロジックとベタ書きで結合してしまう
- 敵やボスなど、別のHP持ちキャラにも同じ演出を入れたくなったときにコピペ地獄
Godot標準の「プレイヤースクリプトで全部やる」方式だと、どうしても継承と巨大クラスに寄っていきがちです。
そこで今回は、HPコンポーネントを監視して、瀕死時にだけローパスフィルターをかける専用コンポーネントを用意して、プレイヤーにも敵にもポン付けできる形にしてしまいましょう。
名前はそのまま、「LowHealthAudio(瀕死音効)」コンポーネントです。
【Godot 4】HPが減ったら世界がこもる!「LowHealthAudio」コンポーネント
このコンポーネントは、ざっくり言うと:
- 指定した「HPコンポーネント」を監視
- HPが一定割合以下になったら、指定のAudioBusにローパスフィルターをオン
- HPが回復して閾値を上回ったら、ローパスを解除
- フェード時間やカットオフ周波数も@exportで調整可能
「HPの概念」と「音の演出」をキレイに分離できるので、コンポーネント指向な構成にぴったりです。
フルコード:LowHealthAudio.gd
extends Node
class_name LowHealthAudio
## 瀕死時にAudioBusへローパスフィルターをかけるコンポーネント
##
## 想定:
## - HPを管理する別コンポーネント(例: HealthComponent)を監視
## - HPが一定割合以下になったらローパスON
## - HPが回復して閾値を上回ったらローパスOFF
##
## 必要条件:
## - GodotのAudioBusLayoutで、対象BusにAudioEffectLowPassFilterを追加しておくこと
@export_node_path("Node") var health_node_path: NodePath
## 監視対象のHPコンポーネントへのパス
## 例: "../HealthComponent"
## コンポーネント側が signal health_changed(current, max) を持っている前提です。
@export var low_health_threshold: float = 0.3
## 瀕死判定のHP割合(0.0〜1.0)
## current_hp / max_hp がこの値以下になったら「瀕死」とみなします。
@export var audio_bus_name: String = "Master"
## ローパスをかけるAudioBusの名前
## プロジェクト設定のAudioタブでBusの名前を確認して合わせてください。
@export var lowpass_cutoff_hz_healthy: float = 20000.0
## 通常時のカットオフ周波数(実質ローパス無効状態)
@export var lowpass_cutoff_hz_low: float = 800.0
## 瀕死時のカットオフ周波数(値を下げるほど「こもった音」になります)
@export var transition_time: float = 0.3
## ローパス切り替え時のフェード時間(秒)
## 0 だと即時切り替え、0.2〜0.5 くらいが自然です。
@export var auto_disable_when_dead: bool = true
## HPが0になったときに、このコンポーネント自体を無効化するかどうか
## 死亡演出が別である場合などに便利です。
# 内部状態
var _health_component: Node = null
var _bus_index: int = -1
var _lowpass_effect: AudioEffectLowPassFilter = null
var _tween: Tween = null
var _is_low_health: bool = false
var _is_dead: bool = false
func _ready() -> void:
# HPコンポーネントの取得
if health_node_path != NodePath(""):
_health_component = get_node_or_null(health_node_path)
if _health_component == null:
push_warning("LowHealthAudio: health_node_path が無効です。HPコンポーネントが見つかりません。")
else:
# HealthComponent 側に health_changed シグナルがある前提
# シグナルシグネチャ: signal health_changed(current: float, max: float)
if _health_component.has_signal("health_changed"):
_health_component.connect("health_changed", Callable(self, "_on_health_changed"))
else:
push_warning("LowHealthAudio: 監視対象に 'health_changed' シグナルがありません。")
# 可能なら初期HP状態を読んで、開始時の状態を反映
if _health_component.has_method("get_current_health") and _health_component.has_method("get_max_health"):
var cur: float = _health_component.call("get_current_health")
var max_val: float = _health_component.call("get_max_health")
_update_state_from_health(cur, max_val)
# AudioBus と LowPassEffect の取得
_bus_index = AudioServer.get_bus_index(audio_bus_name)
if _bus_index == -1:
push_warning("LowHealthAudio: AudioBus '%s' が見つかりません。" % audio_bus_name)
return
# バスに追加済みの AudioEffectLowPassFilter を探す
var effect_count := AudioServer.get_bus_effect_count(_bus_index)
for i in effect_count:
var eff := AudioServer.get_bus_effect(_bus_index, i)
if eff is AudioEffectLowPassFilter:
_lowpass_effect = eff
break
if _lowpass_effect == null:
push_warning("LowHealthAudio: Bus '%s' に AudioEffectLowPassFilter がありません。" % audio_bus_name)
return
# 初期状態を通常時に合わせる
_lowpass_effect.cutoff_hz = lowpass_cutoff_hz_healthy
func _on_health_changed(current: float, max_value: float) -> void:
if max_value <= 0.0:
return
_update_state_from_health(current, max_value)
func _update_state_from_health(current: float, max_value: float) -> void:
var ratio: float = current / max_value
# 死亡判定
if current <= 0.0:
_is_dead = true
if auto_disable_when_dead:
# 死亡時はローパスを解除してから自分を無効化
_set_low_health_state(false)
set_process(false)
return
var new_is_low_health: bool = ratio <= low_health_threshold
_set_low_health_state(new_is_low_health)
func _set_low_health_state(enable: bool) -> void:
if _is_low_health == enable:
return
_is_low_health = enable
if _lowpass_effect == null:
return
var target_cutoff: float = enable ? lowpass_cutoff_hz_low : lowpass_cutoff_hz_healthy
# すでにTweenがあれば止める
if _tween and _tween.is_valid():
_tween.kill()
if transition_time <= 0.0:
# 即時反映
_lowpass_effect.cutoff_hz = target_cutoff
return
# Tweenでなめらかに遷移
_tween = create_tween()
_tween.tween_property(
_lowpass_effect,
"cutoff_hz",
target_cutoff,
transition_time
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
## 手動でローパス状態を強制するためのヘルパー(デバッグ用)
func force_low_health(enable: bool) -> void:
_set_low_health_state(enable)
## 現在のローパス状態を返す(瀕死扱いかどうか)
func is_low_health_active() -> bool:
return _is_low_health
使い方の手順
ここでは、典型的な「プレイヤー + HealthComponent + LowHealthAudio」の構成を例にします。
敵キャラでもボスでも、同じパターンでOKです。
シーン構成例
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── HealthComponent (Node) # HP管理コンポーネント(例) └── LowHealthAudio (Node) # 今回作った瀕死音効コンポーネント
※HealthComponent は以下のような最低限のインターフェースを持つ想定です:
signal health_changed(current: float, max: float)func get_current_health() -> floatfunc get_max_health() -> float
参考用のシンプルな HealthComponent 例:
extends Node
class_name HealthComponent
signal health_changed(current: float, max: float)
@export var max_health: float = 100.0
var current_health: float
func _ready() -> void:
current_health = max_health
emit_signal("health_changed", current_health, max_health)
func damage(amount: float) -> void:
current_health = max(current_health - amount, 0.0)
emit_signal("health_changed", current_health, max_health)
func heal(amount: float) -> void:
current_health = min(current_health + amount, max_health)
emit_signal("health_changed", current_health, max_health)
func get_current_health() -> float:
return current_health
func get_max_health() -> float:
return max_health
手順①:AudioBusにローパスフィルターを追加する
- メインメニューの Project > Project Settings > Audio > Bus Layout… を開く
- 対象の Bus(例:
MasterやGame)を選択 - Add Effect ボタンから
AudioEffectLowPassFilterを追加 - 初期の
Cutoff Hzは 20000Hz など高めにしておく(実質オフ状態)
ここで指定した Bus 名を、LowHealthAudio.audio_bus_name に設定します。
手順②:HealthComponent をキャラクターに追加
- プレイヤーシーン(例:
Player.tscn)を開く - ルートの
CharacterBody2Dの子としてNodeを追加し、HealthComponent.gdをアタッチ - インスペクタで
max_healthを設定(例: 100)
手順③:LowHealthAudio コンポーネントを追加
- 同じくプレイヤーの子として
Nodeを追加し、LowHealthAudio.gdをアタッチ - インスペクタで以下を設定:
- health_node_path:
../HealthComponent(実際のパスに合わせてください) - low_health_threshold: 0.3(HP30%以下で瀕死扱い)
- audio_bus_name:
Masterなど、ローパスを追加したBus名 - lowpass_cutoff_hz_low: 600〜1200 くらいが「こもってる感」出やすいです
- transition_time: 0.3〜0.5 くらいで自然なフェード
- health_node_path:
手順④:実際にHPを減らして動作確認
例えばプレイヤースクリプト側で、デバッグ用にキー入力でダメージを与えてみます:
extends CharacterBody2D
@onready var health: HealthComponent = $HealthComponent
func _process(delta: float) -> void:
# デバッグ: Hキーで10ダメージ
if Input.is_action_just_pressed("debug_damage"):
health.damage(10.0)
HPが30%を切ったあたりで、ゲーム全体の音が「ふっと」こもるのが分かるはずです。
この時、プレイヤー側のスクリプトはHPの数値を扱っているだけで、「ローパスをどうするか」の知識を一切持っていません。
音の演出は LowHealthAudio コンポーネントに丸投げできているのがポイントですね。
メリットと応用
このコンポーネントを使うことで、いくつか嬉しい点があります。
- HPロジックと音演出の分離
HealthComponent は「HPの数値とシグナル」だけに集中できて、
LowHealthAudio は「HP割合を見てローパス制御」だけに集中できます。 - プレイヤー以外にも即使い回し
敵、ボス、味方NPCなど、HPを持つキャラなら同じコンポーネントをペタっと貼るだけ。
それぞれのシーンに LowHealthAudio を追加して health_node_path を合わせればOKです。 - シーン構造が浅くシンプル
「Player.gd が全部やる」方式だと、スクリプトがどんどん太りますが、
コンポーネントを子ノードとしてぶら下げるだけなら、機能ごとに見通しが良くなります。 - AudioBus側の演出とも相性抜群
ローパス以外にも、同じBusにリバーブやコンプレッサーを足して、
「瀕死時はローパス+リバーブを強める」など、Bus設計でさらに遊べるようになります。
「HPが減ったらローパスをかける」という処理は、継承ベースでやるとPlayerWithLowHealthEffect.gd みたいなクラスが増えがちですが、
コンポーネントとして切り出しておけば、「HPを持つあらゆるキャラに後付け」できるのが強みですね。
改造案:ローパス強度をHP割合に応じて連続的に変化させる
今の実装は「閾値を境にON/OFF」ですが、
「HPが減るほど徐々にこもる」ようにしたい場合は、こんな関数を追加してみるのもアリです。
## HP割合に応じてローパスのカットオフを連続的に変化させる例
## ratio: 0.0 = 瀕死, 1.0 = 全快
func _apply_continuous_lowpass(ratio: float) -> void:
if _lowpass_effect == null:
return
# 0.0〜1.0 を反転させて「減るほど強くかかる」ようにする
var t := 1.0 - clamp(ratio, 0.0, 1.0)
# t=0 で healthy, t=1 で low の間を補間
var target_cutoff := lerp(lowpass_cutoff_hz_healthy, lowpass_cutoff_hz_low, t)
if _tween and _tween.is_valid():
_tween.kill()
_tween = create_tween()
_tween.tween_property(
_lowpass_effect,
"cutoff_hz",
target_cutoff,
transition_time
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
この場合は _update_state_from_health() 内で ratio をそのまま渡して呼び出すようにすれば、
「瀕死に近づくほど徐々に世界がこもっていく」演出になります。
こんな感じで、音の演出もコンポーネント化して合成していくと、
「プレイヤーのスクリプトに全部書く」よりも、ずっと育てやすいプロジェクトになりますね。
ぜひ自分のゲームの AudioBus 設計と組み合わせて、LowHealthAudio をカスタマイズしてみてください。
