Unity の StreamingAssets に UnityEngine.WWW クラスでアクセスする時の URI

fb-icon

Unity のゲームでストリーミングアセットを利用することを考えます。

ストリーミングアセットは UnityEngine.WWW (以下 WWW クラス) に file:// プロトコルの URI で場所を指定するとアクセスできます。

しかし単純に "file://" + UnityEngine.Application.streamingAssetsPath + path などとしてコンストラクター引数にぶん投げても動く保証はありません。
というのも、検索してみると “%XX” 形式の文字がデコードされてしまうとか “#” より後の文字が無視されるとか…とにかく一筋縄ではいかないようです。

というわけで、URI の取得方法を調べてみましたので、まとめます。

ストリーミングアセットの URI を取得する方法

検証環境

  • Unity 5.3.4p4
  • Windows 10 (x64)
  • mac OS Sierra (x64)
  • iOS 10.2.1
  • Android 6.0 他公式エミュレーター (AVD) 複数

iOS, macOS の場合

とりあえず URI というからには URI エスケープなるものが必要ですよね?
ところで .NET には System.UriBuilder という便利なクラスがあります。
これを使うと、適切にエスケープされた URI を簡単に取得できます。
System.UriBuilder(string, string, int, string, string) コンストラクター (外部)

    /// <summary>
    /// ストリーミングアセットの基底 URI を作成します。
    /// </summary>
    /// <returns>The streaming assets URI.</returns>
    /// <param name="path">
    /// <see cref="UnityEngine.Application.streamingAssetsPath"/>.
    /// </param>
    /// <summary>※ 引数で渡してるのは単体テストのため</summary>
    public static string CreateStreamingAssetsUri(string path)
    {
        return new UriBuilder(Uri.UriSchemeFile, string.Empty, -1,
            path, string.Empty).Uri.AbsoluteUri;
    }

Android, WebGL の場合

Android や Web で動くときは、 UnityEngine.Application.streamingAssetsPath がすでに正規な URI 形式なので、読みたいストリーミングアセットのパスを結合するだけでおkです。

    /// <summary>
    /// ストリーミングアセットの基底 URI を作成します。
    /// (Web, Android 用)
    /// </summary>
    /// <returns>The streaming assets URI for android.</returns>
    /// <param name="path">
    /// <see cref="UnityEngine.Application.streamingAssetsPath"/>.
    /// </param>
    /// <summary>※ 引数で渡してるのは単体テストのため</summary>
    public static string CreateStreamingAssetsUriForAndroid(string path)
    {
        // Web, Android では streamingAssetsPath がすでに URI
        return path;
    }

ただしこれに続く、アセットのパスの方は何故かAndroid 5 以上でのみエスケープが必要になります。 nandeyanen
※ Android 版ビルドではストリーミングアセットのファイル名に ‘#’ 記号や日本語を含めることはできないようです。

    /// <summary>
    /// ストリーミングアセットの URI を取得します。
    /// </summary>
    /// <returns>アセットの URI</returns>
    /// <param name="path">Assets/StreamingAssets からの相対パス ('/' 区切り)</param>
    public static string GetStreamingAssetsUri(string path)
    {
#if !UNITY_EDITOR && URI_ANDROID
        // Android 5 未満では URI エスケープ不要
        using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
        {
            if (version.GetStatic<int>("SDK_INT") < 21)
            {
                return BaseUri + path;
            }
        }
#endif
        return BaseUri + EscapeUriPath(path);
    }

BaseUri は上記の CreateStreamingAssetsUriForAndroid(string) で取得済とする
EscapeUriPath(string) については下記を参照

Windows の場合

Windows ではディレクトリ区切りがそもそも / ではないので macOS の時と同じコードは使えません。
調べてみるSystem.Uri(string) コンストラクター にパスを放り込んで AbsolutePath をgetすれば万事解決! な情報が出てきますが…
同時に、パスに “%XX” 形式の文字列や ‘#’ が含まれてるとそれらを適切にエスケープしてくれない (前者はむしろデコードされる) という情報も出てきます。
Convert file path to a file URI? (Stack Overflow) (外部)
この問題を解決する方法を僕なりに色々考えてみましたが…
結局、あらかじめ問題の文字をエスケープしておくことにいたしました。

せっかくなので、 ‘%’, ‘#’ 以外の URI 予約文字、制御文字、その他 non-ASCII 文字もエスケープしちゃいます。
予約文字に関しては URI の仕様書で “reserved” を検索したら定義が見つかったので、それを参考に URI 予約文字の正規表現を書いて…

    /// <summary>
    /// 予約文字 (ディレクトリ区切りを除く) にマッチする正規表現
    /// </summary>
    private static Regex reservedCharsRegex_ = null;
    /// <summary>
    /// URI 予約文字 (ディレクトリ区切りを除く) にマッチする正規表現を取得します。
    /// </summary>
    /// <values>予約文字 (':', '/' を除く) にマッチする正規表現</values>
    public static Regex ReservedCharsRegex
    {
        get
        {
            if (reservedCharsRegex_ == null)
            {
                reservedCharsRegex_ = new Regex(
                    @"[\ud800-\udbff][\udc00-\udfff]|[\u0000-\u0020\?\#\[\]@!\$&\(\)'\*\+,;=%\u007f-\uffff]"
                );
            }
            return reservedCharsRegex_;
        }
    }

それを置換する関数を書いて…

using System.Linq;
/* (中略) */
    /// <summary>
    /// ディレクトリ区切りを除く予約文字をエスケープ
    /// </summary>
    /// <returns>The URI path.</returns>
    /// <param name="path">Path.</param>
    static string EscapeUriPath(string path)
    {
        return ReservedCharsRegex.Replace(path,
            m => string.Join(string.Empty,
                System.Text.Encoding.UTF8.GetBytes(m.Value)
                .Select(it => string.Format("%{0:X2}", it)).ToArray()
        ));
    }

これで Application.streamingAssetsPath をエスケープした上で System.Uri(string) コンストラクターに渡すと、ようやく取得できます。

    /// <summary>
    /// ストリーミングアセットの基底 URI を作成します。
    /// (Windows 用)
    /// </summary>
    /// <returns>The streaming assets URI for windows.</returns>
    /// <param name="path">
    /// <see cref="UnityEngine.Application.streamingAssetsPath"/>.
    /// </param>
    /// <summary>※ 引数で渡してるのは単体テストのため</summary>
    public static string CreateStreamingAssetsUriForWindows(string path)
    {
        // Windows 系は URI エスケープして System.Uri に投げると作ってくれる
        return new Uri(EscapeUriPath(path)).AbsoluteUri;
    }

まとめ

以上をまとめたクラスを Gist にアップロードします。

→ StreamingAssetsUriGen.cs (Gist)

パスに含められる文字

ついでなので、ストリーミングアセットの(絶対)パスに含めることのできる文字をまとめました。
PC は流石の対応ですね。しかし PC は完璧対応じゃないとユーザーが奇抜なパスにインストールした時にバグって文句言われてしまうのでむしろ完璧であるべきとも思ったり。
iOS, Android は、まあインストール先のパスが選択できないので、開発者が気をつけてさえいればおkです。

※ 特筆ない文字はちゃんとエスケープした方が無難です。

検証環境\文字 (半角スペース) % # その他予約文字 日本語
Windows 10 (x64 エディター) O O O O O (エスケープするなら UTF-8)
macOS Sierra (x64 エディター) O O O O O (エスケープするなら UTF-8)
iOS 10 (iPad mini 2 実機) O O O O O (UTF-8 でエスケープ必須)
Android 4.x
(API Lv.10/16/19 公式エミュ、Lv.17 実機)
O (エスケープしてはいけない) O (エスケープしてはいけない) X (JAR の仕様?) O (エスケープしてはいけない) X (ビルドエラーのため同梱不可)
Android 5 以上
(API Lv.21/25 公式エミュ、Lv.24 実機)
O O X (JAR の仕様?) O X (ビルドエラーのため同梱不可)

参考 (外部リンク)