最近UnityでAssetBundleの実装をする必要があって、いろいろ調べてからやってみました。
[Unite Japan 2013]シーン/メモリ/アセットバンドル
を見て勉強したりとか、
[UnityのAssetBundleをじっくり理解する手順をまとめてみた。【アセットバンドル】 - NAVER まとめ] (http://matome.naver.jp/odai/2139114084705385001)
を見て勉強したりとかしました。
ということで、AssetBundleを実装していて気付いたことをごちゃごちゃになっちゃいますが、まとめていきます。
やりたかったこと:
・Assets/Resources/以下を全部AssetBundleにする。(画像や音楽ファイルなどが含まれている。)
・サーバー上に上がっているAssetBundleをダウンロードして、そこから特定の画像ファイルのみを取り出す。
・それをUnityEngine.UI.ImageのSpriteに適用する。
環境
Unity4.7Pro(普通にUnity5でも同じことできると思います。)
PlatformはiOSを想定しています。
Assets/Resources/以下を全部AssetBundleにする。
画像や音楽ファイルなどが含まれているResources以下を全部ファイルにします。
しかし困ったことに最初にやった方法だとResources以下のフォルダ構造がとれないという問題点がありました。
最初にやった方法:
http://docs.unity3d.com/ja/current/Manual/BuildingAssetBundles.html
一般的な方法ですね。この中で使っているBuildAssetBundleという関数ですが、
http://docs.unity3d.com/ja/current/ScriptReference/BuildPipeline.BuildAssetBundle.html
引数に
・mainAsset(簡単に取得したいオブジェクト)
・assets(ファイルの実体、この場合なら画像とか音楽ファイルとか)
・pathName(圧縮されたbundleの保存先)
・assetBundleOptions(保存する際に選択したアセットだけではなく、それに関係するすべてのアセットを含める設定をすることができる)
・targetPlatform(iPhoneとかAndroidとかPS4とか。そのアセットバンドルを使うプラットフォームによって変更してください。)
・crc(バージョンです。ロードするときに関係がありますが、現在キャッシュされているAssetBundleのバージョンと比べてこの値が違ったときは、再度ダウンロードするという処理になります。)
を指定することができます。
しかしこれだと、実際にロードするときはファイル名を直接指定することになり、AssetBundle内でフォルダ構造が維持できません。
Resourcesフォルダでは、階層構造にしてもLoad(“Texture/hogehoge”)メソッドで階層指定することができましたが、それができないということです。
これの何がやばいかというと、同一ファイル名で違う内容のファイルが複数あったときにどちらがロードされるかわからないということになります。
というわけでこれを解決したのが以下の内容です。
http://docs.unity3d.com/ja/current/ScriptReference/BuildPipeline.BuildAssetBundleExplicitAssetNames.html
これは先ほどのBuildingAssetBundlesと似ていますが、最大の違いは
「任意の識別子を付加することができる」
という点です。これにより、先ほどの例ではファイル名を直接指定しなければロードができませんでしたが、これを使うとその識別子を指定してロードをすることができます。
この識別子はユーザが任意で決められるため、これをファイルのパスに指定することによってフォルダ構造を維持することができるということになります。
その工夫をしたのが以下のブログになりまして、大変参考にさせていただきました。
(参考:【Unity】Androidアプリ開発 備忘録⑱ AssetBundleでフォルダ構造を維持(C#) )
これをBuildTargetをiPhoneにすることによって、今回私がやりたいことを達成します。
私の場合iOS向けアプリ開発のためUnity上のTargetPlatformは iOSになっています。なのでBuildTargetをiPhoneにしないとエラーが出てしまいますので注意。
(BuildTargetをAndroidにしてAssetBundleを作る→それをiOSで使おうとしてもエラーがでるということ。)
ロードをする。
5つ方法があって、
①WWW.LoadFromCacheOrDownload
②WWW(http://~ )
③WWW(file://~ )
④AssetBundle.CreateFromFile
⑤AssetBundle.CreateFromMemory
がありますが、①が強く推奨されているのでそれを使います。
キャッシュにデータが乗っていればそれをロードする。乗っていなければダウンロードして同時に無圧縮にしてロードしてくれる。という処理を行ってくれるやつですね。
他はメモリをめちゃくちゃ食うとか無圧縮AssetBundleしか扱えないとかなので、今回はスルーで。
おまけ的な話なのですが、Resources.Loadは古いAPIだし、無圧縮AssetBundle使ってどうのこうのしていく方が良いと動画断言されていたので(将来性の観点で)AssetBundleのやり方覚えておくと今後役立つと思います。
では本編に戻ります。
ロードには以下のページを参考にしました。
(参考:http://tech.hisasann.com/cs/183/)
AssetBundleをdelegateで使いやすくする
具体的にはbundleの作成とそこから中身をロードして使う部分を分けています。
・BundleDownloader.cs(bundleの作成)
・AssetLoader.cs
でやっています。
ではまずコードの方をお見せします。
using UnityEngine;
using System;
using System.Collections;
using System.Linq;
public class BundleDownloader : MonoBehaviour {
public string BundleURL = "https://dl.dropboxusercontent.com/u/53181511/hoge.unity3d";
public delegate void onCompleteLoad(AssetBundle bundle);
public int version;
public IEnumerator DownloadAndCache(onCompleteLoad callback)
{
while (!Caching.ready)
yield return null;
using (WWW www = WWW.LoadFromCacheOrDownload(BundleURL+"?time=" + DateTime.Now.Second, version))
{
while (!www.isDone) { // ダウンロード中の処理はここに書く
Debug.Log(Mathf.CeilToInt(www.progress*100));
yield return null;
}
if (www.error != null){
throw new Exception("WWW download had an error:" + www.error);
}
var bundle = www.assetBundle;
callback(bundle);
// Unload the AssetBundles compressed contents to conserve memory
bundle.Unload(false);
} // memory is freed from the web stream (www.Dispose() gets called implicitly)
}
}
using UnityEngine;
using System.Collections;
using System.IO;
using System.Linq;
using System.Collections.Generic;
public class AssetBundleLoaderImproved : MonoBehaviour {
public UnityEngine.UI.Image image;
private BundleDownloader bundleDownloader;
// Use this for initialization
void Start () {
bundleDownloader = new BundleDownloader ();
}
void Update(){
//スペースキーを押したらCallBackでimageに画像がロードされる。
if (Input.GetKeyDown(KeyCode.Space))
{
StartCoroutine(bundleDownloader.DownloadAndCache(ImageChangeCallback));
}
}
public void ImageChangeCallback(AssetBundle bundle){
//もともとResources/Texture/texture1.pngだったものを指定する。
var filePathInBundle = "Texture/texture1";
var tex = (Texture2D)bundle.Load(filePathInBundle);
image.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
}
}
やっていることですが、スペースキーを押すと、画像がロードされるだけというシンプルなものになっています。
具体的にいうと
・スペースキーを押すと、StartCoroutineによってBundleDownloaderクラスのDownloadAndCacheメソッドが開始される。
(その際にコールバックとして関数を引数にとる。)
・キャッシュ上にAssetBundleがのっていればそれをロードする。
(キャッシュ上になければダウンロードして、それをキャッシュ上に展開する。)
・キャッシュ上に展開されたAssetBundleにアクセスするbundleを作成する。
・それをコールバックの関数で処理をする。
・コールバックの関数にはテクスチャをスプライトにしてUnityEngine.UI.Imageの画像として適用する処理が書かれているためそれを処理する。
ということをしています。
キャッシュにAssetBundleがのっかってて、それを元にbundleができた時点でやりたい処理を行うということですね。行いたい処理を書いた関数を引数にとれるので、汎用性のある感じになったかなと思います。
おまけ1
ちなみにAssetBundleを作る際
Resources以下のファイルサイズは合計で10.1MB
BuildTarget.AndroidにしてAssetBundleを作ったら5.1MB
BuildTarget.iPhoneにしてAssetBundleを作ったら7.5MBになりました。
おまけ2
bundle.LoadAll().ToList().ForEach(v=>Debug.Log(v.GetType()));
でファイルによってロードされた際にどういう型になるのか調べてみました。
という感じでした。
なので、今回は
var tex = (Texture2D)bundle.Load(filePathInBundle);
でテクスチャをロードしていましたが、ものによってはAudioClipやGameObjectにキャストして用いると良いと思います。