UnityのAssetBundleを使うときのお作法

AssetBundleについてはまだ勉強を始めたばかりで検証中なのですが、あまりにも情報がいっぱいあるがそれぞれでやり方が違う印象があるので、
いろいろ書いていって最適解を出したいと思います。

なので、この記事はまだ(仮)です。

AssetBundleについて

スマホアプリはインストール時にサイズを50MBにに抑えないとWifi経由でのインストールしかできなくなります。
それだけではなく、オフラインのときでも使えるようにローカルにキャッシュしてほしいなど、制限が結構あります。

そこでAssetBundleを使って、追加コンテンツとしてアプリ内にダウンロードできる仕組みです。

Unityを使って何かしらのスマホアプリを作るなら、割りと使うことになりそうな仕組みですね。

AssetBundleに含められるのは、

  • GameObject
  • Material
  • Texture
  • Prefab

などなど。

AssetBundleをiOS・Android用にビルドするにはiOS Pro・Android Proのライセンスが必要になります。
なので、これらを使わなければお金は掛からないのですが、使うとなると途端に結構な金額のライセンスが必要です。

デメリットとしてメモリを多く消費するという点があるようです。
なので、このAssetBundleをどうゆう単位で作り、いつ破棄するかが結構重要になりそうですね。

また、iOSとAndroidで別に書き出す必要があり、その書き出しのタイミングで最適化がされているようです。
別々のファイルになってしまうので、Unity側でどのAssetBundleを取りに行くかの分岐が必要です。

注意点として、ScriptはAssetBundleに含められません。
iPhone側の話しですが、動的にScriptを生成することを許可していないからです。

AssetBundleを使ってみる

UnityのAssetBundleをじっくり理解する手順をまとめてみた。【アセットバンドル】 – NAVER まとめ
こちらの記事が一番読みやすかったので、これにそって試してみました。

まずはAssetBundleを作成するためのEditor拡張を入れます。
BuildPipeline.BuildAssetBundle
こちらにあるコードを参考にしてたんですが、書いてあるコードにはiOS・Android用のビルドが書かれていなかったので、
Unity AssetBundle Examples.を参考にしました。

// C# の例
// プロジェクト ウィンドウの選択されたオブジェクトからアセットバンドルを作成
// コンパイルした後は "Menu" -> "Assets" へ移動して選択肢から一つを選択して
// アセットバンドルをビルド
using UnityEngine;
using UnityEditor;

public class ExportAssetBundles
{
    [
     MenuItem("Assets/Build AssetBundle From Selection - Track dependencies")]
    static void ExportResource ()
    {
        // 保存ウィンドウのパネルを表示
        string path = EditorUtility.SaveFilePanel ("Save Resource", "", "New Resource", "unity3d");
        if (path.Length != 0) {
            // アクティブなセレクションに対してリソースファイルをビルド

            Object[] selection =
                Selection.GetFiltered (typeof(Object), SelectionMode.DeepAssets);

            // via https://gist.github.com/yaeda/5410868
            // require iOS Pro, Android Pro Lisence
            // for Android
            BuildPipeline.BuildAssetBundle(Selection.activeObject,
                                           selection, path + ".android.unity3d",
                                           BuildAssetBundleOptions.CollectDependencies |
                                           BuildAssetBundleOptions.CompleteAssets,
                                           BuildTarget.Android);

            // for iPhone
            BuildPipeline.BuildAssetBundle(Selection.activeObject,
                                           selection, path + ".iphone.unity3d",
                                           BuildAssetBundleOptions.CollectDependencies |
                                           BuildAssetBundleOptions.CompleteAssets,
                                           BuildTarget.iPhone);

            // for WebPlayer
            BuildPipeline.BuildAssetBundle(Selection.activeObject,
                                           selection, path + ".unity3d",
                                           BuildAssetBundleOptions.CollectDependencies |
                                           BuildAssetBundleOptions.CompleteAssets,
                                           BuildTarget.WebPlayer);

            Selection.objects = selection;
        }
    }

    [
     MenuItem("Assets/Build AssetBundle From Selection - No dependency tracking")]
    static void ExportResourceNoTrack ()
    {
        // 保存ウィンドウのパネルを表示
        string path = EditorUtility.SaveFilePanel ("Save Resource", "", "New Resource", "unity3d");
        if (path.Length != 0) {
            // アクティブなセレクションに対してリソースファイルをビルド

            BuildPipeline.BuildAssetBundle (
                Selection.activeObject,
                Selection.objects, path);
        }
    }
}

■gist
AssetBundle用のエディター拡張

Assets/Build AssetBundle From Selection – No dependency trackingのほうはまだ使えていないので、複数OS分のコードは書いていません。

このクラスを、

Assets/Editor

に配置します。

適当なPrefabを作成し、Prefabを選択した状態で右クリックから、Build AssetBundle From Selection – Track dependenciesを実行します。
すると、

  • Asset.unity3d.android.unity3d
  • Asset.unity3d.iphone.unity3d
  • Asset.unity3d.unity3d

のように、WebPlayer用・android用・iphone用の3つができました。
これをDropboxに適当に配置し、URLをメモしておきます。

■参考リンク
アセットバンドルのビルド / Building AssetBundles

それでは、Unity側でAssetBundleを使ったコードを書いていきましょう。

using System;
using UnityEngine;
using System.Collections;

public class CachingLoadExample : MonoBehaviour
{
    void Start ()
    {
        // Clear Cache
        Caching.CleanCache();

#if   UNITY_ANDROID && !UNITY_EDITOR
        string url = "https://dl.dropboxusercontent.com/Asset.unity3d.android.unity3d
#elif UNITY_IPHONE  && !UNITY_EDITOR
        string url = "https://dl.dropboxusercontent.com/Asset.unity3d.iphone.unity3d
#else
        string url = "https://dl.dropboxusercontent.com/Asset.unity3d.unity3d?dl=1
#endif

        StartCoroutine (DownloadAndCache ("Particle System",
                                          url,
                                          1));
        StartCoroutine (DownloadAndCache ("Sprite",
                                          url,
                                          1));
    }

    public IEnumerator DownloadAndCache (string assetName, string url, int version = 1)
    {
        // キャッシュシステムの準備が完了するのを待ちます
        while (!Caching.ready)
            yield return null;

        // 同じバージョンが存在する場合はアセットバンドルをキャッシュからロードするか、またはダウンロードしてキャッシュに格納します。
        using (WWW www = WWW.LoadFromCacheOrDownload (url, version)) {
            yield return www;
            if (www.error != null) {
                throw new Exception ("WWWダウンロードにエラーがありました:" + www.error);
            }

            AssetBundle bundle = www.assetBundle;
            if (assetName == "")
                Instantiate (bundle.mainAsset);
            else
                Instantiate (bundle.Load (assetName));
            // メモリ節約のため圧縮されたアセットバンドルのコンテンツをアンロード
            bundle.Unload (false);

        } // memory is freed from the web stream (www.Dispose() gets called implicitly)

        Debug.Log(Caching.IsVersionCached(url, 1));
        Debug.Log("DownloadAndCache end");
    }
}

■gist
AssetBundleを使うサンプル

WWW.LoadFromCacheOrDownloadを使うのがミソっぽいです。

■参考リンク
アセットバンドルのダウンロード / Downloading AssetBundles

こちらの記事を読んでみると、WWW.LoadFromCacheOrDownloadを使うのがよさそうですね!

AssetBundleの読み込み方法
  • 1番オススメ:LoadFromCacheOrDownload
    • ダウンロードしたものが展開された状態でストレージに保存される
      • 実行時の読み込みの際に、展開用のメモリが必要ない
      • その分、ストレージの容量は圧迫する
  • 2番目にオススメ:CreateFromFile
    • 圧縮していないAssetBundleしか使えない
    • 圧縮できないのでストレージの容量は圧迫する
  • 非おすすめ:WWW、CreateFromMemory
    • 展開に使う分のメモリが必要
    • 圧縮したAssetBundleが使える → ストレージ容量は節約できる
  • 過去の遺物:Resources
    • AssetBundleと比べて効率が悪い。今から作るならAssetBundleを使ったほうが良い

via: Unite Japan 2013 の1日目に行ってきたメモ – 好き勝手に・げーあにん?

これで、Buildすると各OSでもAssetBundleが使えます。
でも、これでやっと使えるレベル、どう使う?単位は?という道はまだほど遠い。

StreamingAssetsに配置してロードしてみる

StreamingAssetsは簡単に言えば、ファイルをそのままアプリに持っていけるフォルダだ。そこに置いたものは何であれビルド時に余計なエンコードされること無くアプリ内に配置することができる。

via: テラシュールウェア [Unity3D] StreamingAssetsについて

いまいちまだStreamingAssetsをどうゆうときに使うのか分かっていないが、普通にFileアクセスはできずWWW経由でアクセスするようだ。
これはAssetBundleにアクセスする方法と同じだから特に理解には困らないが、StreamingAssetsに置くなら、普通にAssetsディレクトリの下にPrefabとかを置くでもよいのかなーと思っていたり。
ここは次第に実感が湧いてくるんだろう。

このディレクトリにアクセスするなら、Streaming AssetとPersistent DataPathの注意点 Android、iOS Unity – 万年素人からGeekへの道こちらの記事が参考になる。

では実際にファイルをロードするコードを書いてみよう。

// StreamingAssetsからオブジェクトを取得する場合
public IEnumerator LoadStreamingAssets(string assetName, string fileName, int version = 1) {
#if   UNITY_ANDROID && !UNITY_EDITOR
    string url = "jar:file://" + Application.dataPath + "!/assets/" + fileName;
    Debug.Log("AndroidUrl: " + url);
#elif UNITY_IPHONE  && !UNITY_EDITOR
    string url = "file://" + Application.dataPath + "/Raw/" + fileName;
#else
    string url = "file://" + Application.streamingAssetsPath + "/" + fileName; // Application.dataPath + "/StreamingAssets/" + fileName
#endif
    Debug.Log("AssetBundleUrl: " + url);

    // キャッシュシステムの準備が完了するのを待ちます
    while (!Caching.ready)
        yield return null;

    // 同じバージョンが存在する場合はアセットバンドルをキャッシュからロードするか、またはダウンロードしてキャッシュに格納します。
    using (WWW www = WWW.LoadFromCacheOrDownload (url, version)) {
        yield return www;
        if (www.error != null) {
            throw new Exception ("WWWダウンロードにエラーがありました:" + www.error);
        }

        AssetBundle bundle = www.assetBundle;
        if (assetName == "")
            Instantiate (bundle.mainAsset);
        else
            Instantiate (bundle.Load (assetName));
        // メモリ節約のため圧縮されたアセットバンドルのコンテンツをアンロード
        bundle.Unload (false);

    } // memory is freed from the web stream (www.Dispose() gets called implicitly)

    Debug.Log(Caching.IsVersionCached(url, 1));
    Debug.Log("DownloadAndCache end");
}

各パスの説明は、ストリーミングアセット / Streaming Assetsに記載されている。

あと、どうもversionを上げてもキャッシュを見に行ってしまうことがあるようで、その場合はurlの後ろにnow的なものを付けてアクセスするのが良さそうです。

System.DateTime.Now.ToString("yyyyMMddhhmmss")

こんな感じをQueryStringに含めるのがいいんですかね。

アセット依存関係の管理 / Managing asset dependencies

Unity – Unity Manual

複数のAssetBundleに同じオブジェクトやシェーダーが含まれてしまう場合は、まったく別でメモリ確保されてしまうため、
依存関係を管理することができるようです。

BuildPipeline.PushAssetDependencies();
BuildPipeline.PopAssetDependencies();

このメソッドを使うようですが、結構ハードコーディングが必要になりそうだなーという印象です。

keijiroさんのgithubリポジトリがすごく参考になります。
keijiro/unity-shader-bundle

参考リンク

「もののけ大戦“陣”」製作事例
Unity での asset bundle による追加コンテンツの扱い方
Asset bundleなどの、Unity3d基礎知識

こちらは間違いなく見たほうがよい。
[Unite Japan 2013]シーン/メモリ/アセットバンドル