Unity + PlayFab + ADX2でAsset Bundleを介さないサウンドデータの配信を実装する

# Unityゲームアプリのサウンドデータをサーバーからダウンロードする

スマートフォンゲーム開発において、アプリの初期インストールサイズを小さく抑えることは必須課題です。
ストアに置くアプリは最小限のデータを持たせ、ゲーム起動後にリソースを外部のサーバーからダウンロードする手法はよく見ます。ゲームアプリの中で特にファイルの容量を大きく占めるデータは画像と音声データです。

そこで今回は、Unityゲーム開発アプリにおいて、音声データをビルド結果から切り離し、ゲーム起動後に外部のサーバーからダウンロードする仕組みを作ってみます。
サーバーとCDNを介したデータの配信には、Microsoftのゲーム向けBaaSである「PlayFab」を使います。また、サウンド再生ライブラリとして「CRI ADX2 LE」を使います。
本記事では、WindowsとiOSでの動作を確認しています。

※サーバーから直接ストリーミング再生をするのなく、ゲーム用データとして端末ストレージにダウンロード・保存して、そこからサウンドを再生する仕組みです。

### PlayFab
アプリから外部リソースを取得する場合、何らかの方法でネット経由でデータを渡さなくてはなりません。
ある程度の規模のある開発現場では、AWSやGCP、Azureのサービス群でデータをホストし、CDNサービスを通じてデータ配信をするネットワークシステムを構築することが通例です。

ただ、個人ゲームアプリや小規模なプロジェクトの場合は、でそうしたバックエンドを構築するのは手間が大きく、効率的ではありません。かといってGoogle DriveやDropboxなどのクラウドドライブやアップローダーは、こうしたゲーム用データ配布には使用できません。不特定多数のユーザーから大量かつ同時にアクセスされる使われ方を想定していないため、すぐにアクセス制限がかかります。

![playfab.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/f9ff5167-d5a4-c9c5-177d-5e0fd7b5a4ac.png)


そこで、データ配信のCDN機能を併せ持つゲーム向けバックエンドサービスである「Microsoft PlayFab」を使います。

**PlayFab**
https://developer.playfab.com/


実装にはこちらの記事を参考にしました。

UnityでPlayFabのFileContentをDownloadしてSaveする
https://qiita.com/simplestar/items/47dcfaa213a62a7aa360

### CRI ADX2 LE
Unityでは外部からリソースをダウンロードして利用する場合、Asset Bundleを経由する必要があります。画像などバイナリから直接インポートできるものもありますが、Unity標準サウンド機能のAudio ClipはAsset Bundle化して渡す必要があります。Asset Bundleの運用にはひと手間かかります。
（oggやmp3を直接読むNetworking.UnityWebRequestMultimedia.GetAudioClip()は、キャッシュの機能が無いので不向き?）

そこで、統合型サウンドミドルウェアの「CRI ADX2」をUnityプロジェクトに組み込んで使います。今回は無償版である「CRI ADX2 LE」を利用しました。バージョンは v2.10.05です。

![LELOGO_512_t.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/97a9964a-a974-d979-8d90-b7130b85ecdf.png)

**ADX2 LE**
https://game.criware.jp/products/adx2-le/

CRI ADX2は、圧縮音声データをUnityとは別のツール(SDKに含まれているCRI Atom Craft)で生成し、StreamingAssetsからバイナリとしてロード・マウントします。
Unityのアセット管理の管轄外データになるため、Asset Bundleを経由せずにサーバーから追加のデータをダウンロードし、利用できます。
また、ADX2のデータはSceneやPrefabが参照を持たないため、自動でビルドに含まれてしまう、ということがありません。ゲームアプリ本体に含めるデータと、外部からダウンロードしてもらうデータを切り分けることが容易になります。

導入までのチュートリアル、Atom Craftの基本的な使い方についてはこちらをご覧ください。

Unityのサウンド機能をADX2で強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377

## 実装手順
実装の手順としては次のようになります。

0.Unity開発環境にPlayFab, CRI ADX2を組み込む
1.Atom Craftでサーバーアップロード用のデータを作る
2.PlayFabのファイル管理（FileContent）にアップロードする
3.UnityからPlayFabのファイルURLを取得
4.ファイルをダウンロードし、端末ストレージに保存する
5.ストレージからファイルをADX2にマウントする
6.再生

### Atom Craftでサーバーアップロード用のデータを作る
まずはCRI Atom Craftを起動して、配信用のキューシートを作成し、再生するキューを用意します。

![新しいキューシート・キューを作る.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/4ca3ead1-c4dd-c4ef-cb75-4f67b60bb144.png)

例では「NewBGM」というキューシートとしています。キューシートの中にはキューID「1」のキュー「BGM_House」が登録されています。
キューシート名、キュー名は任意の名前で問題ありません。DLCや配信イベントの名前など、外部サーバーから渡すデータであることが分かりやすい名前にするとよいでしょう。

Atom Craftの上では、配信用のデータとゲームアプリ本体に収録するデータの違いはありません。キューを作成したら、通常通りビルドを行います。
ビルドを完了したら、出力したデータの保存先ディレクトリを開いておきます。

デフォルトでは C:\Users\[ユーザー名]\Documents\CRIWARE\CriAtomCraft\[プロジェクト名]\Public\[ワークユニット名]\の中です。

### PlayFabのファイル管理（FileContent）にアップロードする

PlayFabの管理画面にログインし、ゲームのプロジェクトのダッシュボードを開いてから、左メニューから「コンテンツ」をクリックします。
「ファイル管理」タブ（英語版インタフェースではFileContent）を開きます。ここに配布するデータをアップロードします。

「新しいフォルダー」をクリックして音楽データ保存用のmusicフォルダを作り、その中で「ファイルをアップロード」を選びます。
先ほどビルドしたファイルから、配布したいキューシートデータをアップします。この場合はNewBGM.acbをアップロードします。

![playfabadx2data.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/0d750bd0-9865-725c-32b6-bbc409ec2568.png)

ストリーミング再生（スマホのストレージからメモリへストリーミングするの意味、ネットストリームではない）を使う場合は、ファイルがacbとawbの2種類必要になります。
その際は両方のファイルをアップロードするようにします。

### UnityからPlayFabのファイルURLを取得

アップロードしたファイルをダウンロードしてみましょう。
PlayFabのファイルにアクセスするためのURLを取得するコードを用意します。

```PlayFabController.cs

    public IEnumerator GetFileCdnUrl(string key)
    {
        string url = string.Empty;
        
        PlayFabClientAPI.GetContentDownloadUrl(new GetContentDownloadUrlRequest()
            {
                Key = key,
                ThruCDN = true
            }, result =>
            {
                url = result.URL;
            },
            error => Debug.LogError(error.GenerateErrorReport()));

        while (string.IsNullOrEmpty(url))
        {
            yield return null;
        }

        yield return url;
    }

```

PlayFabCrientAPI.GetContentDownloadUrlメソッドをもちいて、指定の名称のファイルのURLを取得します。
このメソッドを実行する前にユーザーとしてログインする必要があります。

keyにはPlayFabのコンソール上で作ったディレクトリ名とファイル名を入れます。例では、music/NewBGM.acbを指定します。また、CDN経由の配信であるフラグThruCDNにtrueを指定します。

### ファイルをダウンロードし、端末ストレージに保存する

UnityWebRequestを用いて先ほどのURLにアクセスし、ストレージにファイルをダウンロードします。

```ExternalDataManager.cs

public class ExternalDataManager : MonoBehaviour
{
    private const string NoBackUpDirectory = "GameData";
    private string deviceGameDataPath;
    
    private void Awake()
    {
        deviceGameDataPath = Path.Combine(Application.persistentDataPath, NoBackUpDirectory);

        if (Directory.Exists(deviceGameDataPath) == false)
        {
            Directory.CreateDirectory(deviceGameDataPath);
            
         //NoBackUpDirectoryはiCloudバックアップから外す//
#if UNITY_IOS
        UnityEngine.iOS.Device.SetNoBackupFlag(Path.Combine(Application.persistentDataPath, NoBackUpDirectory));
#endif
        }
    }

    public IEnumerator DownLoadDataToStorage(string fileName, string url)
    {
        Debug.Log("start download " +url);
        UnityWebRequest www = UnityWebRequest.Get(url);
        yield return www.SendWebRequest();

        if (www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
            
            yield return null;
        }
        else
        {
            byte[] results = www.downloadHandler.data;

            var filePath = Path.Combine(deviceGameDataPath, fileName);

            string directoryName = Path.GetDirectoryName(filePath);

            if (directoryName != null && Directory.Exists(directoryName) == false)
            {
                Directory.CreateDirectory(directoryName);
            }

            File.WriteAllBytes(filePath, results);

            yield return filePath;
        }
    }
}


```
上記の例では、ファイルはApplication.persistentDataPathで取得できる領域に、「GameData」ディレクトリを用意してから保存しています。
PCの場合はC:\Users\[ユーザー名]\AppData\LocalLow\[組織名]\[アプリ名]\ になるので、この例ではGameData/musicディレクトリの下に acbファイルが保存されます。

また、iOS用の設定として、Awakeの中でGameDataファイルをiCloud管理下から外す処理を行っています。これは巨大なゲームデータを渡した場合、すべてiCloud Backupに含まれてしまうのを防ぐためです。

### ストレージからファイルをADX2にマウントする
ストレージに保存できたら、ファイルをADX2にキューシートとしてマウントします。


```ExternalDataManager.cs

  public string GetDataPathFromStorage(string fileName)
    {
        var di = new DirectoryInfo(deviceGameDataPath);
        var fileInfo = di.EnumerateFiles(fileName, SearchOption.AllDirectories).FirstOrDefault();

        return fileInfo?.FullName;
    }

```

上記の例は、指定のストレージデータフォルダ内にファイルが存在するか確認し、あればそのパスを渡すメソッドです。
ファイルパスが無事取得できたら、CriAtom.AddCueSheetAsyncメソッドを通じてマウントします。

```BGMPlayer.cs

    private CriAtomExAcb cueSheet;
    private CriAtomExPlayer criAtomExPlayer;

    private void Awake()
    {
        criAtomExPlayer = new CriAtomExPlayer();
    }
    
    public IEnumerator LoadCueSheetCoroutine(string cueSheetName, string path)
    {
        CriAtom.AddCueSheetAsync(cueSheetName, path, "");

        while (CriAtom.CueSheetsAreLoading == true)
        {
            yield return null;
        }
        
        cueSheet = CriAtom.GetCueSheet(cueSheetName).acb;
    }

    public void Play(int cueId)
    {
        criAtomExPlayer.SetCue(cueSheet, cueId);
        criAtomExPlayer.Start();
    }

```

呼び出し側は次の通りです。

まずストレージに必要なファイルがあるか確認し、なければPlayFabからURLを取得してストレージにダウンロードする、という処理順です。


``` LoadExternalMusicData.cs

    public string externalAcbFileName = "NewBGM.acb";
    public string externalAcbDirectoryName = "music";

    IEnumerator LoadExternalBGM()
    {
        var path = externalAcbDirectoryName + "/" + externalAcbFileName;

        var externalFileDataPath  = externalDataManager.GetDataPathFromStorage(path);

        if (string.IsNullOrEmpty(externalFileDataPath))
        {
            //ログイン後に呼ぶ
            IEnumerator getUrl = playFabController.GetFileCdnUrl(path);

            yield return getUrl;

            if (getUrl.Current != null)
            {
                string url = getUrl.Current.ToString();

                IEnumerator dataLoad =  externalDataManager.DownLoadDataToStorage(path, url);

                yield return dataLoad;

                object current = dataLoad.Current;

                externalFileDataPath = current.ToString();
            }
        }
        
        yield return bgmPlayer.LoadCueSheetCoroutine(externalAcbFileName, externalFileDataPath);
    }

```
### 再生

acbをマウントしてしまえば、再生方法は通常のADX2の利用方法と全く同じです。
BGMPlayer.csの例では、CriAtomExPlayerクラスを介して、キューIDを使って再生しています。

## 運用の方針
Unity + PlayFab + ADX2で、AssetBundleを介さないサウンドデータ配信が可能となりました。
ゲームアプリ内にはメニューボタン音は最初のBGMなど最低限のデータを用意しておき、起動後の追加リソースダウンロードして、その他のBGMやボイスデータをダウンロードする運用が可能になります。

今回はシンプルにストレージへ保存しましたが、1回しか再生しないイベントシーンで使うボイスデータは使ったらすぐストレージから消してもかまいませんし、プレイヤーにキャッシュを選ばせることも可能です。
最近のトレンドはゲーム機同時に「BGMやボイスデータを一括ダウンロードする / しない」を選ばせるスタイルですので、このオプションがあると一番良いかと思います。

また、ロードしたデータのアンロードについても管理が必要です。
その操作については、次の記事を参照にしてください。

Unity + ADX2におけるサウンドデータの読み込みと破棄
https://qiita.com/Takaaki_Ichijo/items/c7e14234f799fdca3e68

## おまけ：メタデータを埋め込み
acbファイルを単体で配信すると、そのデータが何のために使うものなのか、把握しづらい場面が想定できます。そこで、このファイルの中に文字情報を埋め込んでしまいましょう。

ADX2は音の単位を「キュー」という単位で処理しますが、キューにはさまざまな再生用設定の他、「ユーザーデータ」という領域に任意の文字列を保存し、スクリプトから読みだすことができます。
たとえば追加楽曲をADX2を使って配信するとき、作曲者や歌手などの曲にまつわるメタデータを「ユーザーデータ領域」に埋め込んで配信できます。

![ユーザーデータの埋め込み.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/c62c51dd-b5c8-6c6c-9645-8eb53eae6d2c.png)

ユーザーデータは、次の手続きでアクセスできます。

```AccessAcbUserData.cs

private CriAtomExAcb eventVoiceAcb;

public string GetVoiceUserData(int cueId)
{
    CriAtomEx.CueInfo cueInfo;
    eventVoiceAcb.GetCueInfo(cueId, out cueInfo);

    return cueInfo.userData;
}

```

これで、ある程度のメタデータは埋め込むことができます。