【Unity】AssetBundleからMultiple Spriteに含まれるスプライトを取得する方法とその周辺

今回はAssetBundleの中に格納したMultiple Spriteから特定のスプライトを取得する方法と、その挙動についてです。
Multiple Spriteの取得方法について
AssetBundleからSpriteを取得したい場合、それが単品なら単純にAssetBundle.LoadAsset<Sprite>で取得する事が出来ますが、対象がMultiple Sprite、つまりスプライトを分割した内容ならばそうはいきません。

AssetBundle.LoadAllAssetsでは駄目
ここでResources.LoadAllと似たようにAssetBundle.LoadAllAssetsと行きたい処ですが、この方法ではAssetBundleに格納されている全てのアセットをロードする事になり、余計なメモリと解放分のCPUが必要となります。
下の画像は、Sprite一つを呼ぶはずがAssetBundleに格納されている全てのテクスチャ(RobotBoy関連)が読まれてしまっている状態です。

AssetBundle.LoadAssetWithSubAssets
なのでAssetBundle.LoadAssetWithSubAssetsを使用します。
こちらであれば、AssetBundleから指定のテクスチャから生成したスプライトしか呼ぶことは無いので、上のように無駄なテクスチャが読まれる事はありません。

不要なスプライトの破棄
しかし、まだSpriteが無駄にいっぱい生成されています。これは、ロードしたMultipleSpriteが「複数のスプライトを含む1つのアセット」である事に由来してると思います。
これ一つ一つは大した問題では無いのですが、使わないのであれば気持ち悪いので使うものだけ残します。
Resource.Unloadで使っていなければ一つ一つ潰しても良いのですが、Resources.UnloadUnusedAssetsで一気に潰してしまいます。

これで、ひとまずはSpriteを単体ロードする事が出来ます。
下のコードは、StreamingAssetsに格納したplayerアセットバンドルの、RobotBoyJumpSpriteテクスチャが持っているRobotBoyJump00スプライトを取得する例です。
特定のスプライト参照からスプライトを取得する
これとは別に、Spriteを単体で取得する方法もあるにはあります。要するに特定のSpriteへの参照を元にスプライトを取得するアプローチです。
これ以外でも、とにかく「他のComponentやScriptableObject、Animation等から参照されている」場合、該当のスプライトのみを抽出することが出来ます。

この方法で取得した場合、AssetBundle.LoadAssetWithSubAssetsと比較してスプライトが生成しませんしロード後の検索も不要です。
ただし実装にひと手間付きます。
必要な手間は二つ、
- Multiple Spriteの持つスプライトをSpriteScriptableObject に格納する
- SpriteではなくSpriteScriptableObject をAssetBundleに格納する
- SpriteScriptableObjectをロードしスプライトを抽出する(LoadAB)


これでスプライトをロードした場合、スプライトちゃんと単品で呼ばれます。Resources.Unloadで消してやれば一緒に呼ばれるScriptableObjectもちゃんと破棄されます。

ちなみに、こうして指定スプライトを抜きだした場合でもテクスチャは1枚分まるっと読まれます。そうしないと、「テクスチャをロード後、スプライトが表示する範囲のテクスチャを生成し、それを元にSpriteを生成し…」みたいな事になります。
DynamicFontが似たような事やってますが、Spriteではやってないのでやはり負荷が…圧縮したTexture使えなくなりますし。
SpritePackerでAtlas化したMultiple Spriteと挙動
Resourcesと異なり、AssetBundleはSpritePackerでパックしたスプライトもロードすることが可能です。
その際、AssetBundle.LoadAssetWithSubAssetsから呼び出した時のスプライトおよびテクスチャは、「テクスチャはパック済みの内容」「スプライトはテクスチャ単位」で取得されます。

どういった状況かと言えば、こんな感じです。
まずはSpriteをSpritePackerでパックします。元々は2048x2048のスプライト1枚なのですが、パックする事で無駄な領域が消え1024x512となります。名前はSpriteAtlasTexture-Packer云々。


で、これをロードすると、スプライトの元となるテクスチャはAssetBundle1枚ですが、呼び出し生成するSpriteはMultipleSpriteに依存するので、スプライトが一杯作られた…そんな感じだと思います。
このため、AssetBundleからスプライトを取得する場合、Multiple Spriteからスプライトを生成するよりScriptableObject等から特定のスプライトを参照するなり、単体のスプライトをSpritePackerでまとめた方が良いかもしれません…が、PrefabやAnimationからSpriteを参照する場合は普通に読めるので、これは「Spriteをランタイムに取得して差し込む」ようなケース以外ではそれほど重要ではないかもしれません。
関連
【Unity】タグやレイヤー表示を階層構造にする

先日の#UnityTipsで「タグやレイヤーの表示を階層構造にする」といった面白いTipsが紹介されていました。
It's #unitytips Tuesday: Create submenus in the tags & layers dropdown by using a slash in the name! @unity3d pic.twitter.com/wBYT3bxYOC
— Ben Pitt (@robotduck) 2015, 11月 10
やり方は簡単で、タグの間に「/(スラッシュ)」を挟むと、親と子に分かれてグループ可する…といったものです。
これはあくまでも「表示」が変わるだけで実際に効果はほぼ無いのですが、一応グループ化して検索する的なアプローチも同時に紹介されています。
PS You can use this to find within a menu group https://t.co/S5XLheYto1 @LouardOnGames @leizzer @kurai @unity3d pic.twitter.com/HHZwuASNAu
— Ben Pitt (@robotduck) 2015, 11月 10
この機能ですが、Unityのエディタで結構使われています。
代表的なものは、MenuItemでしょうか。メニューの階層化においてスラッシュで分割するといった使われ方はよくされます。

またコンテキストメニューだけでなく、AssetBundleのラベル等でもスラッシュで分割することが出来ます。

ちなみに階層化されてないレイヤーやAssetBundleラベルを階層化すると、大抵変な動作するので注意です。
またラベル・レイアウト・GameViewのアス比等は階層化出来ませんでした。
【Unity】シーンの「追加読込」と、追加読込したシーンの「破棄」
Unity 5.2よりLoadLevelAdtiveでロードしたシーンのアンロード(破棄)が可能になりました。

今までは「LoadLevelAditiveやResourcesからオブジェクトを取得」し、「SceneManagerに親オブジェクトを登録」「不 要になったら親オブジェクトごと消す」のような操作が必要でしたが、これが無くなり割と楽に追加・削除が可能になりました。
使い方(Unity 5.2)
呼び出し元となるScene1、ボタンを押すと飛び出すUIが記述されているScene2を用意します。追加でロードされるシーン2にはCameraやEventSystemは不要です。


後はシーンのロード・アンロードです。
シーンのロードは今まで通りApplication.LoadLevelAdditive ("シーン名");で行います。逆にアンロードしたい場合はApplication.UnloadLevel ("シーン名");を使用します。
なおリソースのアンロードは行ってくれないので、Resources.UnloadUnusedAssetsで解放してやる必要があります。
また「破棄できるシーンは一つのみ」なので、LoadLevelAditiveを連打した後にUnloadをかけても、全てのシーンが破棄されません。
今回のScene2のロード・アンロードの場合、以下のようなコードで行います。
private bool isLoaded = false;
public void OnClick()
{
isLoaded = !isLoaded;
if (isLoaded) {
Application.LoadLevelAdditive ("Scene2");
} else {
Application.UnloadLevel ("Scene2");
Resources.UnloadUnusedAssets();
}
}
Resourcesとの比較
このアプローチの比較として上がるのは、「ResourcesにUI丸ごとプレハブとして配置し、実行時にインスタンス化する」方法だと思います。
この方法と比較すると、Sceneのアンロードは「プレハブとして使える範囲」において少し有利です。

というのも、プレハブは現在「ネスト化が出来ない」仕様があります。何が問題かと言えば、例えば「UIを丸ごとプレハブに」した場合、子プレハブも親プレハブの一部としてリンクが切れてしまうため、UIに使用しているボタンやテキストはプレハブとして登録出来なくなります。UI要素を後で一覧変更したいケースや、多人数でプロジェクト開発を進めた場合コンフリクトが起こりやすく、また起こした時にマージが若干面倒くさい事になる問題が上がります。
UnityYamlMergeがあるので多少は楽ですが…
あとはstatic batchingが効くオブジェクト・Navmesh等が含まれるステージ加算、ライトマップ等が可能な点が差別化になってるかなと思います。
逆にリソースが即読み終わる点がResourcesが優れてます。シーンの読み込みは早くてもフレームの終わりに完了だったと思いますし。
マルチシーンエディティング
これが本当に実用的になるのはUnity 5.3(現在ベータ中)からかなと思います。
Unity 5.3にはマルチシーンエディティングの機能が追加されており、こういった形でLoadLevelAditiveを行うと、下の画像のように各シーン毎にオブジェクトを並べてくれます。
また編集も複数のシーンをまたいで行う・複数のシーンをロードした状態でゲームを再生する等も可能になるので、かなりシーンの加算減算がやりやすくなるかなって気がします。

関連
【Unity】AssetStoreからインポートしたアセットをプロジェクトからアンインストールする「 Package Uninstaller」
PackageUninstallerはインポートしたunitypackageをアンインストールする事が出来るエディタ拡張です。
例えば色々とプロジェクトにUnitypackageを突っ込んだ状態で、特定のパッケージを消したい…といった時に使えます。
使い方
メニュー>Assets>Uninstall Packageを選択して、ウィンドウを開きます。
一覧にインポートされているパッケージ一覧が表示されるので、消したいパッケージの隣にある
ボタンを押します。

押すと「You're going to uninstall the ***. Are you sure you want to delete all the files related to the package?(パッケージに関連するファイル消していい?)」「the operation can not be undone! Are you sure?(この操作は巻き戻せないけど、本当に良いの?)」と聞かれるので、両方「Yes」と回答すると、パッケージがアンインストールされます。


ちなみに一覧にないアセットでも、こっちで指定してやるとアンインストールする事が出来ます。

なお、このエディタ拡張は「パッケージを削除する」機能を持ちますが、それ以上…例えば「使用しているパッケージは削除しない」等は行いません。
つまり該当のアセットを参照していた場合、スクリプトがエラーになったりシェーダーがピンクになったり、missingになったりします。
もし使用していないアセットを消す目的であれば、こっちの方が良いかもしれません。
あとアセットの把握にファイルのID(uuid)を使用しているみたいなので、ファイルのIDが変わると消すことが出来なくなります。ファイルのIDが変わる条件等についてはこちら。
感想
昔似たようなアセットを試作しましたが、似たようなアセットあったんですね。やはり探してみる物です。
Unitypackage uninstaller #unity pic.twitter.com/JlQOSawZuE
— 椿 (@tsubaki_t1) 2015, 7月 16
