『CARAVAN STORIES』(キャラスト)
のアセットバンドル事例
2018/5/9
清水史人
株式会社Aiming / ソフトウェアエンジニア
清水史人
株式会社Aiming / ソフトウェアエンジニア
@fumilin
Unity歴6年ほど。クライアント側が多い。
音ゲーが好き。
主なタイトル
・将棋ウォーズ(HEROZ株式会社)
・どうぶつしょうぎウォーズ(HEROZ株式会社)
・ポケモンコマスター(HEROZ株式会社)
・キャラバンストーリーズ(株式会社Aiming)
・ありすのステージ(同人)
突然ですが
アセットバンドル
好きですか?
僕は嫌いでした
過去に触った
タイトルで
Resources.Loadと
同じように
使えない
確認の時、アプリと
アセットバンドルの
更新が必要
非同期の
処理に直すの
辛いです
そんな思いも
ありましたが
今日は
これらの問題を
解決します少しだけ
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
CARAVAN STORIESについて
• 開発期間 2015/10/16~2017/11/28
• Unity 5.2.1f1~5.6.4p4
• iOS/Android版 2017/11/28リリース
• PC(Windows)版 2018/4/19リリース
プロジェクトについて
• 前身プロジェクトは幻塔戦記グリフォン
• UIは引き続きNGUIを使用
• エフェクトはSPARKGEAR
• Git管理 途中からLFSを使用 サウンドはSubversion
• アセットバンドルは元のシステムが古かったので更新する必要あり
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
アセットバンドル化の機運の高まり
• アプリサイズが2GBを超えてAndroidでビルドできなくなった
• βテストやリリースが近くなってきた
アセットバンドル化の機運の高まり
• アプリサイズが2GBを超えてAndroidでビルドできなくなった
• βテストやリリースが近くなってきた
アセットバンドル嫌い…だけど
やるかーやるしかないかー
アセットバンドル化の時期
開発時間軸
アセットバンドル化
開発の流れ
0 200 400 600 800
Prepro α β master assetbundle
2015/10/16 2016/2/26 2017/4/10 2017/8/15 2017/11/28
2017/7/10 2017/10/31
プロジェクトに含まれている主なアセット
数
キャラ 535キャラ/13155clip
BGM 145wav
SE 733wav
Voice 25307wav
ワールド 125種類
カットシーン 110シーン
UI 50パーツ/5500アイコン
アセットサイズの比率
キャラ サウンド
ワールド カットシーン UIパーツ
その他
アセットバンドル化に取り掛かる(1/2)
• 1ヶ月後のβテストに向けて、アプリサイズを減らしてビルドできるようにする
• 最終的には可能なものは全てアセットバンドル化する
- 前身プロジェクトのグリフォンでそうだったので運用的に同じにしたい
- 容量的にもする必要ありそうだった
• なるべく他の人の作業を止めない
- リリース直前なので止まると致命的
アセットバンドル化に取り掛かる(2/2)
• 1ヶ月後のβテストに向けて、アプリサイズを減らしてビルドできるようにする
• 最終的には可能なものは全てアセットバンドル化する
• なるべく他の人の作業を止めない
➡ これらを満たすために
• ちょっとずつアセットバンドル化
- βテスト時に完全にアセットバンドル化できるとは限らないため
• Resources以下のアセットのパスは、アセットバンドル化まで動かさない
Resources.Load()と同じように扱えればアセットバンドル化しやすそう
Unity2018.2から入るらしい
Addressable Asset Systemみたいなのが欲しい。
車輪の再発明いやだな…。
社内のプロジェクトの
アセットバンドル実装を探る
あった。
Unity TechnologyのAssetBundleManagerから
派生させた
リソース管理システム
特徴
• 単一のリソース名を使って、Resources/LoadAssetAtPath/AssetBundleの順番
で走査し、最初に見つかったリソースを使う
- 単一のリソース名なので、利用する他の開発者は、アセットバンドルかどうかを
気にしなくて良い
- 単一のリソース名にするために、LoadAssetAtPathでの読み込み先はResources
と同じ階層構造で配置する必要がある。
- キャラストではExternalResourcesというフォルダ以下に配置している。
• 一度読み込んだファイルはキャッシュされる。
- あらかじめ読み込んでおけばプリロードできる。
• アセットバンドル周りのめんどうなことはAssetBundleManagerを使う。
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
単一のリソース名から必要な文字列を生成する手順
• 構造を同じにすれば、アセットバンドル名以外は機械的に作成できる。
• 例として、単一のリソース名をUI/Icon/1/1_10.png
• LoadAssetAtPath対象フォルダ名をERとしている
必要な文字列 行う操作
Resources.Load UI/Icon/1/1_10 拡張子を取る
LoadAssetAtPath Assets/ER/UI/Icon/1/1_10.png Assets/ER/を足す
AssetBundleName ui/icon/1 変換用の正規表現を書く
AssetName Assets/ER/UI/Icon/1/1_10.png Assets/ER/を足す
正規表現を使ったアセットバンドル名の解決(1/3)
static readonly List<AssetBundleNameDefinition> Definitions = new List<AssetBundleNameDefinition>()

{

new AssetBundleNameDefinition("ui/", "icon_common", @"/UI/(?<DirectoryIdentifier>Icon)/1/1_d{2}.png$"),

new AssetBundleNameDefinition("ui/", "icon_common", @“/UI/(?<DirectoryIdentifier>Icon)/(11/11_001|13/13_1001).png$"),

new AssetBundleNameDefinition("ui/", @"/UI/(?<DirectoryIdentifier>Icon/17)/(?<Identifier>w+).png$"),

new AssetBundleNameDefinition("ui/", @"/UI/(?<DirectoryIdentifier>Icon)/(?<Identifier>d+).+.png$"),

}


class AssetBundleNameDefinition

{

readonly Regex _assetNameRegex;

readonly string _bundleNamePrefix;

readonly string _bundleNameSuffix;



public AssetBundleNameDefinition(string bundleNamePrefix, string assetNamePattern)

{

_bundleNamePrefix = bundleNamePrefix;

_bundleNameSuffix = "";

_assetNameRegex = new Regex(assetNamePattern);

}



public AssetBundleNameDefinition(string bundleNamePrefix, string bundleNameSuffix, string assetNamePattern)

{

_bundleNamePrefix = bundleNamePrefix;

_bundleNameSuffix = bundleNameSuffix;

_assetNameRegex = new Regex(assetNamePattern);

}
AssetBundlePath.cs
正規表現を使ったアセットバンドル名の解決(2/3)
public string GetBundleNameOrNull(string assetName)

{

var match = _assetNameRegex.Match(assetName);



if (!match.Success) return null;



var prefix = _bundleNamePrefix;

var suffix = _bundleNameSuffix;

{

var prefixId = match.Groups["DirectoryIdentifier"].Value;



if (prefixId != "")

{

prefix += prefixId + "/";

}

}

{

var prefixId = match.Groups["PrefixIdentifier"].Value;



if (prefixId != "")

{

prefix += prefixId;

}

}

var id = match.Groups["Identifier"].Value;

return (prefix + id + suffix).ToLower();

}

}
AssetBundlePath.cs
正規表現を使ったアセットバンドル名の解決(3/3)
static Dictionary<string, string> assetBundleNameCache = new Dictionary<string, string>();
public static string GetAssetBundleNameFromAssetName(string assetName)

{

string bundleName = null;



var hasCache = assetBundleNameCache.TryGetValue(assetName, out bundleName);



if (hasCache)

{

return bundleName;

}



foreach (var definition in Definitions)

{

bundleName = definition.GetBundleNameOrNull(assetName);



if (bundleName != null)

{

assetBundleNameCache.Add(assetName, bundleName);

return bundleName;

}

}



assetBundleNameCache.Add(assetName, bundleName);

return null;

}
AssetBundlePath.cs
正規表現を使ったアセットバンドル名の解決の補足
• アセットバンドルの名前の付け方(グルーピング)は、プロジェクトごとに個性が出
る箇所。参考として具体的な粒度については後ほど紹介します。
• キャラストではアセットバンドルを見やすくするために、PrefixIdentifierや
DirectoryIdentifierやSuffixを追加しました。
• 正規表現数は60個弱。毎回チェックするのは結構重かったので一度変換したものに
ついては、キャッシュを入れています。
• 上から順に判定しているため、後から追加した変換ルールによって、既存のファイ
ルの判定が崩れることがあります。アセットバンドル名の変換のテストを書くこと
を強くおすすめします。正規表現のデバッグも楽になります。
正規表現を使ったアセットバンドル名のテスト
void GetAssetBundleNameFromAssetNameTest(string answer, string[] relativePaths)

{

foreach (var relativePath in relativePaths)

{

var path = AssetBundlePath.GetRootPath() + relativePath; // “Assets/ER” が足される

var solve = AssetBundlePath.GetAssetBundleNameFromAssetName(path);

Assert.AreEqual(answer, solve);

}

}
[Test]

public void AssetBundleNameを取得_UI()

{
var relativePaths = new string[]
{

"UI/Icon/11/11_10.png",

"UI/Icon/11/11_101.png",

"UI/Icon/11/11_75001.png",

};

GetAssetBundleNameFromAssetNameTest(“ui/icon/11”, relativePaths);

var exclusionRelativePaths = new string[]

{

"UI/Icon/11/11_10.prefab",

"UI/Icon/11/11_101.mat",

};

GetAssetBundleNameFromAssetNameTest(null, exclusionRelativePaths);

}
AssetBundlePathTest.cs テストの様子
特徴
• 単一のリソース名を使って、Resources/LoadAssetAtPath/AssetBundleの順番
で走査し、最初に見つかったリソースを使う
- 単一のリソース名なので、利用する他の開発者は、アセットバンドルかどうかを
気にしなくて良い
- 単一のリソース名にするために、LoadAssetAtPathでの読み込み先はResources
と同じ階層構造で配置する必要がある。
- キャラストではExternalResourcesというフォルダ以下に配置している。
• 一度読み込んだファイルはキャッシュされる。
- あらかじめ読み込んでおけばプリロードできる。
• アセットバンドル周りのめんどうなことはAssetBundleManagerを使う。
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
読み込み時の走査
1. Resourcesから読み込む
2. 1で読み込めなかったら、
LoadAssetAtPathを使って読み込む
(エディタのみ)
3. 1や2で読み込めなかったら、アセッ
トバンドルから読み込む
読み込み順コード例(1/2)
internal static T LoadResourceFromAll<T>(string pathWithExtension) where T : UnityEngine.Object

{
var path = GetResourcesPath(pathWithExtension);

var resource = Resources.Load<T>(path);



if (resource != null) return resource;



#if UNITY_EDITOR



var exPath = GetExternalResourcesPath(pathWithExtension);

resource = AssetDatabase.LoadAssetAtPath<T>(exPath);

#endif



return resource;

}
ResourceStore.loader.cs
読み込み順コード例(2/2)
internal IEnumerator LoadResourceFromAllAsyncCoroutine<T>(string pathWithExtension, Action<T> onFinishCallBack = null) where
T : UnityEngine.Object

{

T resource = null;



resource = LoadResourceFromAll<T>(pathWithExtension);



if (resource != null)

{

onFinishCallBack(resource);

yield break;

}

var assetName = GetAssetName(pathWithExtension);

var assetBundleName = AssetBundlePath.GetAssetBundleNameFromAssetName(assetName);
var loadingCoroutine = AssetBundleManager.LoadAssetAsync(assetBundleName, assetName, typeof(T));



while (loadingCoroutine.MoveNext()) { yield return null; }

resource = loadingCoroutine.GetAsset<T>();

onFinishCallBack(resource);

}

ResourceStore.loader.cs
ここまでの実装により…
• 以下の4ステップでアセットバンドル化が可能
1. Resources.Loadの読み込み時に使っていたパスに拡張子を付ける
2. 付けたいアセットバンドル名へ変換する正規表現を記述
3. (必要であれば)非同期処理に変換
4. アセットバンドル化したいアセットを特定フォルダ以下に移す
• アセットバンドル化したアセットも、Resourcesに戻せばアセットバンドルの更新
無しで確認可能
• 事前に使用予定のアセットを読み込んでおけば(プリロード)同期読みも可能
キャラストのアセットバンドル化の流れ
1.全てのアセット読み込みに対して拡張子を追加し、リソース管理システムを置き換
えた
- この時点では同期読みの関数から読み込む
2.アセットバンドル化予定のアセットに対し、アセットバンドル名へ変換する正規表
現を追加
- アセットバンドルやアプリサイズが把握可能に
3. (必要であれば)非同期処理に変換
- 大変なので、同期読みできるアセットは同期読みしたい
4.アセットバンドル化したいアセットを特定フォルダ以下に移す
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
アセットバンドルからのアセット同期読み
internal static T LoadResourceFromAll<T>(string pathWithExtension) where T : UnityEngine.Object

{
var path = GetResourcesPath(pathWithExtension);

var resource = Resources.Load<T>(path);

if (resource != null) return resource;



#if UNITY_EDITOR

var exPath = GetExternalResourcesPath(pathWithExtension);

resource = AssetDatabase.LoadAssetAtPath<T>(exPath);

if (resource != null) return resource;

#endif

// ロードされているアセットバンドルから読み込む
var assetName = GetAssetName(pathWithExtension);

var assetBundleName = AssetBundlePath.GetAssetBundleNameFromAssetName(assetName);
var asset = AssetBundleManager.GetLoadedAssetBundle(assetBundleName);
if (asset == null) return null;

resource = asset.m_AssetBundle.LoadAsset<T>(assetName);


return resource;

}
ResourceStore.loader.cs
• テキストデータなどの小さいサイズのアセットは、1アセットバンドルにまとめ、あら
かじめアセットバンドルをロードしておくことにより、同期読みできるようにした。
アセットバンドルを常にロードしている箇所
• テキストファイル全般(同期読みがしたい)
• Cameraのアニメーション(同期読みがしたい)
• Shaderや密接に結びついてるテクスチャ/マテリアル(依存関係が多い)
• Font(依存関係が多い)
• UIのCommonパーツ(依存関係が多い)
• 背景エフェクト(依存関係が多い)
• ボタン音などのSE(レスポンス良くしたい)
• UIエフェクト(レスポンス良くしたい)
アセットバンドル化の結果
• 1ヶ月後のβテストに向けて、アプリサイズを減らしてビルドできるようにする
➡ ワールドとサウンドをアセットバンドル化し、800MB程となった。
• 最終的には可能なものは全てアセットバンドル化する
➡ 最終的にアプリサイズは45MBになった。
• なるべく他の人の作業を止めない
➡ 「アセットバンドル化したいアセットを特定フォルダ以下に移す」時以外は止めずに
済んだ。
➡ git上で大量の差分が出てしまい、ブランチ移動が困難になったり、ダウンロードの時
間やUnityのインポート時間で作業を止めてしまった。
➡ お昼休みや全体会議の間にダウンロード(fetch)を、夜間にUnityでインポートが出来
るように、予想ダウンロード時間を伝えるようにし、それに合わせて移行をした。
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
AssetBundleManagerの改修点(1/3)
• AssetBundleManagerの2015年頃の物をベースにしています
• 最新版では治っているかもしれません
• 読み込み中のAssetBundleをLoadAssetBundleしたときに参照カウントが壊れる
問題を修正
- @TODO: Do we need to consider the referenced count of WWWs? と書かれている箇所
- AssetBundleManager側にDictionary<アセットバンドル名, 参照数>の
ReferenceCountsを用意して、LoadAssetBundleInternalと
UnloadAssetBundleInternal内の処理を置き換えた
AssetBundleManagerの改修点(2/3)
• AssetBundleManifestのインスタンスを使いまわし
- Initializeを呼ぶ度に毎回GameObjectを生成してたのでSingletonにした
• 同時読み込み数に制限をつけた。
- 制限かけないとダウンロード失敗率が上がる
- 全プラットフォーム共通で4本。値は適当
- 後述のリトライ処理を入れる前はAndroidのみ1本にしていた
• ダウンロード失敗時にリトライ処理を入れるようにした
- わりと息を吐くように失敗する
- AndroidでRetry時にwwwをDisposeしないと連続して失敗することがあった
AssetBundleManagerの改修点(3/3)
• RemapVariantNameの最適化
- 元々のコードは全てのアセットバンドル名を取得して、総当たりで検索。
- AssetBundleManifestのダウンロード後に、Dictionary<アセットバンドル名,
List<Variant名>>の辞書を作成して、一発で取得できるように。
- Variantが少ない場合、O(N)からO(1)に
追加実装した機能(1/3)
• アセットバンドルビルド後に、各アセットバンドルのファイルのサイズを記録
- ダウンロードサイズ出すため
- 合わせて別ブランチとの差分も出して、変な差分がないかも確認
- ファイルサイズデータはアセットバンドル化せず、別途ダウンロードして使用
• アセットバンドルのUnloadを遅らせる
- Load済みのアセットバンドルからのアセットの読み込みは早い
- 同じアセットバンドルに連続してアクセスすることが多い
- キャラストでは、最後にLoadしてから2秒後にUnloadしている
- 同期読みの処理を入れている場合、処理落ちしてしまうこともあるので、適度に
間引く処理も入れたほうが良いかもしれない。
追加実装した機能(2/3)
• Editorで、指定のPlatformのアセットバンドルを使うオプション
- iOSやAndroidのアセットバンドルのエラーをEditorで確認する目的で作られた
- Shaderは互換性ないのでピンク。それでも使える。
- Windowsのアセットバンドルが焼けていないから、iOSに切り替えて最新のSound
の確認を…など、意外に使われる
• Editorで、遅延読み込みのエミュレーションオプション
- 非同期実装がされているかを、アセットバンドルビルド無しで確認できる機能
- 遅延の時間オプションは2フレームと0.6秒の2種類
- 2フレはロードのみ
- 0.6秒はダウンロード+ロードを想定
- 時間を長くしすぎると確認が終わらないのでほどほどの値を設定
これらのエディタ用機能は、メニューアイテムとして追加
追加実装した機能(3/3)
• 開発用のエディタシーンなど、ゲームサーバーに繋がない状態でアセットバンドル
を暗黙的に使えるようにする
- Editor上でアセットバンドル初期化前にアセットバンドルにアクセスがあったら
暗黙的に初期化を呼んでから使えるようにした。
- ゲームサーバーに繋いでいないので、アセットバンドルダウンロード先がわから
ない。夜に自動ビルドを走らせているので、そのビルド生成物を見に行くように
している。
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
UnityEditor上でのロード時間を軽減
• UnityEditor上で、バトル開始時のロード時間が規定時間(20秒)を超えて、タイム
アウトする事がある問題が発生していた。
• シミュレーションモード使わず、アセットバンドルから読み込むと回避できてい
た。
• 調べてみたら、ResourcesやLoadAssetAtPathからのアニメーションの読み込み
が遅かった。
• アニメーションのみアセットバンドルから読むオプションをつけて解決した。
• Asset Serialization(ForceText/Binary)や、アセットバンドルの粒度の影響があ
りそうなので、調べてみた。
UnityEditor上でのAnimationClipの読み込み時間
• アセットバンドルはまとまってたほうが早い
• Resources.LoadとLoadAssetAtPathは同じ
• ForceTextはとても遅く、ForceBinaryはとても早い
• AssetSerializationをMixedで上手く運用できれば最強かも。
31個のAnimationClipの読み込み時間
AssetBundle(LZMA)(1対N)
AssetBundle(LZMA)(1対1)
Resources.Load(ForceText)
LoadAssetAtPath(ForceText)
Resources.Load(ForceBinary)
LoadAssetAtPath(ForceBinary) 58ms
57ms
6,908ms
6,858ms
1,113ms
157ms
AnimationClipだけアセットバンドルから読み込むコード
internal static T LoadResourceFromAll<T>(string pathWithExtension) where T : UnityEngine.Object

{
var path = GetResourcesPath(pathWithExtension);

var resource = Resources.Load<T>(path);

if (resource != null) return resource;



#if UNITY_EDITOR
// 特定のパス以外ならLoadAssetAtPathで読み込む
if (EnableExternalResourcesLoadSpecific(pathWithExtension))

{

var exPath = GetExternalResourcesPath(pathWithExtension);

resource = AssetDatabase.LoadAssetAtPath<T>(exPath);

}

#endif
// ロードされているアセットバンドルから読み込む
var assetName = GetAssetName(pathWithExtension);
ResourceStore.loader.cs
• AnimationはExternalResourcesの処理を飛ばすようにして実装
• Resources内にAnimationClipが存在しない事前提
ローカルのプロジェクトサイズを削減できる
• キャラストのプロジェクトサイズは.gitや.svn含めて130GBほど。
• その内Soundのサイズは.svn込みで35GB。
• 容量圧迫するので、作業的に必要な人だけローカルに落とす運用に。
• そんな中、時々必要になったりするので音聞きたい!って声が。
• なので、シミュレーションモード時、ローカルからロードできなかった場合、アセットバ
ンドルから読み込むオプション(Fallback AssetBundle Loading)を用意して解決した。
• 全てFallbackすると予期しないエラーが出ることもあるので、サウンドのみのオプショ
ンも作成。
• ゆくゆくはキャラやワールドなどサイズの大きいアセット群もノーコストで逃がせるよう
になった。
アプリのビルド時間が早くなる
• Androidで3時間ほどかかっていたビルド時間が5分ほどで終わるように。プログラ
ムだけ変更した時の実機確認などが楽になりました。
• 早期にアセットバンドルを導入する理由の一つになると思います。
• アプリのビルドが早くなった分、アセットバンドルのビルド時間がかかるのです
が、普段では20分〜1時間程なので、トータルでも得しています。
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
アセットバンドルの粒度
• 粒度は大きく、4600アセットバンドル程にまとめた。
- 一般的な1アセットを1アセットバンドルに入れる運用にすると、キャラストでは
10万を超え、数に依存する処理(起動時間など)が遅くなったり管理が大変になる
ため。
- 依存関係が伸びて読み込みが遅くなるため。
• アセットの重複を許容
- 依存先がワールドなど、巨大なアセットバンドルをロードしたくない時
カットシーン(110シーン)
• 1カットシーン1アセットバンドル。
• 中身はカメラのアニメーション+表情アニメーション+特殊な背景小物
• ここは依存関係が激しいのでもっと上手くやりたかった。(特定のカットシーン専
用のサウンドやモーションはできれば入れたかったけど、特定する情報がなくて名
前付けできなかった)
キャラ(535キャラ/13155clip)
• Mesh/Texture/Material、全レアリティまとめて1キャラ1アセットバンドル
- レアリティで共通して参照しているパーツやテクスチャが存在する
- 分けるとアセットバンドル数が2倍弱になる
- 依存関係が増える
• アニメーションは意味レベルで分割
- フィールド
- バトル
- エモーション
- カットシーン(10個ひとまとまり)
サウンド
• BGM(145wav)
- 1BGMを1アセットバンドル
• SE(733wav)
- 基本は1ジャンル(攻撃音など)を1アセットバンドル
- 環境音やカットシーン用の長めのwavは10個1アセットバンドル
• Voice(25307wav)
- 基本は1キャラ1アセットバンドル
- カットシーン用は10個1アセットバンドル
- Mobは10キャラ1アセットバンドル
- Voice数が少ないため
ワールド(3D背景・125種類)
• 1つのフィールドの素材全てを、1つのア
セットバンドル
• 配置データは別アセットバンドル
• サイズは大きめのフィールドで30MBほど
• テクスチャやメッシュには名前をつけない
- カットシーンから参照させたい時に、依
存関係を切るため
UI(50パーツ/5500アイコン)
• 上手くまとめるのが難しい。そもそも流動的に変わるもので、アセットバンドルに
したくないもの筆頭
- 複雑なため、17個の正規表現で名前付け
• アイコンは1ジャンル(顔アイコンなど)1アセットバンドル。同じアイコンは同じタ
イミングで使われるでしょ、という位置づけ。依存関係が激しいもの(例えば
Prefabに最初から関連づいてるもの)は別途切り出してCommonIconという名前
で別アセットバンドルに。
• UIパーツは、全てのプレハブとアトラスをそれぞれまとめて1アセットバンドル
- パーツ間の参照があり、依存関係が激しかったため
• アニメーションも全てまとめて1アセットバンドル
• フォントも全てまとめて1アセットバンドル
大きくまとめた結果…
• 読み込みはアセットバンドル使ったから遅くなったとかはなかった。
• アップデート時の差分が大きくなったのがデメリット。
- ワールド(背景)は基本的に更新がないと踏んでいたが、実際には細かいテクス
チャの修正や追加パーツが増えたことがあった。
- 途中から粒度を変更したものもある。
- SEは全て小さめのファイルだと思いこんでいた中、環境音やカットシーン用の
SEのアセットが予想以上に大きいサイズだった時は、リリース後に粒度を細か
くした。
- アイコンの中にも、ガチャなどで使われる全画面バナー類が追加されていたこ
とがあり、後から1アセット1アセットバンドルに変更した。
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
AssetBundleNameの運用が難しい
• AssetBundleNameの名前付けが重い
- PostProcessorで、アセットの追加/移動時に自動的に名前をつけている。
- その度、metaが変わり再インポートが走り、アセットの読み込み分待たされていそう。
• AssetBundleNameが信頼できない
- 表示されているAssetBundleNameとファイルに保存されているAssetBundleNameが
異なることがある。
- Jenkinsなどでビルドする時に前のブランチの内容のAssetBundleNameのまま、
ReImportが走らず別の名前でビルドしてしまうこともある
- 見つけ次第、ReImportを行い解決している。
• AssetGraphToolを使うなど、AssetBundleNameを使わないアプローチで解決できる。
- キャラストでは、名前付けが複雑なため使うのを断念。
https://bitbucket.org/Unity-Technologies/assetbundlegraphtool
想定しているShaderに切り替わらない
• バトル中に半透明になるはずのキャラが半透明にならない
- Shader.Findで差し替えられていなかった
- Shader.Findはアセットバンドル内のシェーダーを指定できない
- Always Included ShadersにShader.FindしているShaderを含めて解決
• PC版でNGUIのスクロールビュー内のフォントが表示されない
- スクロールビューのShaderが壊れていた
- ビルドマシンでShaderをReImportしたら解決した
- 実はこれはアセットバンドルの問題ではない(けど類似例として掲載)
テクスチャが反映されない
• ビルドマシンの特定のプラットフォームの
workspaceで該当のテクスチャのインポートに失
敗していた
• ビルドマシンでUnityを起動して、該当のテクス
チャをReimportしてエディタ上では解決したが、
実機ではhash値が変わらず新しいアセットバンド
ルをダウンロードしなかった。
• TextureImporterのfilterModeの設定を-1から1
に変更してhash値を変更させて解決した。初期値
は-1で、明示的に指定すると1になる。実質無害。
最新のManifest/アセットバンドルがダウンロードできない
• Amazon S3からダウンロードしている時は常に最新をダウンロードできていた
が、IDCF Cloudコンテンツキャッシュ(CDN)からダウンロードする際に、キャッ
シュの削除(パージ)を行っても最新のファイルがダウンロードされないことがあっ
た。
• Manifest並びにアセットバンドルダウンロード時にQueryStringをつけて解決
• 開発環境では正しく動いていたのに…が起きやすい場所なので注意
// ManifestのダウンロードURL
var randomString = System.Guid.NewGuid();

var manifestUrl = url + "?" + randomString;



// アセットバンドルのダウンロードURL
var hash = m_AssetBundleManifest.GetAssetBundleHash(assetBundleName);

var assetBundleUrl = url + "?" + hash;
BGMが鳴らなくなる
• Resources.UnloadUnusedAssetsを叩いた時に、下記問題が起きていた。
- アセットバンドルの内容が破棄されることがある
- 再生中のサウンドが停止することがある
• 両方共Unity2017.3.0p2では発生せず、5.6.4p4付近特有の問題に見えたので力技
で対処。
- 読み込み時ではなく、BGM破棄時にアセットバンドルをアンロードする。大半は
これで解決したけど一部は直らず。
- Resources.UnloadUnusedAssetsを叩いた後、BGMを10フレームほど監視し、
再生が止まったら再生し直す。一瞬音が消えるデメリットはあるけど、鳴らない
よりはマシ。
Cacheが溜まる
• WWW.LoadFromCacheOrDownload(url, hash, crc)でダウンロードしている場合、同じ
urlで異なるhashのアセットバンドルをダウンロードしても、前のアセットバンドルは消
えない。
• Caching.maximumAvaliableDiskSpaceに予想ダウンロードサイズの1.8倍(LZMAから
LZ4の伸長1.5倍×一回の更新分1.2倍)を入れて、アセットバンドル更新時に最新のファイ
ルをCaching.MarkAsUsedで更新して解決する予定。
• Unity5.6.4p4/OSX Editor環境でCaching.expirationDelay(AssetBundleがキャッシュ
から自動で削除される有効期限)を設定してみた所、全てのアセットバンドルが有効期限を
超えていたら終了時と起動時に全部消える挙動だった。
- Unite2018のなんでも相談所で、5.6.5p1のソースを調べて頂いた所、そのような挙動
にならなさそうとのこと。バージョンの細かい差や、自分の検証が間違っているかもし
れないので、使う場合は個別に検証したほうが良さそうです。
Jenkinsでのアセットバンドルビルドが不安定
• ビルドに時間がかかる
- 差分なしのアセットバンドルビルドで、手元では5分ほどで終わる所、Jenkins(iMac Pro)で
は20分ほどかかる
- ブランチ移動直後では、平気で1〜2時間位かかる
- たまに謎のReImport祭りが起きて数時間かかる
- 一度停止して再ビルドするとReImportなしにすぐ終わる時も
- ローカルに 専用キャッシュサーバーを建てても 効果なく
• ビルドに失敗する
- dll更新時や、ブランチ移動時に良く発生
- batchmodeを使わなくした所、少し安定した(プラシーボかもしれない)
- 解決法1: ビルドマシン再起動
- 解決法2: Library内のScriptAssembliesフォルダを消す
- 解決法3: Library全削除
目次
• CARAVAN STORIESについて(1′00″)
• キャラストのアセットバンドル実装(2′00″)
- 単一のリソース名から必要な文字列を生成する(5′00″)
- アセットの読み込み順(7′00″)
- キャッシュやアセットバンドルからの同期読み込み(8′30″)
• AssetBundleManagerの改修・追加実装した機能(10′00″)
• アセットバンドル化の嬉しいこと(14′30″)
• アセットバンドルの粒度(17′00″)
• アセットバンドル事件集(21′00″)
• おわりに(25′00″)
おわりに
• アセットバンドル化の時期は、α版終了時など、アセットの構造がだいたい決まっ
たときくらいが後戻りが少なくて良いと思う。
• 単一のリソース名での読み込み共通化はやるべき。正規表現の解決の速度はそこま
で遅くないのでデメリットは少なく、アセットバンドル化するかしないかをアセッ
トの置き場所だけで解決できるのは開発上メリットが大きい。
• アセットバンドルの粒度や、必要な機能はプロジェクトの規模や方針によって異な
るので、キャラストの事例を見て、必要そうだと思ったものを参考にして頂ければ
幸いです。
Thank you!
おわりです。ご清聴ありがとうございました!

【Unite 2018 Tokyo】『CARAVAN STORIES』のアセットバンドル事例