【Unity】効果音の仮実装システムを作った

# ソースコード
https://github.com/ayaha401/AudioReserve/tree/main

どんなサウンドを実装したか、実装するべきなのかを一目でわかるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/64058fb5-0502-1977-dfac-9e4553ed8459.png)

# 作ったわけ
学生で集まってゲームを作るときに毎回サウンドデータを作り忘れていたり、実装し忘れたりがあったので仮実装できる仕組みがあったら便利だと思いました。
（仕様作成とプログラミングが同時進行で進んでるので起こる問題だとは思いますが…）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/9e0a4a30-9083-2921-841f-da9c2bb6d46f.png)
プランナーが必要そうなサウンドを表にまとめてサウンドクリエイターが作ってエンジニアが実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/78871068-581e-53bd-b2a3-a74396f20de7.png)
ゲームが完成してから一度遊んでみると『このサウンドなくね？』みたいなこと結構起きるんですよね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/ba143d30-6ac3-6f68-a23a-1d22eeadacb3.png)
1人でゲームに登場するすべての音を考えれるわけがないのでエンジニアが『ここサウンド必要そうだな』と思った場所にコードを書くとどんなサウンドが必要かを一覧でまとめれるようにしました。
``` C#
if (Input.GetMouseButtonDown(0))
{
    AudioReserveManager.AudioReserve("Player", "歩く", this.transform);
    AudioReserveManager.AudioReserve("Player", "走る", this.transform);
    AudioReserveManager.AudioReserve("Player", "攻撃", this.transform);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/65e1a5f8-861f-d06d-38e5-a0aba2e0ff11.png)
どんなオブジェクトがどんな音を出すのかが表になって生成される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/114155ad-c8f6-1a01-ac31-b9063498d7b4.png)
エンジニアは
仕様書には書かれてないが明らかに必要だと思うサウンドを仮実装
プランナーは
自分が思いつかなかった必要なサウンドを仕様書に追加
と相互にやり取りできます。

# データを保存する
特に難しいことはせず、`Dictionary<string, string>`に`Add`するだけです。
``` C#
// AudioReserveContainer.cs

private Dictionary<string, string> audioReserveDic = new Dictionary<string, string>();

public void RegisterAudioReserve(string speaker, string containText)
{
    if (!audioReserveDic.ContainsKey(containText))
    {
        audioReserveDic.Add(containText, speaker);
    }
}
```
このDictionaryをUntiyの`OnApplicationQuit`のタイミングでJsonにします。
`OnApplicationQuit`はエディターなら再生モード終了時に出るのでこれを使います。
```C#
// AudioReserveManager.cs

private void OnApplicationQuit()
{
#if UNITY_EDITOR
    reserveContainer?.SaveLog();
#endif
}
```
Jsonにする方法は特に難しいことはしてません。工夫した点としてはJsonの名前を現在開いてるシーン名を接頭辞としてくっつけたことです。
```C#
string jsonStr = JsonUtility.ToJson(logData, true);
string sceneName = SceneManager.GetActiveScene().name;
string jsonName = AudioReserveStaticData.LOG_JSON_NAME;
string dataPath = Application.dataPath + AudioSettingStaticData.JSON_DIRECTORY_PATH + "/AudioLogData/" + sceneName + "_" + jsonName;
```
同じ名前のシーンごとにデータが作られて変に上書きされたりすることがありません。

# Editor拡張する
プランナーが確認しやすいようにEditor拡張で見やすくします。
## 読み込むデータを選択できるようにする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/d2371cfb-7d00-a7f5-88ec-159e39941a41.png)
```C#
private void OnEnable()
{
    searchPath = Application.dataPath + AudioSettingStaticData.JSON_DIRECTORY_PATH + "/AudioLogData";
    DirectoryInfo dir = new DirectoryInfo(searchPath);
    FileInfo[] info = dir.GetFiles("*.json");

    if (info.Length == 0)
    {
        return;
    }

    jsonDataNames = new string[info.Length];
    for (int i = 0; i < info.Length; i++)
    {
        jsonDataNames[i] = info[i].Name;
    }
}
```
保存場所に保存されてるJsonを全部探して
```C#
using (new GUILayout.VerticalScope())
{
    GUILayout.Label("読み込むデータを選択する");
    using (new GUILayout.HorizontalScope())
    {
        selectedJsonDataIndex.Value = EditorGUILayout.Popup(selectedJsonDataIndex.Value, jsonDataNames);

        if (GUILayout.Button(reloadIconTex, GUILayout.Width(24f)))
        {
            AudioReserveDicFromJson(selectedJsonDataIndex.Value);
        }
    }
}
```
`EditorGUILayout.Popup`を使って一覧から選べれる画面を作ります。
## 一覧表示する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/9332f719-09ab-ad31-3819-2f7a2e397507.png)
まず頑張ってJsonをDictionaryに変換します。
```C#
private void AudioReserveDicFromJson(int index)
{
    string jsonName = jsonDataNames[index];
    string jsonPath = searchPath + "/" + jsonName;
    string dataStr = string.Empty;

    StreamReader streamReader = new StreamReader(jsonPath);
    dataStr = streamReader.ReadToEnd();
    streamReader.Close();

    AudioReserveLogData audioLogData = JsonUtility.FromJson<AudioReserveLogData>(dataStr);
    audioReserveDic = audioLogData.containTextArray.Select((k, i) => new { k, v = audioLogData.speakerArray[i] }).ToDictionary(a => a.k, a => a.v);

    audioReserveDic = audioReserveDic.OrderBy((x) => x.Value).ToDictionary(a => a.Key, a => a.Value);
}
```
`index`は**読み込むデータを選択できるようにする**で選択したデータの番号です。
Dictionaryに変換できたらあとは簡単です。`foreach`で並べて表示するだけです。
```C#
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

using (new GUILayout.HorizontalScope())
{
    using (new GUILayout.VerticalScope())
    {
        foreach (var audioReserve in audioReserveDic)
        {
            using (new GUILayout.HorizontalScope("box"))
            {
                string speakerName = audioReserve.Value;
                GUILayout.Label(speakerName);
                if (GUILayout.Button(copyIconTex, GUILayout.Width(24f)))
                {
                    EditorGUIUtility.systemCopyBuffer = speakerName;
                }
            }
        }
    }

    using (new GUILayout.VerticalScope())
    {
        foreach (var audioReserve in audioReserveDic)
        {
            using (new GUILayout.HorizontalScope("box"))
            {
                string containText = audioReserve.Key;
                GUILayout.Label(containText);
                if (GUILayout.Button(copyIconTex, GUILayout.Width(24f)))
                {
                    EditorGUIUtility.systemCopyBuffer = containText;
                }
            }
        }
    }
}

EditorGUILayout.EndScrollView();
```

# 完成
これでプログラマは『ここサウンドいるんじゃね？』って思う場所に
```C#
AudioReserveManager.AudioReserve("Player", "歩く", this.transform);
```
を書くだけで勝手にJsonに保存されてどんな音を今後作るべきなのかわかるようになります！