Godot でアクションゲームやホラーゲームを作っていると、「HP が減ったときだけ心拍音を鳴らしたい」みたいな演出を入れたくなること、ありますよね。
でも、よくある実装だと…
- プレイヤーのスクリプトに HP ロジック + サウンド再生ロジックがベタ書きされて肥大化する
- 敵やボスにも同じような「瀕死時の心拍音」ロジックをコピペしてしまう
- 「やっぱり心拍音のしきい値変えたい」「テンポ変えたい」となったときに、複数スクリプトを探し回る羽目になる
Godot はノード継承で色々できてしまうので、つい Player.gd を太らせがちですが、それを続けるとメンテがしんどくなっていきます。
そこで今回は、HP が一定以下になったときだけ心拍音を鳴らす「HeartbeatSound」コンポーネントを用意して、どのキャラにもポン付けできる構成にしてみましょう。
HP の管理(ダメージ計算など)は別コンポーネントや既存スクリプトに任せて、
このコンポーネントは 「HP の割合を教えてもらって、しきい値以下なら心拍音を鳴らす」ことだけに集中させます。
【Godot 4】瀕死でドクンドクン!「HeartbeatSound」コンポーネント
今回のコンポーネントの特徴はこんな感じです:
- @export でしきい値やテンポを調整可能(HP 30% 以下で鳴らす…など)
- AudioStreamPlayer を内蔵して、コンポーネント単体で完結
- HP の現在値 / 最大値を外部からセットするだけのシンプル API
- 心拍音のピッチを HP に応じて上げたり、間隔を詰めたりできる
「HP 管理」と「演出(心拍音)」を分離することで、プレイヤーやボス、特殊なギミックにも同じコンポーネントを再利用できます。
GDScript フルコード
extends Node
class_name HeartbeatSound
## HP が一定以下になると「ドクン…ドクン…」という心拍音を再生するコンポーネント
##
## 想定使い方:
## - プレイヤーやボスなどのシーンに、このノードを子として追加
## - HP の現在値と最大値を外部(HP コンポーネントなど)から set_hp_ratio() で渡す
## - しきい値以下になると、自動で一定間隔で心拍音を再生する
@export_category("Heartbeat Settings")
## HP がこの割合以下になったら心拍音を有効化(0.0〜1.0)
@export_range(0.0, 1.0, 0.01)
var low_hp_threshold: float = 0.3
## 心拍音の基本再生間隔(秒)
@export_range(0.1, 5.0, 0.1)
var base_interval_sec: float = 1.2
## HP が低いほど間隔を短くするかどうか
@export var scale_interval_by_hp: bool = true
## 最短間隔(scale_interval_by_hp が true のときに使用)
@export_range(0.1, 5.0, 0.1)
var min_interval_sec: float = 0.4
## しきい値以下のときのピッチ倍率(1.0 が等倍)
@export_range(0.1, 3.0, 0.05)
var base_pitch_scale: float = 1.0
## HP が低いほどピッチを上げるかどうか
@export var scale_pitch_by_hp: bool = true
## ピッチの最大倍率(HP が 0% に近いときにこの値に近づく)
@export_range(0.1, 3.0, 0.05)
var max_pitch_scale: float = 1.4
@export_category("Audio")
## 再生する心拍音の AudioStream
@export var heartbeat_stream: AudioStream
## 3D / 2D 問わず簡単に使えるよう、AudioStreamPlayer を内部で生成して使う
var _player: AudioStreamPlayer
var _timer: Timer
## 0.0〜1.0 の HP 割合(外部から更新してもらう)
var _hp_ratio: float = 1.0
## 現在「瀕死状態(心拍音有効)」かどうか
var _is_low_hp: bool = false
func _ready() -> void:
# 内部用 AudioStreamPlayer を作成
_player = AudioStreamPlayer.new()
_player.name = "HeartbeatPlayer"
_player.stream = heartbeat_stream
_player.autoplay = false
add_child(_player)
# 心拍音の再生タイマー
_timer = Timer.new()
_timer.name = "HeartbeatTimer"
_timer.one_shot = true
_timer.wait_time = base_interval_sec
_timer.timeout.connect(_on_timer_timeout)
add_child(_timer)
# 初期状態を反映
_update_state_from_hp()
# エディタ上でプレビューしやすいよう、エディタ再生時も動く
process_mode = Node.PROCESS_MODE_INHERIT
## 外部から HP を更新するための API
## 現在 HP と最大 HP を渡すと、内部で 0.0〜1.0 に正規化して扱う
func set_hp(current_hp: float, max_hp: float) -> void:
if max_hp <= 0.0:
_hp_ratio = 1.0
else:
_hp_ratio = clamp(current_hp / max_hp, 0.0, 1.0)
_update_state_from_hp()
## すでに 0.0〜1.0 に正規化された HP 割合を渡したい場合はこちら
func set_hp_ratio(ratio: float) -> void:
_hp_ratio = clamp(ratio, 0.0, 1.0)
_update_state_from_hp()
## HP 状態から「瀕死フラグ」とタイマー・ピッチなどを更新
func _update_state_from_hp() -> void:
var was_low = _is_low_hp
_is_low_hp = (_hp_ratio <= low_hp_threshold)
if _is_low_hp:
# 瀕死状態になったらタイマーを起動(まだ動いていなければ)
if not _timer.is_stopped():
# すでに動いているなら何もしない
pass
else:
_update_timer_interval()
_update_player_pitch()
_timer.start()
else:
# 瀕死から回復したらタイマー停止&心拍音停止
_timer.stop()
if _player.playing:
_player.stop()
# 状態が変わったタイミングでデバッグログを出したい場合はここで
# if was_low != _is_low_hp:
# print("Heartbeat low HP state changed: ", _is_low_hp)
## HP に応じて心拍音の再生間隔を調整
func _update_timer_interval() -> void:
if not scale_interval_by_hp:
_timer.wait_time = base_interval_sec
return
# HP が低いほど間隔を短くする
# 例:threshold=0.3, ratio=0.15 のとき → low_side = 0.5
var low_side := 0.0
if low_hp_threshold > 0.0:
low_side = 1.0 - (_hp_ratio / low_hp_threshold)
low_side = clamp(low_side, 0.0, 1.0)
# low_side=0 → base_interval_sec
# low_side=1 → min_interval_sec
var interval := lerp(base_interval_sec, min_interval_sec, low_side)
_timer.wait_time = max(interval, 0.05)
## HP に応じてピッチを調整
func _update_player_pitch() -> void:
if not scale_pitch_by_hp:
_player.pitch_scale = base_pitch_scale
return
var low_side := 0.0
if low_hp_threshold > 0.0:
low_side = 1.0 - (_hp_ratio / low_hp_threshold)
low_side = clamp(low_side, 0.0, 1.0)
var pitch := lerp(base_pitch_scale, max_pitch_scale, low_side)
_player.pitch_scale = pitch
## タイマーが発火したら、心拍音を 1 回再生し、次のタイマーをセット
func _on_timer_timeout() -> void:
if not _is_low_hp:
return
# ストリームが設定されていなければ何もしない
if not heartbeat_stream:
push_warning("HeartbeatSound: heartbeat_stream is not set.")
return
# すでに再生中でも、毎回先頭から鳴らす
_player.stream = heartbeat_stream
_update_player_pitch()
_player.play()
# 次の再生までの間隔を再計算してタイマー再スタート
_update_timer_interval()
_timer.start()
## ゲーム全体を一時停止したいときに外部から呼べるように
func pause_heartbeat() -> void:
_timer.stop()
if _player.playing:
_player.stop()
## 一時停止からの再開
func resume_heartbeat() -> void:
if _is_low_hp:
_update_timer_interval()
_timer.start()
使い方の手順
手順①:心拍音用の AudioStream を用意する
「ドクン」というワンショットの音源(短いドラム + 低音など)を用意して、
プロジェクトにインポートしておきます。ファイル例:
res://audio/sfx/heartbeat.wav
ループ音ではなく、1 回鳴らして止まるタイプの音にしておくと扱いやすいです。
手順②:プレイヤーシーンに HeartbeatSound を追加
プレイヤーのシーン構成例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── HPComponent (Node) ※ HP 管理用、既存でも OK └── HeartbeatSound (Node) ※ 今回のコンポーネント
エディタ上で:
- Player シーンを開く
- 子ノードとして
Nodeを追加し、スクリプトにHeartbeatSound.gdをアタッチ - インスペクタで以下を設定
- heartbeat_stream に心拍音の AudioStream を指定
- low_hp_threshold を 0.3(HP 30% 以下で発動)などに調整
- テンポを変えたい場合は base_interval_sec と min_interval_sec を調整
手順③:HP コンポーネント or スクリプトから HP を渡す
プレイヤー側のスクリプト例(超シンプルな HP 管理):
extends CharacterBody2D
@export var max_hp: int = 100
var current_hp: int = 100
@onready var heartbeat: HeartbeatSound = $HeartbeatSound
func _ready() -> void:
# 初期 HP を反映
heartbeat.set_hp(current_hp, max_hp)
func apply_damage(amount: int) -> void:
current_hp = max(current_hp - amount, 0)
heartbeat.set_hp(current_hp, max_hp)
if current_hp == 0:
die()
func die() -> void:
# 死亡処理いろいろ
heartbeat.pause_heartbeat()
このように、ダメージ処理の中で HP 値を HeartbeatSound に渡すだけで、
HP がしきい値以下になったときに自動で心拍音が鳴り始めます。
手順④:敵やボスにもそのまま再利用
敵シーンの構成例:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── EnemyAI (Node / Script) └── HeartbeatSound (Node) ※ 同じコンポーネントを再利用
敵用スクリプトからも同様に:
@onready var heartbeat: HeartbeatSound = $HeartbeatSound
func _ready() -> void:
heartbeat.set_hp(current_hp, max_hp)
func take_damage(amount: float) -> void:
current_hp = max(current_hp - amount, 0.0)
heartbeat.set_hp(current_hp, max_hp)
これで、プレイヤーと敵でまったく同じ HeartbeatSound コンポーネントを共有できます。
「HP が減ったら心拍音を鳴らす」という仕様が変わっても、コンポーネント側を 1 箇所直すだけで全体に反映されるのが合成の強みですね。
メリットと応用
- プレイヤーのスクリプトがスリムになる
HP 管理、移動、攻撃、UI 更新…といった責務から「演出用の心拍音」を切り離せます。 - シーン構造がフラットで見通しが良い
「心拍音は HeartbeatSound ノードが担当」と一目で分かるので、後から見ても理解しやすいです。 - ボスや特殊ギミックにも簡単に転用できる
HP の概念さえあれば、どのシーンにもポン付けできます。HP ではなく「耐久値」「スタミナ」でも同じインターフェースで流用可能です。 - パラメータ調整が楽
しきい値、テンポ、ピッチをインスペクタから変えるだけで、ゲーム全体の「瀕死演出」の雰囲気を一括で変えられます。
さらに応用として、例えば「HP が減るほど音量も上がる」ようにしても面白いです。
以下のような関数を追加してみましょう。
## HP が低いほど音量(dB)を上げる
func _update_player_volume() -> void:
# low_hp_threshold 以下のときだけボリューム変化させる
if _hp_ratio > low_hp_threshold:
_player.volume_db = 0.0
return
# 0% で +6dB, しきい値で 0dB くらいのイメージ
var low_side := 0.0
if low_hp_threshold > 0.0:
low_side = 1.0 - (_hp_ratio / low_hp_threshold)
low_side = clamp(low_side, 0.0, 1.0)
var volume_db := lerp(0.0, 6.0, low_side)
_player.volume_db = volume_db
この関数を _update_state_from_hp() や _on_timer_timeout() の中から呼べば、
HP が減るほど「近づいてくる恐怖」感のある心拍音にできます。
継承で「PlayerWithHeartbeat」「BossWithHeartbeat」みたいにクラスを増やすより、
こういうコンポーネントをポンポン組み合わせていく方が、Godot 4 では圧倒的に楽ですね。
ぜひ、自分のプロジェクトにも HeartbeatSound コンポーネントを差し込んでみてください。




