【Unity】StreamingAssetsに配置したファイルをAndroidで実行時に読み込む

# はじめに

想定読者様はこちらです。
- StreamingAssets(ストリーミングアセッツ)に配置したファイルをAndroidビルド時に使用したいお方
- 具体的にどのような仕組みでAndroid実行時に読み込んでいるのか知りたいお方

調べてみたところ方法は書いてあっても順序を追って仕組みを説明してくださる方は見かけなかったので確認の意味合いも込めて仕組みについても言及させていただきます。

:::note warn
**折りたたみ**
折りたたみになっている個所は自身の解釈を織り交ぜて解説をしております。
間違い等のご指摘は大変に歓迎しておりますのでお時間のある方はぜひお願いいたします。
:::

# 実装

解説するに際してこのようなものを作成してみました。

```C#:StreamingAssetsPathFormatter.cs
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using Cysharp.Threading.Tasks;

/// <summary>
/// 要求に適したStreamingAssetsパスを返却します
/// </summary>
public static class StreamingAssetsPathConfigurator
{
    // キャッシュしたアンドロイドパスを保存
    private static List<string> _androidCachePath = new();

    /// <summary>
    /// StreamingAssetsのファイルにアクセスするためのファイルパスを返却します
    /// </summary>
    /// <param name="relativePath">指定ファイルのStreamingAssetsからの相対パス</param>
    /// <returns></returns>
    public static async UniTask<string> GetStreamingAssetsPathAsync(string relativePath)
    {
        // Android実行時jarファイルのURIを取得
        string resultPath = @$"{Application.streamingAssetsPath}/{relativePath}";

#if UNITY_ANDROID
        // デバイス上のパブリックディレクトリを取得する
        string persistentDataPath = @$"{Application.persistentDataPath}/{relativePath}";

        // キャッシュしてあるパスなら早期リターン
        if (_androidCachePath.Contains(persistentDataPath))
        {
            return persistentDataPath;
        }

        var request = UnityWebRequest.Get(resultPath);
        
        // UniTaskの機能で非同期に読み込む
        await request.SendWebRequest();

        // 読み込んだデータをパブリックディレクトリへ移す
        File.WriteAllBytes(persistentDataPath, request.downloadHandler.data);

        // 結果のパスをパブリックディレクトリのパスに書き換え
        resultPath = persistentDataPath;

        // 一度取得したファイルへのパスはキャッシュする
        _androidCachePath.Add(resultPath);
#endif

        return resultPath;
    }
}
```

上記のプログラムを一言で説明すると**GetStreamingAssetPathAsyncを使用するユーザーが求めていると予想されるパスを返却するもの**です。

# 解説

順に解説させていただきます。

### 1. GetStreamingAssetsPathAsyncの引数について

まず**relativePath**についてですがこれはStreamingAssetsから読み込みたいファイルへの相対パスを入力します。

 例えば、下記のようなディレクトリ構成であった場合**relativePath**は`Audios/Click.wav`となります。
>StreamingAssets/
　└Audios/ 
　　　└Click.wav

### 2. Application.streamingAssetsPathの仕様

次に返却するためのパスを`@$"{Application.streamingAssetsPath}/{relativePath}"`で取得します。

ここでStreamingAssetsの仕様を知る必要が発生します。

その仕様とはAndroidビルド時に`Application.streamingAssetsPath`はあらかじめStreamingAssetsに配置したアセットを、jarファイルとして圧縮したデータが配置されている**URI**を返却するということです。

<details><summary>URIとは</summary>

>**URI**は*URL*(Web上における住所のようなもの)と*URN*(識別するための名前)が組み合わさったものです。
*URL*がパス、*URN*がファイル名だといえばわかりやすいでしょうか。
一般的なWebページにアクセスするための*URL*は*URN*を含んでいるので、実は*URL*ではなく**URI**なのです。

</details>

<details><summary>jarファイルとは</summary>

> **jarファイル**とは*java*でよく使用されているものです。
*zip*形式とほぼ同じ圧縮方式である**jar**形式で圧縮されています。
*zip*ファイルとの違いは**解凍しなくても使用することができる点**などがあげられます。

</details>

仕様を確認したところで解説を再開します。

### 3. #if UNITY_ANDROIDについて

`#if UNITY_ANDROID`という**プリプロセッサディレクティブ**を使用することで**条件付きコンパイル**を行い、Androidビルド時に特別な処理を行っています。

<details><summary>プリプロセッサディレクティブと条件付きコンパイル</summary>

> プリプロセッサディレクティブを使用することでコンパイルされる情報を特定の条件に応じて変更することができます。
これを条件付きコンパイルと呼び、無用なコンパイルを避けることができます。

</details>


ところで`#if UNITY_ANDROID && !UNITY_EDITOR`を使わないことに違和感を持たれた方も少なくないのではないでしょうか。

ここにつきましては私が以下のように考えているからです。

- リクエストを行わずパフォーマンスを向上させたとしてもAndroidで稼働させるときには意味がないこと
- 及び、実機より性能が良いと考えられる開発側で処理が重いと感じる場合は処理を見直す必要があると考えられること
(見直しの必要を発見することも遅れると考えられます)
- また、記述ミスなどでエラーが発生している場合もエラーの発見が遅れること

無用なファイルの生成、および、リクエストを行うことにつながることも事実ですのでご自由に書き換えていただければと考えております。

### 4. Application.persistentDataPathの仕様

続いて`string persistentDataPath = @$"{Application.persistentDataPath}/{relativePath}";`について解説させていただきます。

こちらはコメントに書いてある通りデバイス上のパブリックディレクトリの位置を取得しています。
セーブデータなどを保存するときはここに保存することも多いようです。

### 5. UnityWebRequestを利用したjarファイルの読み込み

続いて……
```
var request = UnityWebRequest.Get(resultPath);
await request.SendWebRequest();
```
こちらですが`@$"{Application.streamingAssetsPath}/{relativePath}`で取得したURIを使用してjarファイルを取得しています。

**UniTask**の`await`を使用して処理を待機していますが、`IEnumerator`を使った**コルーチン**であれば`yield return`を使用して待機することも可能です。

### 6. パブリックディレクトリへのjarファイルのデータ書き込み

そして`File.WriteAllBytes(persistentDataPath, request.downloadHandler.data);`を行うことで、byte[]型でjarファイルの内容を取得し、デバイス上のパブリックディレクトリに書き込んでいます。

### 7. 疑似的なStreamingAssetsPathの返却

続いて`resultPath = persistentDataPath;`を行うことで**疑似的**にStreamingAssetsのファイルパスを取得したように見せかけています。

### 8. パスのキャッシュ

そして`_androidCachePath.Add(resultPath);`で一度jarファイルを取得しパブリックディレクトリに書き込んだことのあるファイルパスはキャッシュされ……
```
if (_androidCachePath.Contains(persistentDataPath))
{
    return persistentDataPath;
}
```
を行うことでキャッシュ済みのパスか確認を行い、初回のみリクエストを行うことで不要なリクエストを避けています。

# 最後に
AndroidでStreamingAssetsを使用する際に困った方は少なくないと思います。

この記事が皆様のお役に立てれば幸いです。

今年(2024年)の夏は大変に暑くなっております。
皆様ご自愛ください。