【Cocos Creator】アタッチするだけ!SoundButton (SE再生)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】SoundButton の実装:アタッチするだけで Button の押下・ホバーに応じて SE を再生する汎用スクリプト

UI ボタンに「押したとき」「ホバーしたとき」の効果音を簡単につけたい場面は多いですが、そのたびにコードを書くのは面倒です。この記事では、Button コンポーネントを持つノードにアタッチするだけで、pressed / hover イベントに応じて SE を鳴らせる汎用コンポーネント「SoundButton」を実装します。

外部の GameManager やオーディオ管理クラスには一切依存せず、この 1 スクリプトだけで完結する設計になっています。


コンポーネントの設計方針

機能要件の整理

  • 対象ノードには Button コンポーネントがアタッチされていることを前提とする。
  • Button の以下のイベントタイミングで SE を再生する:
    • pressed: ボタンが押された瞬間(マウスダウン / タッチ開始)
    • hover: ボタンにポインタが乗った瞬間(マウスオーバー)
  • SE の音源は AudioClip をインスペクタから指定できるようにする。
  • 音量やミュート、同時再生の抑制などを インスペクタで調整できるようにする。
  • 外部のシングルトンやマネージャには依存せず、自分自身で AudioSource を用意して再生する。
  • Button や AudioSource が見つからない場合は エラーログを出して安全に動作を止める

イベント検出のアプローチ

Cocos Creator 3.8 の Button は、クリック時の clickEvents は提供しますが、pressed / hover 専用のコールバックは持っていません。そこでこのコンポーネントでは:

  • pressed:
    • Button.target(なければ自ノード)に EventHandler を自動登録し、Node.EventType.TOUCH_START で検出する。
  • hover:
    • 同じくターゲットノードに対して Node.EventType.MOUSE_ENTER で検出する。
    • タッチデバイスでは hover が発生しないことを前提とし、PC 向け UI での利用を想定。

これにより Button に特別な設定を追加しなくても、SoundButton をアタッチするだけでイベントを横取りせずに SE だけ追加できます。

インスペクタで設定可能なプロパティ設計

SoundButton コンポーネントに用意する @property は次の通りです。

  • pressedClip: AudioClip | null
    • pressed(押下)時に再生する SE。
    • null の場合は pressed 時の SE 再生を行わない。
  • hoverClip: AudioClip | null
    • hover(ホバー)時に再生する SE。
    • null の場合は hover 時の SE 再生を行わない。
  • pressedVolume: number
    • pressed SE の音量(0.0 ~ 1.0)。
    • デフォルト: 1.0。
  • hoverVolume: number
    • hover SE の音量(0.0 ~ 1.0)。
    • デフォルト: 1.0。
  • mutePressed: boolean
    • true の場合、pressed SE を強制的にミュート(再生しない)。
  • muteHover: boolean
    • true の場合、hover SE を強制的にミュート(再生しない)。
  • preventPressedOverlap: boolean
    • true の場合、pressed SE 再生中は同じ SE を重ねて再生しない。
    • 例: ボタンを連打しても SE が重ならず、1 つだけ鳴る。
  • preventHoverOverlap: boolean
    • true の場合、hover SE 再生中は同じ SE を重ねて再生しない。
  • useSingleAudioSource: boolean
    • true: pressed / hover で 同じ AudioSource を使い回す。
    • false: pressed / hover で 別々の AudioSource を内部的に使い分ける。
    • UI SE 程度なら true で問題ないケースが多い。

これらの設定によって、同じコンポーネントをさまざまな UI ボタンにアタッチしても、インスペクタ調整だけで挙動をカスタマイズできます。


TypeScriptコードの実装

以下が完成した SoundButton.ts の全コードです。


import { _decorator, Component, Node, Button, AudioSource, AudioClip, log, error } from 'cc';
const { ccclass, property } = _decorator;

/**
 * SoundButton
 * Button の pressed / hover タイミングで SE を再生する汎用コンポーネント。
 * - 外部の GameManager / AudioManager には依存しません。
 * - 同じノードに Button がアタッチされていることを前提とします。
 */
@ccclass('SoundButton')
export class SoundButton extends Component {

    // === 再生する AudioClip 設定 ===
    @property({
        type: AudioClip,
        tooltip: 'ボタンが押された瞬間(TOUCH_START)に再生する SE。\n未設定の場合は pressed 時の SE 再生は行いません。'
    })
    public pressedClip: AudioClip | null = null;

    @property({
        type: AudioClip,
        tooltip: 'マウスカーソルがボタンに乗った瞬間(MOUSE_ENTER)に再生する SE。\n未設定の場合は hover 時の SE 再生は行いません。'
    })
    public hoverClip: AudioClip | null = null;

    // === 音量設定 ===
    @property({
        tooltip: 'pressed SE の音量(0.0 ~ 1.0)。'
    })
    public pressedVolume: number = 1.0;

    @property({
        tooltip: 'hover SE の音量(0.0 ~ 1.0)。'
    })
    public hoverVolume: number = 1.0;

    // === ミュート設定 ===
    @property({
        tooltip: 'true の場合、pressed SE を再生しません(ミュート)。'
    })
    public mutePressed: boolean = false;

    @property({
        tooltip: 'true の場合、hover SE を再生しません(ミュート)。'
    })
    public muteHover: boolean = false;

    // === オーバーラップ抑制設定 ===
    @property({
        tooltip: 'true の場合、pressed SE 再生中は同じ SE を重ねて再生しません。'
    })
    public preventPressedOverlap: boolean = true;

    @property({
        tooltip: 'true の場合、hover SE 再生中は同じ SE を重ねて再生しません。'
    })
    public preventHoverOverlap: boolean = true;

    // === AudioSource 利用方法 ===
    @property({
        tooltip: 'true: pressed / hover で同じ AudioSource を使い回します。\nfalse: 内部的に別々の AudioSource を使い分けます。'
    })
    public useSingleAudioSource: boolean = true;

    // 内部用 AudioSource 参照
    private _button: Button | null = null;
    private _pressedSource: AudioSource | null = null;
    private _hoverSource: AudioSource | null = null;

    // ライフサイクル: onLoad
    onLoad() {
        // Button の取得
        this._button = this.getComponent(Button);
        if (!this._button) {
            error('[SoundButton] Button コンポーネントが見つかりません。このコンポーネントは Button と同じノードにアタッチしてください。');
            return;
        }

        // AudioSource の準備
        this._setupAudioSources();

        // イベント登録
        this._registerEvents();
    }

    // AudioSource の準備
    private _setupAudioSources() {
        if (this.useSingleAudioSource) {
            // 同じ AudioSource を共有して利用
            let source = this.getComponent(AudioSource);
            if (!source) {
                source = this.node.addComponent(AudioSource);
                log('[SoundButton] AudioSource コンポーネントが見つからなかったため、自動的に追加しました。');
            }
            this._pressedSource = source;
            this._hoverSource = source;
        } else {
            // pressed 用
            let pressed = this.getComponent(AudioSource);
            if (!pressed) {
                pressed = this.node.addComponent(AudioSource);
                log('[SoundButton] pressed 用 AudioSource を自動的に追加しました。');
            }
            this._pressedSource = pressed;

            // hover 用(pressed と別のインスタンスを持ちたい場合)
            if (this._hoverSource === null || this._hoverSource === this._pressedSource) {
                const hoverNode: Node = this.node;
                let hover = hoverNode.getComponent(AudioSource);
                if (!hover) {
                    hover = hoverNode.addComponent(AudioSource);
                    log('[SoundButton] hover 用 AudioSource を自動的に追加しました。');
                }
                this._hoverSource = hover;
            }
        }
    }

    // イベント登録
    private _registerEvents() {
        if (!this._button) {
            return;
        }

        // Button の target ノード(なければ自分自身)に対してイベントを登録
        const targetNode: Node = this._button.target || this.node;

        // pressed: TOUCH_START
        targetNode.on(Node.EventType.TOUCH_START, this._onPressed, this);

        // hover: MOUSE_ENTER
        targetNode.on(Node.EventType.MOUSE_ENTER, this._onHover, this);
    }

    // イベント解除
    private _unregisterEvents() {
        if (!this._button) {
            return;
        }
        const targetNode: Node = this._button.target || this.node;
        targetNode.off(Node.EventType.TOUCH_START, this._onPressed, this);
        targetNode.off(Node.EventType.MOUSE_ENTER, this._onHover, this);
    }

    // ライフサイクル: onDestroy
    onDestroy() {
        this._unregisterEvents();
    }

    // === イベントハンドラ ===

    // ボタンが押された瞬間
    private _onPressed() {
        if (this.mutePressed) {
            return;
        }
        if (!this.pressedClip) {
            // クリップ未設定なら何もしない(エラーにはしない)
            return;
        }
        if (!this._pressedSource) {
            error('[SoundButton] pressed 用 AudioSource が初期化されていません。');
            return;
        }

        // オーバーラップ抑制
        if (this.preventPressedOverlap && this._pressedSource.playing) {
            return;
        }

        this._pressedSource.volume = this._clampVolume(this.pressedVolume);
        this._pressedSource.playOneShot(this.pressedClip, this._pressedSource.volume);
    }

    // ホバーした瞬間
    private _onHover() {
        if (this.muteHover) {
            return;
        }
        if (!this.hoverClip) {
            // クリップ未設定なら何もしない
            return;
        }
        if (!this._hoverSource) {
            error('[SoundButton] hover 用 AudioSource が初期化されていません。');
            return;
        }

        if (this.preventHoverOverlap && this._hoverSource.playing) {
            return;
        }

        this._hoverSource.volume = this._clampVolume(this.hoverVolume);
        this._hoverSource.playOneShot(this.hoverClip, this._hoverSource.volume);
    }

    // ボリューム値を 0.0 ~ 1.0 にクランプ
    private _clampVolume(v: number): number {
        if (v < 0) return 0;
        if (v > 1) return 1;
        return v;
    }
}

コードのポイント解説

  • onLoad()
    • 同じノードから Button を取得し、存在しなければエラーログを出して処理終了。
    • _setupAudioSources() で AudioSource を準備(なければ自動追加)。
    • _registerEvents() で Button の target ノードに対し、TOUCH_STARTMOUSE_ENTER イベントを登録。
  • _setupAudioSources()
    • useSingleAudioSource が true の場合は 1 つの AudioSource を共有。
    • false の場合は pressed / hover 用に別々の AudioSource を用意。
    • いずれも見つからなければ node.addComponent(AudioSource) で自動追加。
  • _registerEvents() / _unregisterEvents()
    • Button.target が設定されていればそちらに、なければ自ノードにイベントを登録。
    • onDestroy() で必ず off() してリークを防止。
  • _onPressed()
    • ミュートフラグや clip 未設定をチェックしてから再生。
    • preventPressedOverlap が true かつ AudioSource.playing 中であれば再生しない。
    • 音量を 0.0〜1.0 にクランプしたうえで playOneShot() を使用。
  • _onHover()
    • pressed と同様のロジックで hover 用の SE を再生。
    • タッチデバイスでは MOUSE_ENTER が動作しない点に注意(PC 向け UI 用)。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、SE 用スクリプトを置きたいフォルダ(例: assets/scripts/ui)を選択します。
  2. そのフォルダ上で右クリック → CreateTypeScript を選択します。
  3. ファイル名を SoundButton.ts に変更します。
  4. 作成された SoundButton.ts をダブルクリックし、エディタ(VS Code など)で開き、前節の TypeScript コードを丸ごと貼り付けて保存します。

2. テスト用 UI ボタンの作成

  1. Hierarchy パネルで右クリック → CreateCanvas(まだない場合)を作成します。
  2. Canvas を選択した状態で、右クリック → UIButton を作成します。
    • 自動的に Button コンポーネントと Sprite などが付いたボタンノードができます。
  3. 作成された Button ノード(例: Button)を選択します。

3. SoundButton コンポーネントをアタッチ

  1. Button ノードを選択した状態で、右側の Inspector を確認します。
  2. 一番下の Add Component ボタンをクリックします。
  3. CustomSoundButton を選択します。
    • リストに見つからない場合は、スクリプトのクラス名 @ccclass('SoundButton') とファイル名が一致しているか確認してください。

4. SE 用 AudioClip の用意

  1. 効果音ファイル(例: button_click.wavbutton_hover.wav)を Assets パネルにドラッグ&ドロップしてインポートします。
  2. インポートされた Audio ファイルを選択し、Inspector で AudioClip として認識されていることを確認します。

5. SoundButton のプロパティ設定

Button ノードにアタッチされた SoundButton コンポーネントを、Inspector 上で次のように設定してみます。

  • pressedClip: button_click(クリック SE 用の AudioClip をドラッグ&ドロップ)
  • hoverClip: button_hover(ホバー SE 用の AudioClip をドラッグ&ドロップ)
  • pressedVolume: 1.0
  • hoverVolume: 0.8(ホバー音は少し小さめにする例)
  • mutePressed: false
  • muteHover: false
  • preventPressedOverlap: true(連打しても SE が重ならない)
  • preventHoverOverlap: true
  • useSingleAudioSource: true(通常はこれで十分)

6. 再生して動作確認

  1. エディタ上部の Play ボタンを押してゲームを実行します。
  2. ゲームビュー上で:
    • マウスカーソルをボタンの上に乗せると、hoverClip が再生されることを確認します。
    • ボタンをクリック(押し込んだ瞬間)すると、pressedClip が再生されることを確認します。
    • pressed を連打しても、preventPressedOverlap = true の場合は SE が重ならず、綺麗に鳴ることを確認します。

7. よくあるトラブルとチェックポイント

  • SE が鳴らない場合:
    • Button ノードに Button コンポーネントが付いているか確認。
    • SoundButton の pressedClip / hoverClip に AudioClip が設定されているか確認。
    • エディタの Console[SoundButton] のエラーログが出ていないか確認。
    • 実行環境の音量やミュート設定(OS / ブラウザ)も確認。
  • hover が反応しない場合:
    • 実行環境がタッチデバイスの場合、MOUSE_ENTER が発生しません(PC で確認)。
    • ボタンのサイズや Raycast 設定が正しく、マウスがボタン上に乗っているか確認。

まとめ

この記事では、Button ノードにアタッチするだけで pressed / hover に応じて SE を再生できる汎用コンポーネント「SoundButton」を実装しました。

  • 外部の GameManager や AudioManager に依存せず、この 1 スクリプトだけで完結する設計。
  • 必要な情報(AudioClip や音量、オーバーラップ抑制など)はすべて Inspector の @property から設定可能。
  • Button の target ノードに対して直接イベント登録することで、既存のクリック処理を邪魔せずに SE だけ追加。
  • AudioSource が存在しない場合は自動追加し、Button がない場合はエラーログで通知する防御的な実装。

このコンポーネントをプロジェクトの共通パーツとして用意しておけば、UI ボタンに効果音を足す作業は「SoundButton をアタッチして AudioClip を指定するだけ」になります。ボタンの数が増えてもコードを書く必要がなくなり、UI 演出の試行錯誤が格段に楽になります。

応用としては、

  • hoverClip を未設定にして pressed だけ鳴らすボタン
  • 重要なボタンだけ別の SE と音量で強調する
  • オプション画面で mute フラグをまとめて切り替える(別の UI から mutePressed / muteHover を制御)

といった使い方も簡単に行えます。プロジェクトの UI ボタンに順次適用して、操作フィードバックの質を高めていきましょう。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!