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 に一括でサウンドを付ける例で説明します。

  1. AudioSource を用意する
    • Hierarchy で Canvas(または UI の親オブジェクト)を選択
    • Add Component から AudioSource を追加
    • Output を UI 用の AudioMixer グループに接続しておくと後で音量調整しやすいです
    • Play On Awake は OFF にしておきましょう(スクリプト側でも OFF にします)
  2. UIGenericSound をアタッチする
    • 同じオブジェクト(例: Canvas)に UIGenericSound コンポーネントを追加
    • Hover Clip に「カーソルを乗せたときの SE」を設定
    • Click Clip に「ボタンを押したときの SE」を設定
    • Scan Root を空のままにしておけば、自動で自分自身(Canvas)以下を探索します
  3. ボタンを配置する
    例として、メインメニュー Canvas 以下に以下のボタンを配置しておきます。
    • Button_Start(ゲーム開始)
    • Button_Option(オプション)
    • Button_Quit(終了)

    これらは通常通り Button コンポーネントを持っていれば OK です。
    ボタン側にはサウンド用のスクリプトや AudioSource は一切不要です。

  4. 再生して確認する
    • Play モードに入ると、UIGenericSound が Canvas 以下の全ての Selectable を自動探索
    • 各ボタンに UIGenericSoundHoverListener が自動でアタッチされます
    • マウスでボタンにカーソルを乗せる / キーボードでボタンを選択すると Hover 音が鳴る
    • ボタンをクリック / Submit(Enter, Aボタンなど)すると Click 音が鳴る

この構成なら、新しいボタンプレハブを追加しても、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 を組んでいきましょう。