AssetBundleにした音楽ファイルを(なるべくカクつかない様に)ロードする手順 Unity

この記事は、音楽ファイルをAssetBundleにしてビルドし、それをロードして(なるべくカクつかない様に)音楽を再生する手順のメモです。

自分がやった時に大変だったので復習用にまとめます

Unityのバージョン2021.3

誤り、もっと良い方法等があれば是非教えてくださいm(_ _)m


# こういう人向け
・未来の自分
・AssetBundleを使ってみたい
・曲のロードをカクつかないようにしたい



# 1.ScriptableObjectを作る
### 1-1 なぜScriptableObjectを作るか
AssetBundleをビルド・ロードする時にAssetBundleの中に入れるファイルの名前を使います。

ここでAssetBundleに入れたいアセットがばらばらの名前だと、1つずつファイル名を書くか、
~~~cs
List<AssetBundleBuild> builds = new List<AssetBundleBuild>();

var asset0 = new AssetBundleBuild();
asset0.assetBundleName = "musicasset0";
asset0.assetNames = new string[] { "Assets/Musics/かっこいい感じの曲.mp3" };
builds.Add(asset0);

var asset1 = new AssetBundleBuild();
asset1.assetBundleName = "musicasset1";
asset1.assetNames = new string[] { "Assets/Musics/きれいな感じの曲.mp3" }; 
builds.Add(asset1);
~~~
またはファイル名をListにしてforで回すことになると思います。
~~~cs
List<string> files = new List<string>();
files.Add("Assets/Musics/かっこいい感じの曲.mp3");
files.Add("Assets/Musics/きれいな感じの曲.mp3");

List<AssetBundleBuild> builds = new List<AssetBundleBuild>();

for (int i = 0; i < files.Count; i++)
{
    var asset = new AssetBundleBuild();
    asset.assetBundleName = $"musicasset{i}";
    asset.assetNames = new string[] { files[i] };
    builds.Add(asset);
}
~~~

そこで、コードから取得しやすいようにファイルの名前を統一します。
~~~cs
// ファイル名を music0.mp3, music1.mp3 , ... , musicN.mp3 と統一した場合
// music0, music3, music11 をAssetBundleにしたい 
int[] fileNumbers = { 0, 3, 11 };

List<AssetBundleBuild> builds = new List<AssetBundleBuild>();

for (int i = 0; i < fileNumbers.Length; i++)
{
    var asset = new AssetBundleBuild();
    asset.assetBundleName = $"musicasset{fileNumbers[i]}";
    asset.assetNames = new string[] {$"Assets/Musics/music{fileNumbers[i]}.mp3" };
    builds.Add(asset);
}
~~~

fileNumbersの数字を変えるだけでできるようになりましたが、曲名が"music0"のようになってしまい、どんな曲なのかわかりにくくなってしまいました。

そこで**ScriptableObject**を使います。

MusicAssetというScriptableObjectを作成し、その中に情報を入れたものがこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/60727777-e77d-eec3-8ef8-6591e2247fe0.png)


このアセットの名前は"music0"なのでコードから取得しやすく、曲の名前はそのままなのでわかりやすいままです。
また、後からMusicAssetにジャンル名などを追加するときでもMusicAssetのAssetBundleをビルドし直すだけなのでコードが変わらない所もいいなと思います。

以上の理由からScriptableObjectを作ります。

### 1-2 ScriptableObjectの作成

"MusicAssets"等、フォルダを作成してその中にScriptableObjectを作っていきます。

まずは元になるクラスを作っていきます
C# Scriptを作成し、今回は名前をMusicAssetにします。

コードは以下の通り
~~~cs:MusicAsset.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//AssetMenuにMusicAssetを追加
[CreateAssetMenu]
public class MusicAsset : ScriptableObject
{
    public AudioClip audioClip;
}
~~~

AssetMenuからMusicAssetを作って名前を"music" + 数字 にしておきます。
audioClipに音楽ファイルを入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/9bf1c16a-1ddf-6560-525a-f7aa9ec9b3bd.png)

これでScriptableObjectの用意は終了です。

# 2.AssetBundleをビルドする
### 2-1 StreamingAssetsフォルダ
AssetBundleを入れる場所としてStreamingAssetsフォルダを作ります。

StreamingAssetsフォルダとはこういうものです
> Unity は、Unity プロジェクトの StreamingAssets (大文字小文字を区別します) という名のフォルダーに配置したファイルを、ターゲットマシンの特定のフォルダーにそのまま何も変更されない状態で保持します。フォルダーを取得するには、Application.streamingAssetsPath プロパティを使います。StreamingAssets の場所を取得するには、Application.streamingAssetsPath を使うことが、常に最善の方法です。プラットフォーム上でアプリケーションが実行されている正確な場所を示すからです。(https://docs.unity3d.com/ja/2021.3/Manual/StreamingAssets.html)

つまり、このStreamingAssetsフォルダにアセットを配置しておくと、`Application.streamingAssetsPath`を使って配置したアセットをロードできるようになります。


### 2-2 Editorフォルダ
AssetBundleをビルドするためのスクリプトを入れる場所としてEditorフォルダを作ります。
>Editor と呼ばれるフォルダーに置かれたスクリプトは、ランタイムスクリプトではなく、エディター用のスクリプトとして扱われます。これらのスクリプトは開発時にエディターに機能を追加するもので、ランタイムにはビルドでは使用できません。(https://docs.unity3d.com/ja/2019.4/Manual/SpecialFolders.html)

Editorフォルダの中に"BuilldMusicAsset"という名前のC#Scriptを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/67f94c06-ba30-c868-a4a8-d54a70eed12c.png)


### 2-3 AssetBundleとは
AssetBundleは複数のアセットを1つのファイルにまとめられる機能です。

決めるものは2つあります
・入れ物の名前を何にするか
・何を入れるか

先ほどのコードの一部を持ってくると
~~~cs
var asset0 = new AssetBundleBuild();
//入れ物の名前を musicasset0 にする
asset0.assetBundleName = "musicasset0";
//Assets/Musics/ にある music0.mp3 を入れる
asset0.assetNames = new string[] { "Assets/Musics/music0.mp3" }; 
~~~
という感じです。

### 2-4 AssetBundleをビルドする
実際にMusicAssetの music0.asset, music1.asset をビルドするスクリプトを書きます。

~~~cs:BuildMusicAsset.cs
using System.Collections;
using System.Collections.Generic;
using UnityEditor;

public class BuilldMusicAsset
{
    //Assetの中に Build MusicAsset という項目を追加し、クリックで呼び出せるようにする
    [MenuItem("Assets/Build MusicAsset")]
    static void BuildAsset()
    {
        // MusicAssetの music0, music1 をAssetBundleにする

        int[] fileNumbers = { 0, 1 };

        List<AssetBundleBuild> builds = new List<AssetBundleBuild>();

        for (int i = 0; i < fileNumbers.Length; i++)
        {
            var asset = new AssetBundleBuild();
            asset.assetBundleName = $"musicasset{fileNumbers[i]}";
            // ScriptableObject の拡張子は asset
            asset.assetNames = new string[] {$"Assets/MusicAssets/music{fileNumbers[i]}.asset" };
            builds.Add(asset);
        }

        //Windows向けにAssetBundleをビルドする
        BuildPipeline.BuildAssetBundles(
            "Assets/StreamingAssets",
            builds.ToArray(),
            BuildAssetBundleOptions.None,
            BuildTarget.StandaloneWindows64);
    }
}
~~~


これをUnityEditorの上のバーにあるAsset→Build MusicAsset をクリックしてビルドすると、StreamingAssetsフォルダの中にAssetBundleとmanifestファイル、metaファイルが作られます。
![build succes.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/f779a314-a03a-363c-5d00-667b419f20fa.jpeg)

### 2-5 AssetBundleの中身を確認する

ビルドの中身を確認する機能を[こちらの記事](https://tsubakit1.hateblo.jp/entry/2019/03/28/102821 "【Unity】AssetBundleに実際に含まれるアセット一覧を確認する")で提供してくださっています。

実際にmusicasset0の中身を見てみるとこんな感じ

![checker.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/83768879-c5ee-338f-2740-1a25778ed761.jpeg)

ちゃんとMusicAssetのmusic0とaudioClipに入れていた「かっこいい感じの曲」がAsssetBundleの中に入っていることが確認できました。

次の項でこれをロードして音楽を再生します。

# 3.AssetBundleをロードする
### 3-1 AssetBundleのLoad/Unloadを知る
ロードするとなるとUnloadする必要があります
そこで、AssetBundleのLoad/Unloadの仕組みを確認します。

[こちらの記事](https://qiita.com/k7a/items/d27640ac0276214fc850 "AssetBundleを完全に理解する")でわかりやすく紹介してくださっています。

簡単にまとめると、
・AssetBundleのロードには2段階あり、
　　1段階目：メタ情報をロードする　AssetBundle.LoadFromFile
　　2段階目：アセット本体をロードする　LoadAsset
　のようになっている

・Unloadには2種類あり、
　　Unload(true)では、指定したAssetBundleのメタ情報とアセット本体をUnloadする
　　Unload(false)では、指定したAssetBundleのメタ情報のみをUnloadし、アセット本体はResources.UnloadUnusedAssets等でUnloadする

という感じです。

### 3-2 どのようにLoad/Unloadするか決める
音楽ファイルをLoad/Unloadしようとしているということは、メモリ使用量のことを考えてだと思うので、Load/Unloadを いつするか・どのようにするか が大切だと思います。


使ってよいメモリの量とロードにかかってよい時間から変わると思いますが、次のようなやり方が挙げられます。

**1.** 初めにすべてのAssetBundleのメタ情報をLoadしておき、アセットが必要になったタイミングでLoadし、タイミングを見てResources.UnloadUnusedAssets等でUnloadする。メタ情報のUnloadはアプリ終了時にまとめてする。

**2.** アセットが必要になったタイミングでメタ情報・アセット本体両方をLoadし、入れ替わりで前に使っていた曲のAssetBundleをUnload(true)でUnloadする。

1のやり方ではメタ情報をLoadする時間をはじめに集められるため、ロードにかかる時間が2のやり方よりも短くなりますが、その分メタ情報がずっとロードされた状態になります。
筆者の環境で1~3分の曲を16個AssetBundleにし、全てのメタ情報をLoadした所、20~30MBメモリを使いました。

2のやり方ではロードされているアセットは常に1つですが、メタ情報のロード分ロードにかかる時間が長くなります。

今記事では1のやり方でやります。

### 3-3 Load/Unloadの処理を書く

Start関数の中ですべてのメタ情報を読み込み、ボタンを押すとアセット本体をロードして曲が変わるようにします。

真ん中に曲を変更するボタンを配置し…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/96259abe-dc8b-c84d-9a54-b1e671f1347b.png)

AssetBundleのLoad/Unloadを行う"LoadMusic"と曲を再生するAudioSourceを配置します。

![loderobj.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3749182/927846e7-80ae-106d-fd0e-d3cc30d5aade.jpeg)

スクリプトを書きます
~~~cs:LoderMusic.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LoadMusic : MonoBehaviour
{
    // ロードしたAssetBundleを入れるリスト
    List<AssetBundle> bundles;

    //使いたいAssetBundleの番号配列
    int[] assetNumbers = { 0, 1 };

    AudioSource audioSource;

    void Start()
    {
        bundles = new List<AssetBundle>();

        for (int i = 0; i < assetNumbers.Length; i++)
        {
            // musicasset0, musicasset1のメタ情報をロード
            // ロードするファイルのパスは Application.streamingAssetsPath を使って書ける
            var asset = AssetBundle.LoadFromFile($"{Application.streamingAssetsPath}/musicasset{assetNumbers[i]}");
            bundles.Add(asset);
        }

        audioSource = GetComponent<AudioSource>();
    }

    int musicIndex = 0;
    public void LoadAndPlayMusicAsset()
    {
        // MusicAssetをAssetBundleのbundles[musicIndex]からロードする
        // "music0"等、ロードしたいアセットの名前を入れる
        MusicAsset musicAsset = bundles[musicIndex].LoadAsset<MusicAsset>($"music{assetNumbers[musicIndex]}");
        
        // MusicAssetの中のaudioClipをaudioSourceに渡して再生
        AudioClip audioClip = musicAsset.audioClip;
        audioSource.clip = audioClip;
        audioSource.Play();

        //次にボタンを押したときは別の曲を再生する
        musicIndex = (musicIndex + 1) % bundles.Count;

        //前のMusicAssetをUnloadする
        Resources.UnloadUnusedAssets();
    }

    //終了時にメタ情報をUnloadする
    private void OnApplicationQuit()
    {
        foreach (AssetBundle bundle in bundles)
        {
            bundle.Unload(true);
        }
    }
}
~~~

ボタンに関数を割り当ててゲームを始めるとちゃんと再生できます。

しかし…
<iframe width="560" height="315" src="https://www.youtube.com/embed/_byG9iSzprI?si=DU0C0KSVDi91rQVe" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

明らかにカクついている！

ということで次の項でカクツキを減らします。

### 3-4 Load/Unloadのカクツキを減らす
#### ・非同期でLoadする
[こちらの記事](https://kan-kikuchi.hatenablog.com/entry/Resources_LoadAsync "Resourcesのアセットを非同期でロードする【Unity】")を参考にLoadTypeをStreamingに変更します

#### ・音楽ファイルのLoadTypeをStreamingにする
[こちらの記事](https://qiita.com/uchiko/items/91de62f4cc2d243b479f#assetbundle%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB "AssetBundleのサンプルを作成した時のメモ")を参考に
LoadFromFileをLoadFromFileAsyncに、LoadAssetをLoadAssetAsyncに変更します

結果

<iframe width="560" height="315" src="https://www.youtube.com/embed/eayQbyTge6c?si=wWCJ2g7tFg88KimT" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

かなり緩和できました

# 問題点
LoadTypeをStreamingにしたことでCPU負荷が上がってます

頻繁に使う曲を変えない場合や、曲を変えるときはロードを挟む場合、カクついても問題にならないので音楽を変えるタイミングによっても実装を変えたほうが良さそうです。

あとは
サーバーからのAssetBundleのダウンロードや、AssetBundleのバージョン管理などうまく実装出来たら続きを書きたいと思います


# 参考にさせていただいた記事
https://docs.unity3d.com/ja/2021.3/Manual/StreamingAssets.html

https://docs.unity3d.com/ja/2019.4/Manual/SpecialFolders.html

https://tsubakit1.hateblo.jp/entry/2019/03/28/102821

https://qiita.com/k7a/items/d27640ac0276214fc850

https://yakuentei.jp/article/gamedev/unity_asset_bundle/

https://tsubakit1.hateblo.jp/entry/2016/07/19/235900

https://qiita.com/uchiko/items/91de62f4cc2d243b479f

https://kan-kikuchi.hatenablog.com/entry/Resources_LoadAsync