Unityにおける効果音管理クラスの実装

AudioManagerによるシーンをまたいだSE再生の設計と解説


はじめに

本資料では、Unityにおける効果音(SE:Sound Effect)を効率的に再生・管理するための「AudioManager」クラスの実装方法と設計思想について解説します。このクラスはシングルトンパターンを用いており、シーンをまたいでも効果音を再生可能にする設計となっています。


クラスの目的と特徴

目的

  • 効果音を一括で読み込み、名前で指定して再生できるようにする
  • シーンを移動してもAudioManagerが破棄されないようにする

特徴

  • シングルトンパターンによるインスタンスの一元管理
  • Resources フォルダからのAudioClip自動読み込み
  • 名前による効果音の再生呼び出し
  • Unityの AudioSource を利用したシンプルな構造

ソースコード

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Audio管理クラス。シーンをまたいでも破棄されないシングルトンで実装。
/// </summary>
public class AudioManager : MonoBehaviour
{
    private static AudioManager instance;

    [SerializeField] private AudioSource _audioSource;
    private readonly Dictionary<string, AudioClip> _clips = new Dictionary<string, AudioClip>();

    public static AudioManager Instance
    {
        get { return instance; }
    }

    private void Awake()
    {
        if (null != instance)
        {
            Destroy(gameObject);
            return;
        }

        DontDestroyOnLoad(gameObject);
        instance = this;

        var audioClips = Resources.LoadAll<AudioClip>("2D_SE");
        foreach (var clip in audioClips)
        {
            _clips.Add(clip.name, clip);
        }
    }

    public void Play(string clipName)
    {
        if (!_clips.ContainsKey(clipName))
        {
            throw new Exception("Sound " + clipName + " is not defined");
        }

        _audioSource.clip = _clips[clipName];
        _audioSource.Play();
    }
}

コード解説

クラスとフィールドの定義

private static AudioManager instance;
  • シングルトンパターンのインスタンス格納用。アプリケーション中に1つだけ保持される。
[SerializeField] private AudioSource _audioSource;
  • 効果音を再生するための AudioSource コンポーネント。インスペクターで割り当てる。
private readonly Dictionary<string, AudioClip> _clips = new Dictionary<string, AudioClip>();
  • 読み込んだ音声データ(AudioClip)を名前で管理する辞書。

シングルトンと初期化処理(Awake)

if (null != instance)
{
    Destroy(gameObject);
    return;
}
  • すでにインスタンスが存在している場合、自分自身を削除して重複を防止。
DontDestroyOnLoad(gameObject);
instance = this;
  • シーン遷移時に削除されないよう設定し、自身をシングルトンとして登録。
var audioClips = Resources.LoadAll<AudioClip>("2D_SE");
foreach (var clip in audioClips)
{
    _clips.Add(clip.name, clip);
}
  • Resources/2D_SE フォルダ内にあるすべての AudioClip を読み込み、名前をキーにして辞書へ登録。

効果音の再生(Playメソッド)

public void Play(string clipName)
  • 引数として渡された名前に該当する効果音を再生する。
if (!_clips.ContainsKey(clipName))
{
    throw new Exception("Sound " + clipName + " is not defined");
}
  • 指定された効果音名が存在しなければ例外を投げる。
_audioSource.clip = _clips[clipName];
_audioSource.Play();
  • AudioSourceAudioClip をセットし、再生。

使用手順

  1. 空のGameObjectを作成し、AudioManager スクリプトをアタッチする。
  2. 同じオブジェクトに AudioSource コンポーネントを追加し、インスペクターで _audioSource に割り当てる。
  3. Assets/Resources/2D_SE フォルダを作成し、使用する .wav.mp3 ファイルを配置する。
  4. 他のスクリプトから以下のように呼び出して音声を再生する。
AudioManager.Instance.Play("jump");

注意点と改善案

  • Resources フォルダの利用は手軽だが、非推奨な場面もあるため、規模が大きいプロジェクトでは Addressable Assets の使用を検討。
  • 音量調整やフェード、再生中の判定などの機能も追加可能。
  • 現状では例外がスローされるが、ユーザー向けの通知がないため、より柔軟なエラーハンドリングにすることも検討できる。

以下は、AudioSourceを利用して音量調整、フェードイン・フェードアウト、再生中の判定の機能を実装した簡単なサンプルです。
このスクリプトは、アタッチされたAudioSourceコンポーネントを操作することで、音声の再生や音量の操作が行えます。


AudioControllerサンプルコード

using UnityEngine;
using System.Collections;

public class AudioController : MonoBehaviour
{
    // AudioSourceコンポーネント(Inspectorで設定してもよい)
    private AudioSource audioSource;

    // フェードの時間(秒)
    public float fadeDuration = 1.0f;

    // 初期化処理:必ず同一GameObjectにAudioSourceがアタッチされていることが必要です
    void Awake()
    {
        audioSource = GetComponent<AudioSource>();
    }

    /// <summary>
    /// 現在の設定の音量で再生開始
    /// </summary>
    public void PlayAudio()
    {
        if (audioSource != null)
        {
            audioSource.Play();
        }
    }

    /// <summary>
    /// 音声を即時停止
    /// </summary>
    public void StopAudio()
    {
        if (audioSource != null)
        {
            audioSource.Stop();
        }
    }

    /// <summary>
    /// 再生中かどうかを返す
    /// </summary>
    public bool IsAudioPlaying()
    {
        return audioSource != null && audioSource.isPlaying;
    }

    /// <summary>
    /// 音量を手動で調整(0~1の範囲)
    /// </summary>
    public void SetVolume(float volume)
    {
        if (audioSource != null)
        {
            audioSource.volume = Mathf.Clamp01(volume);
        }
    }

    /// <summary>
    /// フェードイン処理:音量0からtargetVolumeまで徐々に上げながら再生
    /// </summary>
    public IEnumerator FadeIn(float targetVolume)
    {
        if (audioSource != null)
        {
            audioSource.volume = 0f;
            audioSource.Play();
            float timer = 0f;
            while (timer < fadeDuration)
            {
                audioSource.volume = Mathf.Lerp(0f, targetVolume, timer / fadeDuration);
                timer += Time.deltaTime;
                yield return null;
            }
            audioSource.volume = targetVolume;
        }
    }

    /// <summary>
    /// フェードアウト処理:現在の音量から0まで徐々に下げ、停止する
    /// </summary>
    public IEnumerator FadeOut()
    {
        if (audioSource != null)
        {
            float startVolume = audioSource.volume;
            float timer = 0f;
            while (timer < fadeDuration)
            {
                audioSource.volume = Mathf.Lerp(startVolume, 0f, timer / fadeDuration);
                timer += Time.deltaTime;
                yield return null;
            }
            audioSource.volume = 0f;
            audioSource.Stop();
        }
    }
}

サンプルの利用例

  • 音量調整
    SetVolume(0.5f); とすることで、音量を50%に設定できます。
  • フェードイン
    コルーチンとして StartCoroutine(FadeIn(1.0f)); を呼び出すと、音が再生されながら音量が徐々に1.0(最大音量)まで上昇します。
  • フェードアウト
    コルーチンとして StartCoroutine(FadeOut()); を呼び出すと、現在の音量から徐々に0まで下がり、音が停止します。
  • 再生中の判定
    IsAudioPlaying() メソッドを利用することで、AudioSourceが再生中かどうかを簡単にチェックできます。

このように、AudioControllerクラスを活用すれば、音量の調整やフェード効果、再生状態の確認など、音声再生に関する柔軟な制御が可能となります。


まとめ

AudioManagerクラスは、Unityでの効果音管理を効率化するための基本設計を提供します。シーンをまたいだ再利用が可能で、辞書によるクリップ管理により、複雑な再生処理もシンプルに記述できます。プロジェクトの規模や用途に応じて、さらなる機能の追加・拡張が可能です。

参考

以下は、複数のオーディオファイルをAddressable Assetsで管理する場合の置き換えサンプルです。
このサンプルでは、事前にAddressableとして登録した複数のAudioClipを、インスペクターで設定したAssetReference配列として管理し、必要に応じて非同期にロード・キャッシュして再生する方法を示します。また、フェードイン・フェードアウトの処理も併せて実装しています。


1. チュートリアル概要

このチュートリアルでは、従来のResourcesパターンを置き換える形で、複数のオーディオファイル(AudioClip)をAddressable Assetsを利用して管理する方法を学びます。
具体的には、以下の機能を実装します。

  • インスペクターで複数のAudioClip(AssetReference配列)を設定
  • 必要なオーディオファイルを非同期にロードしてキャッシュし、再生する
  • 再生中のオーディオに対してフェードイン・フェードアウトを実現
  • 不要になったオーディオファイルのアンロード

2. Addressable Assets の準備

  1. Addressablesパッケージのインストール
    Unityの【Window > Package Manager】から「Addressables」パッケージをインストールします。
  2. アセットの登録
    再生したい各オーディオファイル(AudioClip)が保存されたPrefabまたは直接AudioClipファイルを対象に、インスペクター上で「Addressable」チェックボックスにチェックを入れ、アドレスを設定します。

3. スクリプトの実装

以下のサンプルコードは、複数のオーディオファイルを扱うためのクラス例です。
同一のGameObjectにAudioSourceコンポーネントが存在するか、スクリプト内で自動的に追加する仕組みになっています。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Collections;
using System.Collections.Generic;

public class AudioAddressableLoader : MonoBehaviour
{
    // インスペクターで複数のオーディオファイル(AudioClip)へのAssetReferenceを設定できるようにする
    public AssetReference[] audioAssetReferences;

    // ロード済みのAudioClipをキャッシュするための辞書
    private Dictionary<AssetReference, AudioClip> loadedAudioClips = new Dictionary<AssetReference, AudioClip>();

    // オーディオ再生用のAudioSource
    private AudioSource audioSource;

    // フェード処理の時間(秒)
    public float fadeDuration = 1.0f;

    void Awake()
    {
        // AudioSourceの取得、なければ追加
        audioSource = GetComponent<AudioSource>();
        if (audioSource == null)
        {
            audioSource = gameObject.AddComponent<AudioSource>();
        }
    }

    /// <summary>
    /// 指定されたインデックスのオーディオクリップをロードし、再生します。
    /// キャッシュがあればロードは行わず再生するだけです。
    /// </summary>
    /// <param name="index">audioAssetReferences配列内のインデックス</param>
    public void LoadAndPlayClip(int index)
    {
        if (index < 0 || index >= audioAssetReferences.Length)
        {
            Debug.LogError("指定されたインデックスが範囲外です。");
            return;
        }

        AssetReference assetRef = audioAssetReferences[index];

        // 既にロード済みの場合はキャッシュから再生
        if (loadedAudioClips.ContainsKey(assetRef))
        {
            PlayClip(loadedAudioClips[assetRef]);
        }
        else
        {
            // 非同期にAudioClipをロード
            assetRef.LoadAssetAsync<AudioClip>().Completed += handle =>
            {
                if (handle.Status == AsyncOperationStatus.Succeeded)
                {
                    AudioClip clip = handle.Result;
                    loadedAudioClips[assetRef] = clip;
                    PlayClip(clip);
                    Debug.Log("オーディオのロードと再生に成功しました。");
                }
                else
                {
                    Debug.LogError("オーディオのロードに失敗しました。");
                }
            };
        }
    }

    /// <summary>
    /// 指定されたAudioClipをAudioSourceで再生します。
    /// </summary>
    private void PlayClip(AudioClip clip)
    {
        if (audioSource == null)
        {
            Debug.LogError("AudioSourceが見つかりません。");
            return;
        }
        audioSource.clip = clip;
        audioSource.Play();
    }

    /// <summary>
    /// フェードイン処理を行い、AudioClipを再生します。
    /// </summary>
    public IEnumerator FadeInAndPlay(AudioClip clip, float targetVolume = 1.0f)
    {
        if(audioSource == null)
        {
            Debug.LogError("AudioSourceが見つかりません。");
            yield break;
        }
        audioSource.volume = 0f;
        audioSource.clip = clip;
        audioSource.Play();

        float timer = 0f;
        while (timer < fadeDuration)
        {
            audioSource.volume = Mathf.Lerp(0f, targetVolume, timer / fadeDuration);
            timer += Time.deltaTime;
            yield return null;
        }
        audioSource.volume = targetVolume;
    }

    /// <summary>
    /// フェードアウト処理を行い、再生中のオーディオを停止します。
    /// </summary>
    public IEnumerator FadeOutAndStop()
    {
        if(audioSource == null)
        {
            Debug.LogError("AudioSourceが見つかりません。");
            yield break;
        }
        float startVolume = audioSource.volume;
        float timer = 0f;
        while (timer < fadeDuration)
        {
            audioSource.volume = Mathf.Lerp(startVolume, 0f, timer / fadeDuration);
            timer += Time.deltaTime;
            yield return null;
        }
        audioSource.volume = 0f;
        audioSource.Stop();
    }

    /// <summary>
    /// ロード済みのすべてのAudioClipをアンロードします。
    /// </summary>
    public void ReleaseAllAudioClips()
    {
        foreach (var kvp in loadedAudioClips)
        {
            Addressables.Release(kvp.Value);
        }
        loadedAudioClips.Clear();
        Debug.Log("全オーディオクリップのアンロードが完了しました。");
    }
}

3.1 コードのポイント解説

  • AssetReference配列の利用
    複数のオーディオファイルのアセット参照をインスペクターから設定可能とすることで、コードにファイルパスなどを直接書かず、柔軟に管理できます。
  • 非同期ロードとキャッシュ
    指定したインデックスのオーディオファイルがすでにキャッシュされているかどうかを判定し、キャッシュがなければ非同期にロードします。ロード完了後、Dictionaryに格納して再利用可能にしています。
  • フェードイン/フェードアウト処理
    再生開始時に音量を徐々に上げるフェードイン、再生終了時に音量を徐々に下げるフェードアウト処理をコルーチンで実装しています。
  • アンロード処理
    使用済みのオーディオファイルが不要になった場合、Addressables.Releaseを利用してメモリから解放し、キャッシュをクリアすることで効率的なリソース管理を行います。

4. シーン上でのテスト

  1. スクリプトの設定
    • 上記のAudioAddressableLoaderスクリプトを空のGameObjectまたは適切なオブジェクトにアタッチします。
    • 同オブジェクトにAudioSourceコンポーネントがない場合は自動的に追加されます。
  2. アセットの設定
    • インスペクターで、audioAssetReferences配列にAddressableとして登録した各AudioClipをドラッグ&ドロップで設定します。
  3. 実行とテスト
    • 任意のタイミングで、例えばボタンのクリックなどから LoadAndPlayClip(index) を呼び出して指定のオーディオを再生してください。
    • また、必要に応じて StartCoroutine(FadeOutAndStop())StartCoroutine(FadeInAndPlay(clip)) を利用してフェード処理を確認してください。
    • オーディオ再生後、不要になったら ReleaseAllAudioClips() によりアンロード処理も実施します。

5. まとめ

このサンプルコードは、従来のResourcesパターンで管理していた複数オーディオファイルの置き換えとして、Addressable Assetsを利用した管理方法を示しています。
Addressable Assetsを活用することで、プロジェクトが大規模になった際のメモリ管理、非同期ロード、依存関係の管理の面で大きなメリットを享受できます。
ぜひこのサンプルを基に、実際のプロジェクト環境に合わせた拡張や調整を行ってください。


Audio,Unity

Posted by hidepon