Unityで効率的、安全、使いやすいSingleton AudioPlayerを実装する.

UnityでのBGM、SE再生ロジックを組んでいて、以下のような欲求を抱いたことはありませんか？

- SE、BGMの再生処理はどこからでも呼び出したい
- 音声名とかファイル名とかハードコードするの辛いし怖い
- ファイルをResourcesから読み込むときとかファイルはキャッシュしたい


まあようするに使いやすいAudioPlayerほしいよねみたいな気持ちです。僕は以上のような気持ちを常に抱いています。

以上の要件を満たす実装とはすなわち、

- AudioPlayerはSingletonである。
- 音声ファイル名はEnumによって管理されている。
- Enumの拡張メソッド「ToAudioClip」などで対応するAudioClipを呼び出すことができる。
- 読み込んだファイルはキャッシュされている。

のようになりますね。ではこれらを実現していきましょう。


# AudioFileのEnumを実装する
まずAudioFileのEnumを定義しましょう。

```c#
public enum AudioFile {
  GetItem,
  Damage
}
```

次に、このEnumからAudioClipを取得する拡張メソッドを生やします。

冒頭で `fileName` のようなファイル名を保持する変数を作り、switch文を使って各caseで `fileName` にファイル名を代入します。
そして、その `fileName` を使い、最終的に `AudioClipLoader` を介して音声ファイルを取得させるような実装にします。

最終的なコードは以下のようになります。

```c#:AudioFile.cs
using UnityEngine;

public enum AudioFile {
  GetItem,
  Damage
}

static class AudioFileExtentions {
  public static AudioClip ToAudioClip (this AudioFile audioFile) {
    string fileName;

    switch (audioFile) {
      case AudioFile.GetItem:
        fileName = "get_item";
        break;
      case AudioFile.Damage:
        fileName = "damage";
        break;
      default:
        return null;
    }

    return AudioClipLoader.Load ("Sound/" + fileName);
  }
}
```

# AudioClipをResourcesから読み込むクラスを実装する
続いて、AudioClipをResourcesからキャッシュさせつつ読み込むクラスを実装します。

一度読み込んだファイルは、path名: AudioClipのようなDictionaryにキャッシュさせるような実装にします。
また、全ファイルをプリフェッチしておいたり、キャッシュを破棄するような関数もあると良いですね。

コードは以下のようになります。

```c#:AudioClipLoader.cs
using System;
using System.Collections.Generic;
using UnityEngine;

public static class AudioClipLoader {
  private static readonly Dictionary<string, AudioClip> Cache = new Dictionary<string, AudioClip>();

  public static AudioClip Load (string path) {
    if (!Cache.ContainsKey (path)) {
      Cache[path] = Resources.Load (path, typeof(AudioClip)) as AudioClip;
    }

    return Cache[path];
  }

  public static void PreLoadAll () {
    foreach (SoundFile sf in Enum.GetValues (typeof(SoundFile))) {
      sf.ToAudioClip();
    }
  }

  public static void Clear () {
    Cache.Clear();
  }
}
```

# AudioPlayerクラスを実装する
最後に、AudioPlayerクラスを実装します。

こちらのAudioPlayerは、複数のAudioSourceを所持します。
それぞれ、BGM再生用のもの、SE再生用のもの、となっています。
また、SE再生用のものは、SEの多重再生が行われることを予想して、一つだけではなく複数のAudioSourceを作成しておきます。

また、再生関数の引数には先程作成したSoundFileを取るようにします。

コードは以下のようになります。

```c#:AudioPlayer.cs
using UnityEngine;

public class AudioPlayer {
	private const int DefaultSeChannelCount = 20;

	private static AudioPlayer _mInstance;

	public static AudioPlayer Instance => _mInstance ?? (_mInstance = new AudioPlayer (DefaultSeChannelCount));

	private int _bgmFileIdx;
	private readonly AudioSource _bgmChannel;

	private readonly int _seChannelCount;
	private readonly AudioSource[] _seChannels;

	private int _seChannelIndex;

	private AudioPlayer (int seChannelCount) {
		var rootObject = new GameObject ("AudioPlayer");
		Object.DontDestroyOnLoad (rootObject);

		_bgmChannel = rootObject.AddComponent<AudioSource> ();
		_seChannelCount = seChannelCount;

		_seChannels = new AudioSource [seChannelCount];

		for (int i = 0; i < seChannelCount; i++) {
			_seChannels [i] = rootObject.AddComponent<AudioSource> ();
		}

		_seChannelIndex = 0;
	}


	// BGMの再生.
	public static bool PlayBgm (SoundFile soundFile) {
		return Instance.DoPlayBgm (soundFile);
	}

	// BGMの一時停止.
	public static void PauseBgm (bool flag) {
		Instance.DoPauseBgm (flag);
	}

	// BGMの停止.
	public static void StopBgm () {
		Instance.DoStopBgm ();
	}

	//BGMの再生.
	public bool DoPlayBgm (SoundFile soundFile) {

		if ((int)soundFile != _bgmFileIdx) {
			_bgmChannel.Stop();
			_bgmFileIdx = (int)soundFile;

			var clip = soundFile.ToAudioClip();
			if (clip == null) return false;
			
			_bgmChannel.clip = clip;
			_bgmChannel.loop = true;
			_bgmChannel.volume = 1;

			_bgmChannel.Play ();
		}
		return true;
	}


	// BGMの一時停止.
	public void DoPauseBgm (bool flag) {
		if (flag) {
			_bgmChannel.Pause ();
		} else {
			_bgmChannel.Play ();
		}
	}

	// BGMの停止.
	public void DoStopBgm () {
		_bgmChannel.Stop ();
		_bgmFileIdx = -1;
	}


	// SE再生.
	public static AudioSource PlaySe (SoundFile soundFile) {
		return Instance.DoPlaySe (soundFile);
	}

	public AudioSource DoPlaySe (SoundFile soundFile) {
		var seChannel = _seChannels [_seChannelIndex];
		seChannel.Stop ();

		var clip = soundFile.ToAudioClip();
		if (clip == null) return null;

		if (++_seChannelIndex >= _seChannelCount) {
			_seChannelIndex = 0;
		}

		seChannel.clip = clip;
		seChannel.volume = 1.0f;
		seChannel.pitch = 1.0f;

		seChannel.Play();

		return seChannel;
	}

	public static bool StopAllSe () {
		return Instance.DoStopAllSe ();
	}

	public bool DoStopAllSe () {
		for (int i = 0; i < _seChannels.Length; i++) {
			_seChannels [i].Stop ();
		}
		return true;
	}
}
```

# 〆
このような実装を行うことで、タイトルで掲げた効率的 & 安全 & 使いやすいAudioPlayerを実装することができました。
このようなファイル名などに補完が効くというのはすごく大きいです。

AnimatorのParameterやScene名などstringで扱われるものについてはミスを無くすため、できるだけEnumやclassなどで管理することをおすすめします。
合わせて、Resourcesが絡むようなものは上記のような実装を同じく行うと良いのではないでしょうか。

それでは良いUnityライフを。