UnityのUI実装でありがちなのが、Update()に「入力処理」「サウンド再生」「アニメーション」「状態管理」などを全部書いてしまうパターンですね。
最初は動いているように見えますが、ボタンが増えるたびに if 文だらけになり、どのボタンがどの音を鳴らしているのか分からなくなりがちです。
UIサウンドも同じで、
- 各ボタンごとに AudioSource を持たせている
- ボタンの OnClick に毎回 PlaySound() を手動で登録している
- ボタンを追加するたびにスクリプトを修正している
といった構成だと、シーンやメニューが増えるほどメンテがつらくなります。
この記事では、「全てのボタンに自動で Hover / Click 音を付ける」ためのコンポーネント
UIGenericSound を作って、UIサウンドを一括管理するやり方を紹介します。
UI用のサウンドはこのコンポーネントに任せて、ボタン側は「押されたかどうか」だけに集中させる、というコンポーネント指向な分割を目指しましょう。
【Unity】UIサウンド管理を一括コンポーネント化!「UIGenericSound」コンポーネント
UIGenericSound は、指定した Canvas(または任意の親オブジェクト)以下にある Button / Selectable を自動で走査し、
- マウスホバー(選択)時に Hover 音
- クリック(Submit)時に Click 音
を共通の AudioSource から再生してくれるコンポーネントです。
ボタン側には一切スクリプトを貼らず、プレハブも汚さずに UI サウンドを後付けできるのがポイントです。
フルコード(Unity6 / C#)
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UI
{
/// <summary>
/// 全ての子階層にある Button / Selectable に
/// Hover / Click 音を一括で付与するコンポーネント。
///
/// ・親オブジェクト(Canvas など)にアタッチして使用
/// ・AudioSource はこのオブジェクトに 1 つだけ
/// ・ボタン個別のスクリプト修正は不要
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class UIGenericSound : MonoBehaviour
{
[Header("=== サウンド設定 ===")]
[SerializeField]
private AudioClip hoverClip; // ホバー(選択)時に再生する SE
[SerializeField]
private AudioClip clickClip; // クリック時に再生する SE
[SerializeField, Tooltip("同時に複数の音を重ねて再生したい場合は ON")]
private bool allowOverlapPlay = true;
[Header("=== 探索設定 ===")]
[SerializeField, Tooltip("有効な Button / Selectable のみを対象にする")]
private bool onlyInteractable = true;
[SerializeField, Tooltip("シーン開始時に自動でボタンを探索して接続する")]
private bool autoScanOnStart = true;
[SerializeField, Tooltip("ボタン探索の対象ルート(未設定ならこの GameObject 自身)")]
private Transform scanRoot;
// 内部で使用する AudioSource
private AudioSource audioSource;
// すでにサウンドを付与したボタンを記録しておく(重複登録防止)
private readonly HashSet<GameObject> registeredObjects = new HashSet<GameObject>();
private void Awake()
{
audioSource = GetComponent<AudioSource>();
// UI SE 用なので 2D サウンドにしておくのが無難
audioSource.playOnAwake = false;
audioSource.spatialBlend = 0f;
// scanRoot が未設定なら自分自身を起点にする
if (scanRoot == null)
{
scanRoot = transform;
}
}
private void Start()
{
if (autoScanOnStart)
{
ScanAndRegisterAll();
}
}
/// <summary>
/// 外部からも呼べるようにしておくと、
/// 動的に生成したボタンにもサウンドを付与しやすい。
/// </summary>
[ContextMenu("Scan And Register All UI Buttons")]
public void ScanAndRegisterAll()
{
// Button と Selectable をまとめて探索する
var selectables = scanRoot.GetComponentsInChildren<Selectable>(includeInactive: true);
foreach (var selectable in selectables)
{
if (selectable == null) continue;
if (onlyInteractable && !selectable.interactable) continue;
RegisterToSelectable(selectable);
}
}
/// <summary>
/// 個別の Selectable(Button, Toggle, Slider など)に
/// Hover / Click 音のイベントを登録する。
/// </summary>
/// <param name="selectable">対象の UI.Selectable</param>
public void RegisterToSelectable(Selectable selectable)
{
if (selectable == null) return;
var go = selectable.gameObject;
// すでに登録済みならスキップ(重複登録を防ぐ)
if (registeredObjects.Contains(go))
{
return;
}
// Hover 音用のイベントリスナを追加
var hoverListener = go.GetComponent<UIGenericSoundHoverListener>();
if (hoverListener == null)
{
hoverListener = go.AddComponent<UIGenericSoundHoverListener>();
}
hoverListener.Initialize(this);
// Click 音は Button だけに付与する
var button = go.GetComponent<Button>();
if (button != null)
{
// onClick にメソッドを追加
button.onClick.AddListener(PlayClick);
}
registeredObjects.Add(go);
}
/// <summary>
/// ホバー音を再生する(外部からも呼べるように public)
/// </summary>
public void PlayHover()
{
PlayClip(hoverClip);
}
/// <summary>
/// クリック音を再生する(Button.onClick から呼ばれる)
/// </summary>
public void PlayClick()
{
PlayClip(clickClip);
}
/// <summary>
/// 実際の AudioClip 再生処理を 1 箇所にまとめる
/// </summary>
private void PlayClip(AudioClip clip)
{
if (clip == null) return;
if (allowOverlapPlay)
{
// 重ねて再生したい場合は PlayOneShot を使う
audioSource.PlayOneShot(clip);
}
else
{
// 1 つずつ再生したい場合は clip を差し替えて Play
if (audioSource.isPlaying)
{
audioSource.Stop();
}
audioSource.clip = clip;
audioSource.Play();
}
}
}
/// <summary>
/// EventSystem からの Pointer / Select イベントを拾って
/// UIGenericSound に通知するための小さなコンポーネント。
///
/// ・各ボタンに自動でアタッチされる
/// ・責務は「イベントを拾って通知するだけ」
/// </summary>
public class UIGenericSoundHoverListener :
MonoBehaviour,
IPointerEnterHandler,
ISelectHandler
{
private UIGenericSound uiGenericSound;
/// <summary>
/// UIGenericSound 側から初期化される
/// </summary>
public void Initialize(UIGenericSound sound)
{
uiGenericSound = sound;
}
/// <summary>
/// マウスカーソルがボタン上に乗ったとき
/// </summary>
public void OnPointerEnter(PointerEventData eventData)
{
if (!IsActiveAndInteractable()) return;
uiGenericSound?.PlayHover();
}
/// <summary>
/// キーボード / ゲームパッドでボタンが選択されたとき
/// </summary>
public void OnSelect(BaseEventData eventData)
{
if (!IsActiveAndInteractable()) return;
uiGenericSound?.PlayHover();
}
/// <summary>
/// 無効なボタンでは音を鳴らさないようにチェック
/// </summary>
private bool IsActiveAndInteractable()
{
if (!isActiveAndEnabled) return false;
var selectable = GetComponent<Selectable>();
if (selectable == null) return false;
return selectable.IsActive() && selectable.interactable;
}
}
}
使い方の手順
ここでは、メインメニューの UI に一括でサウンドを付ける例で説明します。
-
AudioSource を用意する
- Hierarchy で
Canvas(または UI の親オブジェクト)を選択 Add ComponentからAudioSourceを追加Outputを UI 用の AudioMixer グループに接続しておくと後で音量調整しやすいですPlay On Awakeは OFF にしておきましょう(スクリプト側でも OFF にします)
- Hierarchy で
-
UIGenericSound をアタッチする
- 同じオブジェクト(例:
Canvas)にUIGenericSoundコンポーネントを追加 Hover Clipに「カーソルを乗せたときの SE」を設定Click Clipに「ボタンを押したときの SE」を設定Scan Rootを空のままにしておけば、自動で自分自身(Canvas)以下を探索します
- 同じオブジェクト(例:
-
ボタンを配置する
例として、メインメニュー Canvas 以下に以下のボタンを配置しておきます。Button_Start(ゲーム開始)Button_Option(オプション)Button_Quit(終了)
これらは通常通り
Buttonコンポーネントを持っていれば OK です。
ボタン側にはサウンド用のスクリプトや AudioSource は一切不要です。 -
再生して確認する
- Play モードに入ると、
UIGenericSoundが Canvas 以下の全てのSelectableを自動探索 - 各ボタンに
UIGenericSoundHoverListenerが自動でアタッチされます - マウスでボタンにカーソルを乗せる / キーボードでボタンを選択すると Hover 音が鳴る
- ボタンをクリック / Submit(Enter, Aボタンなど)すると Click 音が鳴る
- Play モードに入ると、
この構成なら、新しいボタンプレハブを追加しても、Canvas 以下に置くだけで自動的にサウンドが付くようになります。
プレハブ側の OnClick 設定などには一切触れずに、UI サウンドを後からまとめて差し替えられるのもメリットです。
メリットと応用
UIGenericSound を使うことで、UI サウンド周りの設計がかなりスッキリします。
- サウンドの一元管理
Hover / Click 用の SE を 1 箇所で設定しているので、「メニューの効果音を変えたい」となったときに
全ボタンのプレハブを開き直す必要がありません。UIGenericSoundのインスペクタだけ触れば OK です。 - プレハブがクリーンになる
それぞれのボタンプレハブに AudioSource やサウンド再生用スクリプトを持たせる必要がなくなります。
ボタンの責務は「押されること」だけにして、サウンドの責務はUIGenericSoundにまとめる、という
Single Responsibility な分割ができています。 - レベルデザインが楽になる
UI を増やしたり、別シーンにメニューを複製したりするときも、Canvas ごとコピーすればサウンド設定も一緒に付いてきます。
「このシーンだけ Hover 音を変えたい」場合は、その Canvas に別のUIGenericSoundを置けば良いだけです。
応用としては、
- 特定のボタンだけ別の Click 音を鳴らしたい
- ボタンの種類(決定系 / キャンセル系)で音を変えたい
- ゲーム内メニューとタイトルメニューで音を変えたい
といった拡張も考えられます。
例えば、「キャンセルっぽいボタン(Back, Close など)だけ別の SE を鳴らす」改造案として、
UIGenericSound にこんなメソッドを追加して、特定ボタンにだけ違う音を登録することもできます。
/// <summary>
/// 特定の Button だけ別のクリック音を鳴らしたい場合に使うサンプル。
/// (例: キャンセルボタン、閉じるボタンなど)
/// </summary>
public void RegisterButtonWithCustomClick(Button button, AudioClip customClickClip)
{
if (button == null || customClickClip == null) return;
// まず通常の登録処理を行う(Hover は共通)
RegisterToSelectable(button);
// 既存のリスナは残したまま、追加で専用のリスナを登録
button.onClick.AddListener(() =>
{
// 共通の Click 音ではなく、指定された音を再生
PlayClip(customClickClip);
});
}
このように、小さな責務のコンポーネントとして UIGenericSound を用意しておくと、
UI サウンドの仕様変更にも柔軟に対応しやすくなります。
「ボタンはボタン」「サウンドはサウンド」で役割を分けて、God クラス化を防ぎつつ、気持ちよく UI を組んでいきましょう。
