【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 で初期化するかどうか。
    • テスト時に便利。
  • 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 = falseplayOnAwake = false に設定。
    • Inspector の volume_baseVolume として保持。
  • start
    • startWithMaxHp が true の場合、_currentHpmaxHp で初期化。
    • 初期状態で低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になった場合は即座に心拍停止。
  • フェード処理
    • enableFade が true のときだけ有効。
    • _startFadeIn で音量0から開始し、fadeDuration 秒かけて _baseVolume まで上げる。
    • _startFadeOut で現在音量から0まで下げ、0になったら stop() して音量を _baseVolume に戻す。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を HeartbeatSound.ts にします。
  3. 自動生成されたコードをすべて削除し、上記の TypeScript コードをそのまま貼り付けて保存します。

2. テスト用ノード(プレイヤーなど)の作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite(または任意のノード)を作成します。
  2. ノード名を分かりやすく Player などに変更します。

3. 心拍音の AudioClip を用意

  1. プロジェクト内に心拍音の音声ファイル(例:heartbeat.wav / heartbeat.mp3)を用意し、Assets にドラッグ&ドロップします。
  2. インポートされた AudioClip を選択し、必要に応じて Audio の設定(Sample Rate など)を調整します(基本的にはデフォルトでOK)。

4. HeartbeatSound コンポーネントをアタッチ

  1. Hierarchy で先ほど作成した Player ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom → HeartbeatSound を選択してアタッチします。

5. Inspector でプロパティを設定

Player ノードにアタッチされた HeartbeatSound コンポーネントを選択し、以下のように設定してみます:

  • Max Hp100
  • Low Hp Threshold30
  • Heartbeat Clip:先ほどインポートした心拍音 AudioClip をドラッグ&ドロップ
  • Volume0.8
  • Heartbeat Interval0.8
  • Enable Fade:チェック(有効)
  • Fade Duration0.25
  • Start With Max Hp:チェック(有効)
  • Debug Log:必要に応じてチェック(HP変化をログで確認したい場合)

6. 簡単な動作確認(エディタ上で HP を減らしてテスト)

このコンポーネントは外部から setHp / damage などを呼び出して使う想定ですが、まずは簡単に動作だけ確認してみます。

  1. 再生ボタン(▶)を押してゲームを実行します。
  2. 実行中に Player ノードを選択し、Inspector → HeartbeatSound を開きます。
  3. スクリプト内の 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以外のパラメータ(スタミナ、酸素ゲージ、精神力など)に対しても、しきい値判定とサウンド再生ロジックを流用できます。
危機感を演出する「音のフィードバック」を簡単に仕込めるようにしておくと、ゲーム全体のクオリティアップにつながります。