【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_STARTとMOUSE_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. スクリプトファイルの作成
- エディタ左下の Assets パネルで、SE 用スクリプトを置きたいフォルダ(例:
assets/scripts/ui)を選択します。 - そのフォルダ上で右クリック → Create → TypeScript を選択します。
- ファイル名を
SoundButton.tsに変更します。 - 作成された
SoundButton.tsをダブルクリックし、エディタ(VS Code など)で開き、前節の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用 UI ボタンの作成
- Hierarchy パネルで右クリック → Create → Canvas(まだない場合)を作成します。
- Canvas を選択した状態で、右クリック → UI → Button を作成します。
- 自動的に
ButtonコンポーネントとSpriteなどが付いたボタンノードができます。
- 自動的に
- 作成された Button ノード(例:
Button)を選択します。
3. SoundButton コンポーネントをアタッチ
- Button ノードを選択した状態で、右側の Inspector を確認します。
- 一番下の Add Component ボタンをクリックします。
- Custom → SoundButton を選択します。
- リストに見つからない場合は、スクリプトのクラス名
@ccclass('SoundButton')とファイル名が一致しているか確認してください。
- リストに見つからない場合は、スクリプトのクラス名
4. SE 用 AudioClip の用意
- 効果音ファイル(例:
button_click.wav、button_hover.wav)を Assets パネルにドラッグ&ドロップしてインポートします。 - インポートされた 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. 再生して動作確認
- エディタ上部の Play ボタンを押してゲームを実行します。
- ゲームビュー上で:
- マウスカーソルをボタンの上に乗せると、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 ボタンに順次適用して、操作フィードバックの質を高めていきましょう。




