【Unity】AssetBundleを使ってリソースを管理する

Unity
マイケル
マイケル
みなさんこんばんは!
マイケルです!!
エレキベア
エレキベア
おひさクマ〜〜〜〜
マイケル
マイケル
今日は久しぶりのUnity記事!
AssetBundle を使ってみるぞ!
エレキベア
エレキベア
AssetBundleって何クマ?
マイケル
マイケル
説明しよう!
AssetBundleとは!!
AssetBundleとは
  • Unityで読み込める形にアセットを事前にビルドしておくことで
    アプリ外部からアセットをロードできる仕組みのこと!
エレキベア
エレキベア
アプリ本体とアセットを分けることができるということクマ?
マイケル
マイケル
その通り!
アプリを直接更新せずにアセット更新の運用をすることができるんだ!
ScreenShot 2021 04 19 11 17 55
↑AssetBundleファイルの作成処理
ScreenShot 2021 04 19 11 18 03
↑AssetBundleファイルのロード処理
エレキベア
エレキベア
これは使うのが楽しみクマ〜〜〜
スポンサーリンク

参考書籍

マイケル
マイケル
AssetBundleの使い方を勉強するにあたり、Unity公式マニュアルの他に、
下記の書籍を参考にさせていただきました!

[Unity公式マニュアル]

[参考書籍]

マイケル
マイケル
こちらの「AssetBundle」の章を参考にしています!
実践的なサンプルがたくさん載っているので、
今回の記事を読んで 詳細を知りたくなった方
他のサンプルが気になる方 は是非読んでみてください!
エレキベア
エレキベア
内容が盛りだくさんだから
1冊持っていて損はないクマ〜〜〜

基本的な使い方

マイケル
マイケル
それじゃ早速使っていこう!

AssetBundleファイルのビルド処理

ScreenShot 2021 04 19 11 17 55
マイケル
マイケル
まずはAssetBundleファイルのビルド処理から!
AssetBundleにしたいAssetを選択して、Inspector最下部よりAssetBundle名を入力します!
ScreenShot 2021 04 18 15 28 37
↑AssetBundle名の入力
マイケル
マイケル
AssetBundleを指定したら、ビルド処理を行います!
ビルドは下記メソッドより行うことができます!
// ビルド処理
BuildPipeline.BuildAssetBundles(
    "./AssetBundle01",            // ビルドパス
    BuildAssetBundleOptions.None, // オプション(圧縮、梱包等)
    BuildTarget.iOS               // プラットフォーム
);
マイケル
マイケル
第一引数にAssetBundleファイルを作成するパス
第二引数に圧縮方式第三引数にビルドするプラットフォーム
を指定します!
マイケル
マイケル
圧縮方式とプラットフォームは下記を指定できます!
用途に応じて設定しましょう!

・圧縮方式

圧縮方式実装
LZMA形式BuildAssetBundleOptions.None
非圧縮BuildAssetBundleOptions.UncompressedAssetBundle
LZ4形式BuildAssetBundleOptions.ChunkBasedCompression

・プラットフォーム(※一部抜粋)

圧縮方式実装
iOSBuildTarget.iOS
AndroidBuildTarget.Android
WindowsBuildTarget.StandaloneWindows
MacBuildTarget.StandaloneOSX

※詳細は下記参照
アセットバンドルのビルド – Unity スクリプトリファレンス
BuildTarget – Unity スクリプトリファレンス

エレキベア
エレキベア
いろんなオプションがあるのクマね
マイケル
マイケル
このメソッドを使って、実際にビルドしてみましょう!
下記スクリプトをEditorフォルダに作成します!
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;

/// <summary>
/// AssetBundleビルド用Editorクラス
/// </summary>
public class AssetBundleBuild
{
    /// <summary>
    /// AssetBundleビルド処理(AssetBundle01)
    /// </summary>
    [MenuItem("Assets/AssetBundle01 Build")]
    public static void Build01()
    {
        Build("./AssetBundle01");
    }

    /// <summary>
    /// AssetBundleビルド処理(共通)
    /// </summary>
    /// <param name="assetBundleDirectory">AssetBundleファイル作成パス</param>
    private static void Build(string assetBundleDirectory)
    {
        // ディレクトリ作成
        if (!Directory.Exists(assetBundleDirectory))
        {
            Directory.CreateDirectory(assetBundleDirectory);
        }
        // ビルド処理
        BuildPipeline.BuildAssetBundles(
            assetBundleDirectory,         // ビルドパス
            BuildAssetBundleOptions.None, // オプション(圧縮、梱包等)
            BuildTarget.iOS               // プラットフォーム
        );
        // 終了メッセージ表示
        EditorUtility.DisplayDialog("AssetBundleビルド完了", "AssetBundleのビルドが終わったでやんす。", "承知!");
    }
}
マイケル
マイケル
するとAssetウィンドウに「AssetBundle01 Build」というメニューが追加されるので実行しましょう!
ScreenShot 2021 04 18 15 32 56
↑ビルド処理の実行
マイケル
マイケル
実行すると下記のようにAssetBundleファイルとmanifestファイルが作成されます!
ScreenShot 2021 04 18 15 33 35
↑完了メッセージ
ScreenShot 2021 04 18 15 34 00
↑AssetBundleファイルとmanifestファイルが作成される
マイケル
マイケル
manifestファイルには、ハッシュ値や依存関係といった情報が記載されています!
手動で処理を行う場合、使うことも多いと思うため覚えておきましょう!
エレキベア
エレキベア
これで準備完了クマ〜〜〜

AssetBundleファイルのロード処理

ScreenShot 2021 04 19 11 18 03
マイケル
マイケル
AssetBundleファイルを作成したら、次はロード処理!
ローカルからのロード処理
マイケル
マイケル
ネットワーク経由でロードするのが一般的だけど、まずはローカルからのロード処理を実装します!
AssetBundle.LoadFromFile()メソッドでファイルのロード、LoadAsset()メソッドでインスタンスの生成を行うことができます!
// AssetBundleファイルをロード
AssetBundle assetBundle = AssetBundle.LoadFromFile("【AssetBundle格納パス】");

// ロードしたAssetBundleファイルからインスタンス生成
GameObject prefab = assetBundle.LoadAsset<GameObject>("【Asset名】");
エレキベア
エレキベア
ローカルからも読み込めるのクマね
マイケル
マイケル
上記メソッドを使って、下記のようなスクリプトを作成します!
Sceneの管理クラスとしてアタッチしてみましょう!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Test01Scene管理クラス
/// </summary>
public class Test01SceneManager : MonoBehaviour
{
    /// <summary>
    /// 開始処理
    /// </summary>
    void Start()
    {
        // AssetBundleロード処理
        StartCoroutine("LoadAssetBundle");
    }

    /// <summary>
    /// AssetBundleロード処理
    /// (ローカルからの取得)
    /// </summary>
    IEnumerator LoadAssetBundle()
    {
        // AssetBundleファイルをロード
        AssetBundle assetBundle = AssetBundle.LoadFromFile("AssetBundle01/char_prefab");
        if (assetBundle == null)
            yield break;
        
        // ロードしたAssetBundleファイルからインスタンス生成
        GameObject prefab = assetBundle.LoadAsset<GameObject>("Kani_red");
        Instantiate(prefab);
        Debug.Log("カニをロード!!");
        
        // マウスクリックでアンロード
        yield return new WaitUntil(() => Input.GetMouseButton(0));
        assetBundle.Unload(true);
        Debug.Log("カニをアンロード!!");
    }
}
マイケル
マイケル
するとこのようにしっかりとロードできていることが分かります!
使い終わったら、assetBundle.Unload()メソッドでアンロードしましょう!
(※上記処理ではマウスクリックでアンロードしています。)
01 LoadLocal
エレキベア
エレキベア
(シュールクマ・・・。)
ネットワーク経由でのロード処理
マイケル
マイケル
そして次はネットワーク経由でのロード処理!
下記のようにUnityWebRequestAssetBundle.GetAssetBundle()メソッドでリクエストを取得して、サーバからダウンロードします!

// Webサーバーからアセットバンドルをダウンロード
using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("【AssetBundle格納URL】"))
{
    // Webサーバーからのダウンロード待機
    yield return req.SendWebRequest();
    if (req.result == UnityWebRequest.Result.ConnectionError 
        || req.result == UnityWebRequest.Result.ProtocolError)
    {
        Debug.LogError("エラー:" + req.error);
        yield break;
    }

    // AssetBundleファイルをロード後、インスタンス生成
    AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(req);
    GameObject prefab = assetBundle.LoadAsset<GameObject>("【Asset名】");
}
マイケル
マイケル
このメソッドを使って実装してみましょう!
サーバを準備する必要がありますが、Python3がインストールされている環境であれば下記コマンドで立ち上げることができます!
python -m http.server 8080
マイケル
マイケル
コマンドをプロジェクト配下で実行することで「http://localhost:8080/【AssetBundle格納パス】」でアクセスできるようになります!
マイケル
マイケル
続けてスクリプトを下記に修正しましょう!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// Test01Scene管理クラス
/// </summary>
public class Test01SceneManager : MonoBehaviour
{
    /// <summary>
    /// 開始処理
    /// </summary>
    void Start()
    {
        // AssetBundleロード処理
        StartCoroutine("LoadAssetBundleNetwork");
    }

・・・略・・・

    /// <summary>
    /// AssetBundleロード処理
    /// (Webサーバからの取得)
    /// </summary>
    IEnumerator LoadAssetBundleNetwork()
    {
        // Webサーバーからアセットバンドルをダウンロード
        using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle("http://localhost:8080/AssetBundle01/char_prefab"))
        {
            // Webサーバーからのダウンロード待機
            yield return req.SendWebRequest();
            Debug.Log("http response:" + req.responseCode);
            if (req.result == UnityWebRequest.Result.ConnectionError 
                || req.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError("エラー:" + req.error);
                yield break;
            }
            Debug.Log("サーバからAssetBundleを取得!!");

            // AssetBundleファイルをロード後、インスタンス生成
            AssetBundle assetBundle = DownloadHandlerAssetBundle.GetContent(req);
            GameObject prefab = assetBundle.LoadAsset<GameObject>("Kani_red");
            Instantiate(prefab);
            Debug.Log("カニをロード!!");
            
            // マウスクリックでアンロード
            yield return new WaitUntil(() => Input.GetMouseButton(0));
            assetBundle.Unload(true);
            Debug.Log("カニをアンロード!!");
        }
    }
}
マイケル
マイケル
こちらも問題なく読み込めていることがわかります!
02 LoadNetwork
エレキベア
エレキベア
(さっきと変わらないクマ・・・。)

ロード時のテクニック

マイケル
マイケル
以上が基本的な処理となりますが、実用上使うにはまだまだ問題があります!
その中でもよく使いそうなロード時のテクニックとして2点紹介します!

複数ファイルのロード処理(依存関係の考慮)

マイケル
マイケル
まずは複数のAssetBundleファイルが存在する場合!
各Assetにはそれぞれ依存性があり、ロードする順番には気をつけなければならないです!
マイケル
マイケル
manifestファイルより、GetAllDependencies()メソッド依存性のあるAssetを全て取得することができるので、下記のように事前にロードしておきます!

// Manifestファイル
AssetBundleManifest manifest;
// ロード済AssetBundleリスト
Dictionary<string, AssetBundle> loadAssetBundleList = new Dictionary<string, AssetBundle>(); 

/// <summary>
/// AssetBundleロード処理
/// (Webサーバから複数ファイル取得)
/// </summary>
IEnumerator LoadAssetBundle()
{
    // AssetBudnle格納URL
    string url = "【AssetBundle格納URL】";

    // Manifestファイルをロード
    yield return loadAssetBundle(url, "【AssetBundleフォルダ名】");
    manifest = loadAssetBundleList["【AssetBundleフォルダ名】"].LoadAsset<AssetBundleManifest>("Assetbundlemanifest");

    // AssetBundleファイルのロード
    // (Manifestファイルから依存関係のファイルもロードする)
    foreach (string depAssetBundleName in manifest.GetAllDependencies("【AssetBundle名】"))
        yield return loadAssetBundle(url, depAssetBundleName);
    yield return loadAssetBundle(url, "【AssetBundle名】");

    // ロードしたAssetBundleファイルからインスタンス生成
    AssetBundle prefabAssetBundle = loadAssetBundleList["【AssetBundle名】"];
    GameObject prefab_kani_red = prefabAssetBundle.LoadAsset<GameObject>("【Asset名】");
}

/// <summary>
/// AssetBundleファイルロード処理
/// </summary>
/// <param name="url">AssetBundle格納URL</param>
/// <param name="assetBundleName">AssetBundle名</param>
private IEnumerator loadAssetBundle(string url, string assetBundleName)
{
        // ロード済の場合、処理しない
        if (loadAssetBundleList.ContainsKey(assetBundleName))
            yield break;

        ・・・ロード処理・・・
		
        // ロード済AssetBundleリストに追加
        loadAssetBundleList[assetBundleName] = DownloadHandlerAssetBundle.GetContent(req);
    }
}
マイケル
マイケル
一点気をつけないといけないのは、同じAssetを何度もロードしないことです!
上記処理では、loadAssetBundleList変数にロード済のAssetを保持するようにしています!
エレキベア
エレキベア
複数のAssetBundleファイルがある場合は、
・依存性のあるAssetを事前にロードする。
・一度ロードしたAssetは保持しておく。

ことが大事クマね
マイケル
マイケル
そのとおり!
それじゃこの処理を使って実装してみよう!
今回は下記のようにカニ3種類をchar_prefab、それぞれのテクスチャをchar_textureに設定してAssetBundleファイルを作成してみます!

・AssetBundke名の設定

AssetBundle名Asset名
char_prefabKani_red
char_prefabKani_blue
char_prefabKani_yellow
char_texturekani_red_col
char_texturekani_blue_col
char_texturekani_yellow_col
ScreenShot 2021 04 19 0 35 02
ScreenShot 2021 04 19 0 35 11
マイケル
マイケル
さっき作ったフォルダと分けるため、
今回はAssetBundle02フォルダに作成しました!
ScreenShot 2021 04 19 0 37 18
マイケル
マイケル
作成されたmanifestファイルの中身を見てみると、
char_prefabが、char_textureに依存していることが分かります!
ScreenShot 2021 04 19 0 37 32
↑char_prefabはchar_textureに依存している
エレキベア
エレキベア
char_textureから先にロードしないといけないわけクマね
マイケル
マイケル
その通り!
新たにSceneを作成して、下記スクリプトをアタッチしてみよう!
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// Test02Scene管理クラス
/// </summary>
public class Test02SceneManager : MonoBehaviour
{
    // Manifestファイル
    AssetBundleManifest manifest;
    // ロード済AssetBundleリスト
    Dictionary<string, AssetBundle> loadAssetBundleList = new Dictionary<string, AssetBundle>(); 

    /// <summary>
    /// 開始処理
    /// </summary>
    void Start()
    {
        // AssetBundleロード処理
        StartCoroutine("LoadAssetBundle");
    }

    /// <summary>
    /// AssetBundleロード処理
    /// (Webサーバから複数ファイル取得)
    /// </summary>
    IEnumerator LoadAssetBundle()
    {
        // AssetBudnle格納URL
        string url = "http://localhost:8080/AssetBundle02/";

        // Manifestファイルをロード
        yield return loadAssetBundle(url, "AssetBundle02");
        manifest = loadAssetBundleList["AssetBundle02"].LoadAsset<AssetBundleManifest>("Assetbundlemanifest");

        // AssetBundleファイルのロード
        // (Manifestファイルから依存関係のファイルもロードする)
        foreach (string depAssetBundleName in manifest.GetAllDependencies("char_prefab"))
            yield return loadAssetBundle(url, depAssetBundleName);
        yield return loadAssetBundle(url, "char_prefab");

        // ロードしたAssetBundleファイルからインスタンス生成
        AssetBundle prefabAssetBundle = loadAssetBundleList["char_prefab"];
        GameObject prefab_kani_red = prefabAssetBundle.LoadAsset<GameObject>("Kani_red");
        Instantiate(prefab_kani_red);
        GameObject prefab_kani_blue = prefabAssetBundle.LoadAsset<GameObject>("Kani_blue");
        Instantiate(prefab_kani_blue);
        GameObject prefab_kani_yellow = prefabAssetBundle.LoadAsset<GameObject>("Kani_yellow");
        Instantiate(prefab_kani_yellow);
        Debug.Log("カニ軍団をロード!!");

        // マウスクリックでアンロード
        yield return new WaitUntil(() => Input.GetMouseButton(0));
        prefabAssetBundle.Unload(true);
        Debug.Log("カニ軍団をアンロード!!");
    }

    /// <summary>
    /// AssetBundleファイルロード処理
    /// </summary>
    /// <param name="url">AssetBundle格納URL</param>
    /// <param name="assetBundleName">AssetBundle名</param>
    private IEnumerator loadAssetBundle(string url, string assetBundleName)
    {
        // ロード済の場合、処理しない
        if (loadAssetBundleList.ContainsKey(assetBundleName))
            yield break;
        
        // AssetBundleのロード処理
        string loadUrl = Path.Combine(url, assetBundleName);
        using (UnityWebRequest req = UnityWebRequestAssetBundle.GetAssetBundle(loadUrl))
        {
            // Webサーバーからのダウンロード待機
            yield return req.SendWebRequest();
            Debug.Log("http response:" + req.responseCode);
            if (req.result == UnityWebRequest.Result.ConnectionError 
                || req.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError("エラー:" + req.error);
                yield break;
            }
            Debug.Log("サーバからAssetBundleを取得!! -> " + assetBundleName);

            // ロード済AssetBundleリストに追加
            loadAssetBundleList[assetBundleName] = DownloadHandlerAssetBundle.GetContent(req);
        }
    }
}
マイケル
マイケル
これで無事にカニ達を召喚することができたね!
03 ManyKani
エレキベア
エレキベア
(増えたクマ・・・。)

差分がある場合のみロードする

マイケル
マイケル
次は差分が発生した時だけロードする方法です!
これは大きく、ロード時にバージョンを指定する方法と、ハッシュ値を指定する方法の2つがあります。
バージョンを指定する
マイケル
マイケル
バージョンを指定する方法については、下記のようにロード時に引数でバージョンを渡します!
private string url = "【AssetBundle格納URL】";
private uint version = 1;
req = UnityWebRequestAssetBundle.GetAssetBundle(url, version, 0);
↑バージョン指定によるロード処理
マイケル
マイケル
バージョンを指定することでキャッシュされて、違うバージョンを渡した時のみ再ロードするようになります!
エレキベア
エレキベア
これだけでキャッシュしてくれるのは便利クマね〜〜〜
マイケル
マイケル
ちなみに第3引数は、CRCチェックをする場合に指定します!
今回は使わないので、チェックを行わないという意味の0を指定しています。
ハッシュ値を指定する
マイケル
マイケル
バージョンによる管理ができれば便利だけど、全てのAssetのバージョンを管理するのは大規模なシステムだと難しい!
そこでもう一つ、ハッシュ値を指定する方法があります!
マイケル
マイケル
ハッシュ値はManifestファイルに記載されていて、下記のように取得して引数に指定してあげると、バージョン指定と同じようにキャッシュされます!
private string url = "【AssetBundle格納URL】";
private Hash128 hash = manifest.GetAssetBundleHash(【AssetBundle名】);
req = UnityWebRequestAssetBundle.GetAssetBundle(url, hash, 0);
↑ハッシュ値指定によるロード処理
マイケル
マイケル
バージョン指定やハッシュ値指定のロード処理は、
下記のように組み込んで使いましょう!
    /// <summary>
    /// AssetBundleロード処理
    /// (AssetBundleの変更をhash値から検知する)
    /// </summary>
    IEnumerator LoadAssetBundle()
    {

・・・略・・・

        // キャッシュシステムの準備を待つ
        yield return new WaitUntil(() => Caching.ready);

        // Manifestファイルをロード
        // (ハッシュ未使用、常にロードする)
        yield return loadAssetBundle(url, "【AssetBundle格納パス】", false);
        manifest = loadAssetBundleList["【AssetBundle格納パス】"].LoadAsset<AssetBundleManifest>("Assetbundlemanifest");
        
・・・略・・・

        // AssetBundleファイルのロード
        // (ハッシュ使用、Manifestファイルから依存関係のファイルもロードする)
        foreach (string depAssetBundleName in manifest.GetAllDependencies("【AssetBundle名】"))
            yield return loadAssetBundle(url, depAssetBundleName, true);
        yield return loadAssetBundle(url, "【AssetBundle名】", true);

・・・略・・・

    }

    /// <summary>
    /// AssetBundleファイルロード処理
    /// </summary>
    /// <param name="url">AssetBundle格納URL</param>
    /// <param name="assetBundleName">AssetBundleファイル名</param>
    /// <param name="useHash">ロードにハッシュを使用するか</param>
    private IEnumerator loadAssetBundle(string url, string assetBundleName, bool useHash)
    {
        // ロード済の場合、処理しない
        if (loadAssetBundleList.ContainsKey(assetBundleName))
            yield break;
        
        // hashの有無によりロード方法を変更
        string loadUrl = Path.Combine(url, assetBundleName);
        UnityWebRequest req;
        if (useHash)
        {
            // hashを使用する場合、変更分のみロードする
            Hash128 hash = manifest.GetAssetBundleHash(assetBundleName);
            req = UnityWebRequestAssetBundle.GetAssetBundle(loadUrl, hash, 0);
        }
        else
        {
            // hashを使用しない場合、常にロードする
            req = UnityWebRequestAssetBundle.GetAssetBundle(loadUrl);
        }

        // AssetBundleのロード処理
        using (req)
        {
            // Webサーバーからのダウンロード待機
            yield return req.SendWebRequest();
            Debug.Log("http response:" + req.responseCode);
            if (req.result == UnityWebRequest.Result.ConnectionError 
                || req.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError("エラー:" + req.error);
                yield break;
            }
            Debug.Log("サーバからAssetBundleを取得!! -> " + assetBundleName);
            // ロード済AssetBundleリストに追加
            loadAssetBundleList[assetBundleName] = DownloadHandlerAssetBundle.GetContent(req);
        }
    }
マイケル
マイケル
キャッシュされているかどうかは、req.responseCodeで確認することができます!
2回目以降の実行で、下記のようにレスポンスが0で返ってきていれば上手くキャッシュ出来ています!
ScreenShot 2021 04 19 1 02 01
↑キャッシュ後のロード処理
マイケル
マイケル
キャッシュされた後、位置を変えるなど変更を加えた後に再ビルドすると、レスポンスが200で返ってきて再ロードされていることも確認できます!
ScreenShot 2021 04 19 1 05 22
↑再ロード処理
エレキベア
エレキベア
うまく効いてるクマね〜〜

おわりに

マイケル
マイケル
というわけで今回はAssetBundleを触ってみました!
どうだったかな?
エレキベア
エレキベア
なかなか慣れるまでが大変そうクマね〜〜
でもアプリと分離化できるのは便利クマ!
マイケル
マイケル
大規模なゲーム開発となると、
使うのはほぼ必須になると思うから、がんばって慣れていこう!!
マイケル
マイケル
それでは今日はこの辺で!!
アデューー!!
エレキベア
エレキベア
クマ〜〜〜〜〜〜

【Unity】AssetBundleを使ってリソースを管理する 〜完〜

Unity
スポンサーリンク
この記事をシェアしよう!
フォローお待ちしています!
都会のエレキベア

コメント