AssetBundleを高速に作る


概要

Unity Advent Calendar 2014 15日目の記事です。

http://qiita.com/advent-calendar/2014/unity


Unityを使う上でめっちゃ困る事象である

「platformをswitchしたときクッソ重い」

を簡単に避ける方法を見つけたので、書く。


ほんとはAssetStoreに新Assetを入れた(申請中)のを記事にしたかったんだけど、

むしろこっちの方が良い話だろうという感じ。



解決したかったこと1

Unityを使って複数プラットフォームでの開発をしていて困ること、のなかで、

「プラットフォーム切り替え時」

の時間がかかることがかなり上位に来ると思う。



既知の有償な手段:fast platform switch を使う

http://jemast.com/unity/fast-platform-switch


有償、かつ有効。



オルタナティブな手段:拡張子を変える

で、もっと簡単に、手軽にできるんじゃねーの?っていうアイデアがこちら。


「ファイルの拡張子をUnityがどうしようもないものに変える」というもの。

実際に試してみたところ、マジで実現できた。

のだけど!!!


エラーが出る条件があって、

それを避けようとやってみればみるほど、fast platform switchの手法にメッチャ似てきて、


これを公開すると

ただ単に「fast platform switch」に喧嘩を売るだけになってしまう


ということがわかった。

つまり新規性が無い。

なんとかして新規性を獲得しようと思ったんだけど無理だった。


ので、別の問題を解決することにした。

現在 2014/12/14 23:05:36











解決したかったこと2

AssetBundleを高速に作る


箇条書きにすると、


1. あるプラットフォーム向けのAssetBundleを作るとき、エディタの環境設定がそのプラットフォームになってしまい、

AssetBundleにしたいファイル以外のファイルも、プラットフォームの変更に巻き込まれて再度importされてしまう。

これはメッチャ遅いし時間掛かってストレスフルなので、再度importされないようにしたい。

2. ファイルを移動するようなコードを書きたくない。

3. 複数のプラットフォームのAssetBundleを連続で高速に作成したい。


1が特にわっかりにくいと思うので具体的に説明する。



説明

以下のような構成で、AssetBundleにしたいファイルがProject内、Resourcesフォルダ下にあったとしよう。

スクリーンショット 2014-12-15 1.44.18.png

AssetBundleにしたい素材が入っているフォルダ BundlizeTargetの中身はこう。

スクリーンショット 2014-12-15 1.47.10.png

対して、AssetBundleにしたくない素材が入っているフォルダNeverReImportの中身がこう。

スクリーンショット 2014-12-15 1.48.44.png

ここで、BundlizeTargetフォルダの中身だけを、iOSとかAndroid用に、AssetBundleとして吐き出したいとする。

ad.png

AssetBundleを作るときの手順のおさらい

1. リソースを用意する

2. スクリプトで以下のようなのを書く

var res1 = Resources.Load(Path.Combine(targetFolderName, "Yeaaahhhhh"));

var targetPlatform = BuildTarget.iPhone;// Unity5だとiOSになったな~

uint crc;

if (res1 != null) {

BuildPipeline.BuildAssetBundle(

res1,

null,

Path.Combine(destinationPath, "Yeaaahhhhh_bundlized"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

targetPlatform

);

}

}


3. 実行する

4. 実行後には、エディタのプラットフォーム設定は、BuildPipeline.BuildAssetBundleメソッドの内容で使われたプラットフォームになっている。

つまりどこかでプラットフォームスイッチが発生している。



実際にプラットフォームが変更されるのは、3. の内部で、まさにBuildPipeline.BuildAssetBundleメソッドが実行された瞬間になっている。

ちなみに例えばAssetBundleの吐き出し先が無いとかでエラーになっても、プラットフォームはスイッチされる。



プラットフォームスイッチが発生すると

AssetBundleにしたいBundlizedフォルダ以外にも、Assetsより下の素材的なファイルはだいたい根こそぎ、再importされる。

(ソースコードとか.unityファイルとかは再度importされない。)

で、

1__#$!@%!#__スクリーンショット 2014-12-15 1.44.18.png

AssetBundleに変えたくないファイルも、再度importに巻き込まれる。

そして何より問題なことが一つ。


importは、遅い。

特にiOS、Android用のimportは、画像、サウンド、3Dモデルなどすべてにおいてクッソ遅いし重い。


余計なファイルをimportすることは辛い。

ということは、余計なファイルをimportしないようにすれば、高速化できるのでは?


っつーことでやってみた。



解決策

AssetBundleを作る瞬間に、

AssetBundleにしたくないファイル全部の拡張子を、

Unityが気にしない物に変更する。


プロジェクトはこちら。

https://github.com/sassembla/PlatformSwitcher

名前は気にしないでください。



プロジェクトを適当にDLしてきて、

メニュー > Window > Bundlize で、Bundlizeフォルダの中身を高速にAssetBundle化する。

iOS用、Android用の順に、計2つのAssetBundleを、PlatformSwitcher/Bundlized フォルダ内に吐き出す。

スクリーンショット 2014-12-15 2.27.57.png


その際、AssetBundle化が完了するまでの間、NeverReImportフォルダの中身は再度importされない。

つまり無駄なimportが無く高速。



手法解説

1. プラットフォームスイッチでimportされる可能性のあるファイルの拡張子をすべて + .deactivate 付きに変える(deactivate化)

2. 開始前のプラットフォームを記録しておく

3. AssetBundle化したいやつだけdeactivateを解く(activate化)

4. AssetBundleを好きなだけ作る

5. 3で保存しておいたプラットフォームに戻す(今回はダミーAssetBundleを作ることで無理矢理戻している)

6. 1で変えたファイルの拡張子を全部戻す

ね、簡単でしょう?



状況解説

実行部分だけだとこんな感じ。

using UnityEngine;

using UnityEditor;


using System;

using System.IO;

using System.Collections.Generic;


class AssetBundleContainer {

[MenuItem ("Window/Bundlize", false, 1)]

static void AssetBundlize () {

// Bundle化したあとのモノの置き場を適当に作る

var destinationPath = "Bundlized";

FileController.Renew(destinationPath);


/*

deactivate

対象が含まれる、すべての「importされると面倒なファイル」を、importされなそうな拡張子にリネームする。

*/

var deactivateTargetPath = Path.Combine(Application.dataPath, "Resources");

var transactionId = Deactivator.DeactivateFilesUnderPath(deactivateTargetPath);

var targetFolderName = "BundlizeTarget";

var targetFolderPath = Path.Combine(deactivateTargetPath, targetFolderName);


// 現在のプラットフォームを記録

var beforePlatform = EditorUserBuildSettings.activeBuildTarget;


// AssetBundleにする対象のファイルの拡張子だけを戻す(この場合フォルダ単位で戻している)

Deactivator.ActivateFilesUnderPath(transactionId, targetFolderPath);


// iOS用にAssetBundleを作成

if (true) {

var res = Resources.Load(Path.Combine(targetFolderName, "Yeaaahhhhh"));

var targetPlatform = BuildTarget.iPhone;// Unity5だとiOSになったな~

uint crc;

if (res != null) {

BuildPipeline.BuildAssetBundle(

res,

null,

Path.Combine(destinationPath, "Yeaaahhhhh_bundlized_ios"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

targetPlatform

);

}

}


var afterPlatform1 = EditorUserBuildSettings.activeBuildTarget;

Debug.Log("after1:" + afterPlatform1);



// Android用にAssetBundleを作成

if (true) {

var res = Resources.Load(Path.Combine(targetFolderName, "Yeaaahhhhh"));

var targetPlatform = BuildTarget.Android;// Unity5だとiOSになったな~

uint crc;

if (res != null) {

BuildPipeline.BuildAssetBundle(

res,

null,

Path.Combine(destinationPath, "Yeaaahhhhh_bundlized_android"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

targetPlatform

);

}

}


var afterPlatform2 = EditorUserBuildSettings.activeBuildTarget;

Debug.Log("after2:" + afterPlatform2);


// リセット

if (true) {

uint crc;

var resetterRes = Resources.Load("dummyText");

if (resetterRes != null) {


BuildPipeline.BuildAssetBundle(

resetterRes,

null,

Path.Combine(destinationPath, "resetter"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

beforePlatform

);

}

}


var afterPlatform3 = EditorUserBuildSettings.activeBuildTarget;

Debug.Log("after3:" + afterPlatform3);


// 無効化していたのを元に戻す

Deactivator.RollbackTransaction(transactionId);

}

}


class Importer : UnityEditor.AssetPostprocessor {

public void OnPreprocessTexture () {

Debug.LogError("assetPath:" + assetPath);

}

}



プラットフォームスイッチが発生すると、再importが発生する。

再importが発生すると、OnPreprocessTextureメソッドが呼ばれ、

Debug.LogErrorが走ってあぶなーーーいっ! って言ってくれるようになっている。



で、先ほどのフォルダ構成に対して、

2__#$!@%!#__スクリーンショット 2014-12-15 1.44.18.png


1. プラットフォームスイッチでimportされる可能性のあるファイルの拡張子をすべて + .deactivate 付きに変える(deactivate化)

/*

deactivate

対象が含まれる、すべての「importされると面倒なファイル」を、importされなそうな拡張子にリネームする。

*/

var deactivateTargetPath = Path.Combine(Application.dataPath, "Resources");

var transactionId = Deactivator.DeactivateFilesUnderPath(deactivateTargetPath);



2. 開始前のプラットフォームを記録しておく

// 現在のプラットフォームを記録

var beforePlatform = EditorUserBuildSettings.activeBuildTarget;



3. AssetBundle化したいやつだけdeactivateを解く(activate化)

// AssetBundleにする対象のファイルの拡張子だけを戻す(この場合フォルダ単位で戻している)

Deactivator.ActivateFilesUnderPath(transactionId, targetFolderPath);


4. AssetBundleを好きなだけ作る

// iOS用にAssetBundleを作成

if (true) {

var res = Resources.Load(Path.Combine(targetFolderName, "Yeaaahhhhh"));

var targetPlatform = BuildTarget.iPhone;// Unity5だとiOSになったな~

uint crc;

if (res != null) {

BuildPipeline.BuildAssetBundle(

res,

null,

Path.Combine(destinationPath, "Yeaaahhhhh_bundlized_ios"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

targetPlatform

);

}

}


var afterPlatform1 = EditorUserBuildSettings.activeBuildTarget;

Debug.Log("after1:" + afterPlatform1);



// Android用にAssetBundleを作成

if (true) {

var res = Resources.Load(Path.Combine(targetFolderName, "Yeaaahhhhh"));

var targetPlatform = BuildTarget.Android;

uint crc;

if (res != null) {

BuildPipeline.BuildAssetBundle(

res,

null,

Path.Combine(destinationPath, "Yeaaahhhhh_bundlized_android"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

targetPlatform

);

}

}


var afterPlatform2 = EditorUserBuildSettings.activeBuildTarget;

Debug.Log("after2:" + afterPlatform2);


ここで、Debug.Logの内容は計2つ出る。(Pro版持ってないとそれはそれでエラー出て怒られる)


5. 3で保存しておいたプラットフォームに戻す(今回はダミーAssetBundleを作ることで無理矢理戻している)

// リセット

if (true) {

uint crc;

var resetterRes = Resources.Load("dummyText");

if (resetterRes != null) {


BuildPipeline.BuildAssetBundle(

resetterRes,

null,

Path.Combine(destinationPath, "resetter"),

out crc,

BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets,

beforePlatform

);

}

}


var afterPlatform3 = EditorUserBuildSettings.activeBuildTarget;

Debug.Log("after3:" + afterPlatform3);

実際、プラットフォームの変更は,

EditorUserBuildSettings.SwitchActiveBuildTarget メソッドでも変更可能なんだけど、


ここでなんでAssetBundle作る方法を使っているかというと、

バッチから実行したいから。

EditorUserBuildSettings.SwitchActiveBuildTarget メソッドはバッチ上では使えない。



6. 1で変えたファイルの拡張子を全部戻す

// 無効化していたのを元に戻す

Deactivator.RollbackTransaction(transactionId);



この順で動作すると、importされる内容は、開始時のプラットフォームがPCプラットフォームだった場合、

・iOSプラットフォームに一度変更、BundlizeTarget フォルダ内の画像 Yeaaahhhhh.jpeg だけが変換される

・その後Androidプラットフォームに変更、BundlizeTarget フォルダ内の画像 Yeaaahhhhh.jpeg だけが変換される

・最後にPCプラットフォームに戻り、BundlizeTarget フォルダ内の画像 Yeaaahhhhh.jpeg だけが変換される

・無効化されていた画像が復帰

という感じになる。


完璧、、、!! かに思えたのだが、


実は最後にぶり返しみたいなのがあって、

無効化されていた別フォルダのものが復帰後、再度Unity Editor.appにフォーカスをあてるとかすると、PCプラットフォームとして変換される。

(PCプラットフォームだと高速っちゃあ高速なので気にはならないが。)


プラットフォームスイッチに巻き込まれる分の時間は確実に減っている。


で、実際どのくらい速いの?

コマンドラインから実行して時間を計測してみた。


deactivateあり:

ts:00:00:01.9974540 (1.9秒



deactivate無し:

ts:00:00:03.9435570 (3.9秒



だいたい2倍なんだけど、、地味だな、、、ファイル数あたり2秒かかってるだけ。



試しに邪魔なファイルを増やして実行

呪われたみたいな絵面のNeverReImportフォルダ

スクリーンショット 2014-12-15 2.58.47.png

deactivateあり:

ts:00:00:02.8803690 (2.8秒



deactivate無し:

ts:00:00:50.5550690 (50.5秒



速い。(確信)

deactivateありの0.8秒くらいの増加は何だろう。あとで調べよう。



気づき

、、、、


書いてて気づいたんだけど、別にAssetBundleにするやつも、Resource.Loadとかで読み込んだあとだったら、

deactivateして良かったのでは。


そしたらその分のimportも無くて、もっと速かったな???



おまけ小ネタ

Editor用のScriptを書いていて、「現在の設定」を表示したいときがある。

スクリーンショット 2014-12-14 22.39.34.png

接続状態を表したかったり、

モードを表したかったり。


で、このような見た目になるインターフェースをつくってしまったことがあった。

connect と disconnectが排他でconnect状態だからdisconnectが押せる、とか

現状autoConnectがonだからoffが押せる、とか。


メニューからのプルダウンで、状態を表示したいだけなんだけど、こう、なんだ、2つ持たなきゃ行けないという。

コードの量も多い。


これが、例えばだけど、こう書くことで単純に解消できた。


#if UNITY_IPHONE

[MenuItem ("Window/changePlatform/Now: iOS Platform", false, 1)]


#elif UNITY_ANDROID

[MenuItem ("Window/changePlatform/Now: Android Platform", false, 1)]


#elif UNITY_STANDALONE_OSX

[MenuItem ("Window/changePlatform/Now: OSX Platform", false, 1)]


#endif

public static void CurrentPlatform () {}// こいつは実際どうでもいい、ただメニューでぶったたかれる関数が必要。気に喰わなければdisableにすればより良い。


compiler directiveで、メニューに出てくる項目を弄ると、

directiveに何かをセットするようなコードを状態セットのあとにいれるだけで、

メニューに表示される文言 = 状態表示を弄れる。


たとえば上記のコードだと、現在のプラットフォームがPCなら、プルダウンメニューに表示される項目はこんな感じになる。

スクリーンショット 2014-12-14 22.44.53.png

メニューで状態が表示できた。

この例だとUnity Editor自体の上部バーをみればまあ分かるじゃんって感じなんだけど、

まあ例として。

ちなみにこの方法には欠点があって、

状態が変わる→コンパイルが発生する、という段取りをこなさないと、compiler directiveが変化せずに表示が変わらない。


書き終わった。

2014/12/15 3:54:34