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() -> float
  • func 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にローパスフィルターを追加する

  1. メインメニューの Project > Project Settings > Audio > Bus Layout… を開く
  2. 対象の Bus(例: MasterGame)を選択
  3. Add Effect ボタンから AudioEffectLowPassFilter を追加
  4. 初期の Cutoff Hz は 20000Hz など高めにしておく(実質オフ状態)

ここで指定した Bus 名を、LowHealthAudio.audio_bus_name に設定します。

手順②:HealthComponent をキャラクターに追加

  1. プレイヤーシーン(例: Player.tscn)を開く
  2. ルートの CharacterBody2D の子として Node を追加し、HealthComponent.gd をアタッチ
  3. インスペクタで max_health を設定(例: 100)

手順③:LowHealthAudio コンポーネントを追加

  1. 同じくプレイヤーの子として Node を追加し、LowHealthAudio.gd をアタッチ
  2. インスペクタで以下を設定:
    • 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 くらいで自然なフェード

手順④:実際に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 をカスタマイズしてみてください。