【Godot 4】EarRinging (耳鳴り) コンポーネントの作り方

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時点)

FPSやアクションゲームで「手榴弾を近距離で食らった直後、しばらく耳鳴りだけになる」演出、入れたくなりますよね。
Godotで素直に実装しようとすると、

  • プレイヤーシーンに直接オーディオ処理を書く
  • ダメージ処理のスクリプトからBGMやSEのボリュームをいじる
  • AudioBusLayoutを都度書き換える

…みたいに「プレイヤー」「ダメージ処理」「サウンド管理」がベッタリ結合してしまいがちです。
継承で頑張ると、今度は PlayerWithEarRinging みたいなクラスが増えていって管理がつらくなります。

そこで今回は、「耳鳴り演出」だけを独立したコンポーネントに切り出した EarRinging コンポーネント を用意します。
プレイヤーでも敵でも、カメラに追従する AudioListener でも、必要なノードにペタッとアタッチするだけで耳鳴り演出を合成できるようにしていきましょう。

【Godot 4】爆風の余韻をコンポーネントで!「EarRinging」コンポーネント

この EarRinging コンポーネントは、ざっくり言うと:

  • 耳鳴り用の AudioStreamPlayer を自動生成
  • 指定したバス(例: Master)のボリュームを一時的に下げる
  • 耳鳴り音を再生しながら、元のボリュームにフェードバック
  • trigger() を呼ぶだけで発動

という「音響演出のまとまり」です。
プレイヤーの HP やダメージ計算は一切知らなくてよくて、「大ダメージが発生した」というイベント側から ear_ringing.trigger() を呼んであげるだけです。


フルコード:EarRinging.gd


extends Node
class_name EarRinging
## 大ダメージを受けた直後に「キーン」という耳鳴り演出を行うコンポーネント。
## - 対象オーディオバスのボリュームを一時的に下げる
## - 耳鳴り用のAudioStreamを再生する
## - 時間経過とともに元のボリュームにフェードバックする
##
## 使い方の概要:
##   1. プレイヤー(またはカメラ等)の子ノードとしてこのコンポーネントを追加
##   2. Inspectorで耳鳴りSEや各種パラメータを設定
##   3. ダメージ処理などから `ear_ringing.trigger()` を呼ぶ

@export_category("基本設定")
## 耳鳴りSE。ループしない短めの「キーン」音を推奨。
@export var ringing_stream: AudioStream

## 耳鳴りを鳴らすオーディオバス名。
## 通常は "Master" だが、BGMだけ残したいなら "SFX" などに分けておくと良い。
@export var target_bus: StringName = "Master"

## 耳鳴りの総時間(秒)。
## この時間をかけて、ボリュームを元の値に戻していく。
@export_range(0.1, 10.0, 0.1)
@export var duration: float = 3.0

## 耳鳴り中に下げるボリューム量(dB)。
## 0 に近いほど元の音が残り、-80 に近いほど「ほぼ耳鳴りだけ」になる。
@export_range(-80.0, 0.0, 0.5)
@export var attenuation_db: float = -20.0

## 耳鳴りの立ち上がり時間(秒)。
## 0 にすると即座にボリュームが下がる。0.1〜0.3くらいにすると自然。
@export_range(0.0, 2.0, 0.05)
@export var attack_time: float = 0.1

## 耳鳴りの余韻(リリース)時間(秒)。
## duration の最後のこの時間で、よりなめらかに元の音量へ戻す。
@export_range(0.0, 2.0, 0.05)
@export var release_time: float = 0.5

@export_category("トリガー条件")
## 連続してダメージを受けたとき、次の耳鳴りを無視するクールダウン時間(秒)。
## 0 にすると連打可能だが、うるさくなりやすい。
@export_range(0.0, 10.0, 0.1)
@export var cooldown_time: float = 1.5

## すでに耳鳴り中に再度 trigger() された場合の挙動。
## true: タイマーをリセットして耳鳴りを延長する
## false: 無視する
@export var extend_when_triggered_while_active: bool = true

@export_category("デバッグ")
## テスト用に、シーン再生直後に自動で耳鳴りを発生させる。
@export var test_on_start: bool = false

## 実際に耳鳴りSEを鳴らすプレイヤー
var _player: AudioStreamPlayer
## 対象バスのインデックス
var _bus_index: int = 0
## 元のボリューム(dB)
var _original_bus_volume_db: float = 0.0
## 耳鳴りの経過時間
var _time: float = 0.0
## 現在耳鳴り中かどうか
var _is_active: bool = false
## 前回の耳鳴り終了時刻
var _last_end_time: float = -1000.0

func _ready() -> void:
    # バスのインデックスを取得
    _bus_index = AudioServer.get_bus_index(target_bus)
    if _bus_index == -1:
        push_warning("EarRinging: Bus '%s' が見つかりません。Master を使用します。" % target_bus)
        _bus_index = AudioServer.get_bus_index(&"Master")
        target_bus = &"Master"

    # 元のボリュームを記録
    _original_bus_volume_db = AudioServer.get_bus_volume_db(_bus_index)

    # 耳鳴り用のAudioStreamPlayerを自動生成
    _player = AudioStreamPlayer.new()
    _player.bus = target_bus
    _player.autoplay = false
    _player.stream = ringing_stream
    add_child(_player)

    set_process(true)

    if test_on_start:
        trigger()

func _process(delta: float) -> void:
    if not _is_active:
        return

    _time += delta
    var t := _time / max(duration, 0.001)
    t = clamp(t, 0.0, 1.0)

    # Attack / Release を考慮したカーブを作る
    var target_db := _compute_volume_db(t)

    AudioServer.set_bus_volume_db(_bus_index, target_db)

    if t >= 1.0:
        # フェードアウト完了
        _finish_effect()

## 外部から呼び出して耳鳴りを発生させる。
## 例: ダメージ処理の中で
##   if damage > 50:
##       $EarRinging.trigger()
func trigger() -> void:
    var now := Time.get_ticks_msec() / 1000.0

    # クールダウンチェック
    if not _is_active:
        if now - _last_end_time < cooldown_time:
            return
    else:
        # すでにアクティブな場合
        if not extend_when_triggered_while_active:
            return

    # 初期化
    _time = 0.0
    _is_active = true

    # 再度元のボリュームを読み直しておく(他で変更されている可能性に備える)
    _original_bus_volume_db = AudioServer.get_bus_volume_db(_bus_index)

    # 耳鳴りSEを再生
    if ringing_stream:
        _player.stream = ringing_stream
        _player.play()
    else:
        push_warning("EarRinging: ringing_stream が設定されていません。音は鳴りません。")

## t: 0.0〜1.0 の正規化時間から、現在のバスボリューム(dB)を計算する。
func _compute_volume_db(t: float) -> float:
    # Attack フェーズ: 0〜attack_time
    var attack_ratio := 0.0
    if attack_time > 0.0:
        attack_ratio = clamp(_time / attack_time, 0.0, 1.0)
    else:
        attack_ratio = 1.0

    # Release フェーズ: duration - release_time 〜 duration
    var release_ratio := 0.0
    if release_time > 0.0 and _time > duration - release_time:
        var rel_t := (_time - (duration - release_time)) / release_time
        release_ratio = clamp(rel_t, 0.0, 1.0)
    else:
        release_ratio = 0.0

    # Attackで一気に下げ、Releaseで戻すイメージ
    var down_db := lerp(0.0, attenuation_db, attack_ratio)
    var up_db := lerp(attenuation_db, 0.0, release_ratio)

    # Attack中はdown_db優先、Release中はup_db優先、それ以外はattenuation_db固定
    var current_offset_db := attenuation_db
    if attack_ratio < 1.0:
        current_offset_db = down_db
    elif release_ratio > 0.0:
        current_offset_db = up_db

    return _original_bus_volume_db + current_offset_db

## 耳鳴り終了処理
func _finish_effect() -> void:
    _is_active = false
    _last_end_time = Time.get_ticks_msec() / 1000.0
    _time = 0.0

    # ボリュームを元に戻す
    AudioServer.set_bus_volume_db(_bus_index, _original_bus_volume_db)

    # 念のためプレイヤーも止める
    if _player.playing:
        _player.stop()

使い方の手順

ここでは 2D の FPS っぽいプレイヤーを例にしますが、3D でも考え方は同じです。

① シーンに EarRinging を追加する

プレイヤーシーンにコンポーネントとして追加します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Camera2D
 └── EarRinging (Node)  <-- このスクリプトをアタッチ
  1. Godot で Player シーンを開く
  2. Player の子として Node を追加し、名前を EarRinging に変更
  3. その Node に上記 EarRinging.gd をアタッチ

3D ならこんな感じですね:

Player3D (CharacterBody3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 ├── Camera3D
 └── EarRinging (Node)

② Audio バスの準備

プロジェクト設定で、音をバスに分けておくと調整しやすいです。

  • Master … 全体
  • BGM … BGM 用
  • SFX … 効果音用

今回は「効果音だけ耳鳴りにしたい」なら EarRinging の target_bus"SFX" に、
「ゲーム全体を耳鳴り状態にしたい」なら "Master" のままにしておきましょう。

③ パラメータを設定する

Inspector から以下を調整します:

  • ringing_stream … 耳鳴り用の「キーン」SE を指定
  • duration … 2.5〜4.0 秒くらいがゲーム的に気持ちいいです
  • attenuation_db … -20〜-40 dB くらいで「ほぼ耳鳴りだけ」に
  • attack_time … 0.1〜0.3 秒くらいで自然な立ち上がり
  • release_time … 0.4〜0.8 秒くらいで余韻を演出
  • cooldown_time … 1.0〜2.0 秒で連続爆風のときのうるささを軽減

テスト時にすぐ確認したい場合は test_on_startOn にしておくと、シーン再生直後に耳鳴りが発生します。

④ ダメージ処理から trigger() を呼ぶ

最後に、「どのタイミングで耳鳴りを発生させるか」を決めます。
たとえばプレイヤーのダメージ処理スクリプトがこんな感じだとします:


# Player.gd (一例)
extends CharacterBody2D

@onready var ear_ringing: EarRinging = $EarRinging

var hp: int = 100

func apply_damage(amount: int, source: Node = null) -> void:
    hp -= amount
    if hp <= 0:
        die()
        return

    # 一定以上のダメージで耳鳴りを発生させる
    if amount >= 40:
        ear_ringing.trigger()

グレネードの爆風などで apply_damage(60, grenade) を呼べば、耳鳴りコンポーネントが発動します。
ここで重要なのは、Player は「耳鳴りの実装詳細」を一切知らないことです。
「大ダメージのときに何かしらの演出をする」= ear_ringing.trigger() というインターフェイスだけを意識すればOKですね。


メリットと応用

コンポーネント化のメリット

  • プレイヤーの継承ツリーが増えない
    PlayerWithEarRinging みたいな派生クラスを作らなくて済みます。
    ただの PlayerEarRinging コンポーネントを「合成」するだけです。
  • シーン構造が素直
    音響演出はすべて EarRinging 内で完結しているので、他のスクリプトから AudioServer を触る必要がありません。
  • 使い回しが簡単
    敵キャラにも同じコンポーネントをつければ、「敵が爆風を受けたときだけ耳鳴り」などもすぐ実現できます。
    (その場合はカメラ側の AudioListener とどう連携するかを考える必要はありますが、ロジック自体は使い回せます)
  • バランス調整が楽
    耳鳴りの長さやボリュームはすべて Inspector から変更できるので、ゲームデザイナーが自分でいじれます。

応用アイデア

  • HP が低いときに常に小さな耳鳴りを鳴らす「瀕死状態演出」
  • 水中に入ったときに高音をカットする「水中フィルタ」コンポーネント
  • スタングレネード専用の「ホワイトアウト+耳鳴り」複合コンポーネント

いずれも、「プレイヤーに何かが起きたときに AudioBus を一時的にいじる」というパターンなので、
今回の EarRinging をベースにいろいろと派生させていけます。

改造案:距離に応じて耳鳴りの強さを変える

例えば「爆心地からの距離によって耳鳴り時間を変えたい」場合、
trigger() に距離を渡して、内部で duration を調整するようにしても良いですね。


## 距離に応じて耳鳴りを発生させる例
## distance: 爆心地からの距離
## max_distance: それ以上なら耳鳴りなし
func trigger_with_distance(distance: float, max_distance: float) -> void:
    if distance >= max_distance:
        return

    # 0.0 (至近距離) 〜 1.0 (最大距離) に正規化
    var ratio := clamp(1.0 - (distance / max_distance), 0.0, 1.0)

    # 至近距離ならフル時間、遠いほど短くする
    var min_duration := 0.5
    var max_duration := 4.0
    duration = lerp(min_duration, max_duration, ratio)

    trigger()

グレネード側からはこんな感じで呼べます:


# Grenade.gd の爆発処理
func _explode() -> void:
    for body in _get_bodies_in_radius():
        if body.has_node("EarRinging"):
            var ear: EarRinging = body.get_node("EarRinging")
            var dist := global_position.distance_to(body.global_position)
            ear.trigger_with_distance(dist, 300.0)

こうしておくと、「継承より合成」で耳鳴り演出をどんどん拡張していけますね。
サウンド演出をコンポーネントとして切り出しておくと、プロジェクト後半の調整が本当にラクになるので、ぜひ試してみてください。

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をコピーしました!