【Cocos Creator 3.8】HeartbeatSound の実装:アタッチするだけで「HPが減ったときのドクンドクン心拍音」を自動再生する汎用スクリプト
このコンポーネントは、HPが一定値以下になると「ドクン…ドクン…」という心拍音をループ再生し、危機感を演出するための汎用スクリプトです。
HP値の管理もこのコンポーネント単体で行えるので、プレイヤーや敵キャラなどのノードにアタッチするだけで、インスペクタからHPとしきい値、音量などを調整してすぐに使えます。
コンポーネントの設計方針
機能要件の整理
- このコンポーネント単体で完結する(他の GameManager やシングルトンに依存しない)。
- HP(0〜最大値)を内部で管理し、「残りHPが少ない」状態をしきい値で判定する。
- HPがしきい値以下になったら、心拍音を一定間隔でループ再生する。
- HPがしきい値を上回ったら、心拍音の再生を止める。
- 心拍音の間隔や音量、フェードイン/アウト時間などをインスペクタから調整できる。
- AudioSource コンポーネントを内部で利用し、存在しない場合は自動追加する(防御的な実装)。
- ゲームの他のロジックから HP を変更できるように、公開メソッドを用意する(例:
setHp/addHp/damage)。
外部依存をなくすためのアプローチ
- HP管理はこのコンポーネント内部で完結させる。
- 「現在HPをどこから取得するか?」という問題は、外部からメソッド呼び出しで値を渡すことで解決する。
- 例:攻撃判定スクリプトが
heartbeatSound.damage(10);を呼ぶ。
- 例:攻撃判定スクリプトが
- 心拍音再生には Cocos の
AudioSourceを利用し、getComponent(AudioSource)で取得を試み、なければ自動追加する。 - エディタで音源(AudioClip)を差し替え可能にし、ゲームごとに自由な心拍音を使えるようにする。
インスペクタで設定可能なプロパティ設計
以下のプロパティを @property で公開します:
- maxHp: number
- 最大HP。
- 初期HPもこの値で初期化される。
- 例:100。
- lowHpThreshold: number
- 「残りHPが少ない」と判定するしきい値。
- 現在HPがこの値以下になったら心拍音を再生開始。
- 例:30。
- heartbeatClip: AudioClip
- 再生する心拍音の AudioClip。
- エディタからドラッグ&ドロップで設定。
- volume: number
- 心拍音の基本音量(0〜1)。
- AudioSource.volume に反映。
- 例:0.8。
- heartbeatInterval: number
- 心拍音と心拍音の間隔(秒)。
- 例:0.8 秒なら「ドクン…0.8秒…ドクン…」。
- enableFade: boolean
- 心拍音の開始/停止時にフェードを使うかどうか。
- fadeDuration: number
- フェードイン/フェードアウトにかける時間(秒)。
enableFadeが true のときのみ使用。
- startWithMaxHp: boolean
- ゲーム開始時に現在HPを
maxHpで初期化するかどうか。 - テスト時に便利。
- ゲーム開始時に現在HPを
- debugLog: boolean
- HP変化や心拍音状態をログ出力するかどうか。
- 挙動確認用。
内部的には、以下の状態を保持します:
- _currentHp: number … 現在HP。
- _isLowHp: boolean … 低HP状態かどうか。
- _isHeartbeatPlaying: boolean … 心拍ループが動作中かどうか。
- _heartbeatTimer: number … 次の心拍再生までの残り時間(秒)。
- _audioSource: AudioSource | null … このノードの AudioSource 参照。
- _baseVolume: number … フェード制御用に保持する基準音量。
TypeScriptコードの実装
import { _decorator, Component, AudioSource, AudioClip, clamp01, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* HeartbeatSound
* HPがしきい値以下になると心拍音を一定間隔で再生する汎用コンポーネント
*/
@ccclass('HeartbeatSound')
export class HeartbeatSound extends Component {
@property({
tooltip: '最大HP。start時に現在HPをこの値で初期化します(startWithMaxHpがtrueの場合)。'
})
public maxHp: number = 100;
@property({
tooltip: '低HPと判定するしきい値。この値以下になると心拍音が再生されます。'
})
public lowHpThreshold: number = 30;
@property({
type: AudioClip,
tooltip: '心拍音として再生するAudioClip。ドクン…という効果音を設定してください。'
})
public heartbeatClip: AudioClip | null = null;
@property({
tooltip: '心拍音の基本音量(0〜1)。'
})
public volume: number = 0.8;
@property({
tooltip: '心拍音の再生間隔(秒)。例:0.8なら0.8秒ごとにドクン…と鳴ります。'
})
public heartbeatInterval: number = 0.8;
@property({
tooltip: '心拍音の開始・停止にフェードイン/アウトを使うかどうか。'
})
public enableFade: boolean = true;
@property({
tooltip: 'フェードイン/フェードアウトにかける時間(秒)。'
})
public fadeDuration: number = 0.25;
@property({
tooltip: 'trueの場合、start時に現在HPをmaxHpで初期化します。'
})
public startWithMaxHp: boolean = true;
@property({
tooltip: 'デバッグログを出力するかどうか。HP変化や心拍状態を確認するのに便利です。'
})
public debugLog: boolean = false;
// ==== 内部状態 ====
private _currentHp: number = 0;
private _isLowHp: boolean = false;
private _isHeartbeatPlaying: boolean = false;
private _heartbeatTimer: number = 0;
private _audioSource: AudioSource | null = null;
private _baseVolume: number = 1.0;
private _fadeTime: number = 0;
private _isFadingIn: boolean = false;
private _isFadingOut: boolean = false;
// ===================== ライフサイクル =====================
onLoad() {
// AudioSourceを取得 or 自動追加
this._audioSource = this.getComponent(AudioSource);
if (!this._audioSource) {
this._audioSource = this.addComponent(AudioSource);
if (this.debugLog) {
log('[HeartbeatSound] AudioSourceが見つからなかったため、自動で追加しました。');
}
}
if (!this._audioSource) {
warn('[HeartbeatSound] AudioSourceを取得・追加できませんでした。このコンポーネントは動作しません。');
return;
}
// AudioSource初期設定
this._audioSource.loop = false; // 心拍は1回ずつ再生し、自前で間隔制御する
this._audioSource.playOnAwake = false;
// フェード用に基準音量を保存
this._baseVolume = clamp01(this.volume);
this._audioSource.volume = this._baseVolume;
}
start() {
if (this.startWithMaxHp) {
this._currentHp = this.maxHp;
}
// 初期状態で低HPかどうか判定
this._updateLowHpState();
this._resetHeartbeatTimer();
}
update(deltaTime: number) {
if (!this._audioSource) {
return;
}
// ボリュームがInspectorで変えられた場合にも追従
this._baseVolume = clamp01(this.volume);
// フェード処理
this._updateFade(deltaTime);
// 低HPでなければ何もしない
if (!this._isLowHp) {
return;
}
// 心拍音ループ処理
if (this._isHeartbeatPlaying) {
this._heartbeatTimer -= deltaTime;
if (this._heartbeatTimer
コードのポイント解説
- onLoad
getComponent(AudioSource)で AudioSource を取得し、なければaddComponent(AudioSource)で自動追加。- 心拍音は自前で間隔制御するため、
loop = false、playOnAwake = falseに設定。 - Inspector の
volumeを_baseVolumeとして保持。
- start
startWithMaxHpが true の場合、_currentHpをmaxHpで初期化。- 初期状態で低HPかどうかを判定し、必要なら心拍音を開始。
- update
- 毎フレーム、フェード処理(
_updateFade)を行う。 - 低HP状態のときだけタイマーを減算し、0以下になったら心拍音を1回再生。
- 毎フレーム、フェード処理(
- setHp / addHp / damage
- 外部スクリプトから HP を変更するための公開API。
- 値は必ず 0〜
maxHpにクランプされる。 - HP変更後に
_updateLowHpStateを呼び、低HP判定と心拍音開始/停止を自動制御。
- _updateLowHpState
- 現在HPが
lowHpThreshold以下かつ > 0 のときに_isLowHp = true。 - 状態遷移(通常 → 低HP / 低HP → 通常)に応じて
_startHeartbeat/_stopHeartbeatを呼び出す。 - HPが0になった場合は即座に心拍停止。
- 現在HPが
- フェード処理
enableFadeが true のときだけ有効。_startFadeInで音量0から開始し、fadeDuration秒かけて_baseVolumeまで上げる。_startFadeOutで現在音量から0まで下げ、0になったらstop()して音量を_baseVolumeに戻す。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を HeartbeatSound.ts にします。
- 自動生成されたコードをすべて削除し、上記の TypeScript コードをそのまま貼り付けて保存します。
2. テスト用ノード(プレイヤーなど)の作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite(または任意のノード)を作成します。
- ノード名を分かりやすく Player などに変更します。
3. 心拍音の AudioClip を用意
- プロジェクト内に心拍音の音声ファイル(例:
heartbeat.wav/heartbeat.mp3)を用意し、Assets にドラッグ&ドロップします。 - インポートされた AudioClip を選択し、必要に応じて Audio の設定(Sample Rate など)を調整します(基本的にはデフォルトでOK)。
4. HeartbeatSound コンポーネントをアタッチ
- Hierarchy で先ほど作成した Player ノードを選択します。
- Inspector の下部にある Add Component ボタンをクリックします。
- Custom → HeartbeatSound を選択してアタッチします。
5. Inspector でプロパティを設定
Player ノードにアタッチされた HeartbeatSound コンポーネントを選択し、以下のように設定してみます:
- Max Hp:
100 - Low Hp Threshold:
30 - Heartbeat Clip:先ほどインポートした心拍音 AudioClip をドラッグ&ドロップ
- Volume:
0.8 - Heartbeat Interval:
0.8 - Enable Fade:チェック(有効)
- Fade Duration:
0.25 - Start With Max Hp:チェック(有効)
- Debug Log:必要に応じてチェック(HP変化をログで確認したい場合)
6. 簡単な動作確認(エディタ上で HP を減らしてテスト)
このコンポーネントは外部から setHp / damage などを呼び出して使う想定ですが、まずは簡単に動作だけ確認してみます。
- 再生ボタン(▶)を押してゲームを実行します。
- 実行中に Player ノードを選択し、Inspector → HeartbeatSound を開きます。
- スクリプト内の
setHpを直接呼ぶことはできないので、テスト用に一時的なコードを追加してもよいです:- 例:
start()内の最後にthis.setHp(20);を書いてビルドし直すと、開始直後から低HP状態になり、心拍音が鳴り始めます。
- 例:
テスト用コード例(start() の末尾に一時的に追加):
// テスト用:開始直後にHPを20にして低HP状態にする
// 実際のゲームでは削除してください
this.setHp(20);
ゲームを再生すると、フェードインしながら「ドクン…ドクン…」と一定間隔で心拍音が鳴ることを確認できます。
7. 実際のゲームロジックと連携する
実運用では、攻撃判定やダメージ処理のスクリプトから HP を操作します。
たとえば、同じ Player ノードに別のスクリプト PlayerDamage.ts を付けて、そこから HeartbeatSound を取得して damage を呼び出す形になります。
例:最低限の連携コード(参考。外部依存禁止ルールに反しないよう、このコンポーネント側には一切の依存を持たせていません)
// PlayerDamage.ts(参考例。HeartbeatSound側には依存を持たせていない)
import { _decorator, Component } from 'cc';
import { HeartbeatSound } from './HeartbeatSound';
const { ccclass } = _decorator;
@ccclass('PlayerDamage')
export class PlayerDamage extends Component {
private _heartbeat: HeartbeatSound | null = null;
start() {
this._heartbeat = this.getComponent(HeartbeatSound);
}
public takeDamage(amount: number) {
if (this._heartbeat) {
this._heartbeat.damage(amount);
}
}
}
このように、HeartbeatSound 自体は完全に独立しており、他のどんなスクリプトとも疎結合に保たれています。
まとめ
- HeartbeatSound は、HPが一定値以下になったときに自動で心拍音を再生する汎用コンポーネントです。
- AudioSource コンポーネントを自動取得・自動追加するため、ノードにアタッチして AudioClip を設定するだけで動作します。
- HP管理(最大値・現在値・しきい値)、心拍間隔、音量、フェードイン/アウトなどをすべてインスペクタから調整できます。
- 外部スクリプトからは
setHp/addHp/damageを呼び出すだけで、危機的状況の演出が自動的に行われます。 - このスクリプト単体で完結しているため、プレイヤー・ボス・敵ユニットなど、どのノードにも再利用しやすく、ゲーム開発の効率を大きく高められます。
応用として、HP以外のパラメータ(スタミナ、酸素ゲージ、精神力など)に対しても、しきい値判定とサウンド再生ロジックを流用できます。
危機感を演出する「音のフィードバック」を簡単に仕込めるようにしておくと、ゲーム全体のクオリティアップにつながります。
