Unity Addressables入門(1.サーバーからデータをロードする)

目次

Addressablesってなに?

Unityでゲーム開発をしていてこんなことに直面したことはないでしょうか?
・ゲームの起動時間を短くしたい
・アプリの容量が150MBを超えてしまい、GooglePlayStoreの審査にアプリを出せない
・ビルドを上げ直さずに運用中のゲームのコンテンツを入れ替えたい

UnityのAddressablesという機能を使うと、サーバーからアセットをDL・ロードすることが可能になり、上記のような問題を解消することが可能です。

ですが、Addressablesは実装が難しいイメージがあります。非同期処理、サーバーの内容が絡むので、始めての人には少しハードルがあると思いますが、できるだけわかりやすく、サーバーサイド側の設定も含めて、Addressablesの使い方をわかりやすく解説していきます。

今回のゴールとしては、Addressablesを使って以下を実現していきます。
・サーバーにゲーム側から呼び出したいデータをアップロードする
サーバーからデータをロードしてゲームの中に取り込む

Addressableのインストールと初期設定

まずは、UnityにAddressablesのインストールをしていきます。
インストールはカンタンです。パッケージマネージャーから、Addressableと検索してインストールをしていきます。

インストールが完了後、[Window] > [AssetManagement] > [Addressables] > [Group]を選択します

[Create Addressables Settings]というボタンが出てくるので押します。

これでAddressablesGroupが作成されました。

今回、StageDataをAddressablesとして管理する、というシナリオなので、StageDataというGroupsNameにしました。

Addressablesでは、AddressablesGroupの単位で、データをサーバーからロードしたり、ゲーム内にロードしたデータをアンロードすることができます。

なので、例えば、Aシーンをロードする際にAddressablesGroup-Aをロードし、AシーンからBシーンに遷移したタイミングでAddressablesGroup-Aをアンロードする、といった管理が可能になります。

Addressableでアセットをビルド

Addressable用のScriptableObjectの作成

つぎに、Addressableにしたいアセットを作り、実際にAddressableとしてパッケージ化していきます。

今回はサンプルとして、ステージのデータをAddressableとしてパッケージ化し、サーバーにアップロードしていきます。
まずは、StageDataをScriptableObjectとして作成します。

ScriptableObjectって何?という方は以下の記事を参照ください。

StageDataとして以下のようなScriptableObjectを定義しました。

using UnityEngine;
using UnityEngine.AddressableAssets;

[CreateAssetMenu(fileName = "StageData", menuName = "Scriptable Objects/StageData")]
public class StageData : ScriptableObject
{
    [Header("ステージID")]
    public int stageID;

    [Header("サムネイル画像")]
    public Sprite thumbnail;

    [Header("ステージプレハブ(Addressables対応)")]
    public AssetReference prefab;
}

ポイントとしては、Prefabを指定したい場所を、GameObjectではなくAssetReferenceとしている点です。
Addressableを使ってGameObjectを指定する場合は、AssetReferenceを使います。

今回、適当に作った以下のオブジェクト郡を1つのPrefabとしてAddressableに登録してみます。
まずはStage1というPrefabに設定しました

次に、PrefabのインスペクタでAddressablesのチェックを入れます。チェックを入れると先程作ったAddressableGroupとの紐づけが表示されます。

次に、この作ったPrefabをScriptableObjectにも登録し、同様にAddressableにチェックを入れます。

サムネイルには以下のような適当なSpriteを設定しました。

このSpriteに設定しているTextureもAddressablesに登録しておきます。

これで、Addressable用のデータを作る準備ができました。

格納先のサーバーのパスを作る

次に、リモートに配置するデータのパスを作ります。

今回、無料枠も使えるGoogleCloudStorage(GCS)を使うことにしました。
余談ですが、筆者が提供している以下の画像変換サービスもGoogleCloudStorageを使っています。

あわせて読みたい

アカウントを作成し、プロジェクトを作り、GoogleCloudStorageに以下のようなフォルダを作りました。
もちろん、AWSなどのサービスを使うことも可能です。

また、本番運用では禁止した方がよいですが、今回は検証目的ですので、パブリックアクセスを許可しています。

パブリックアクセス禁止のチェックを外す

ストレージを作ると以下のような画面になります。今回バケットの中にstage-dataというフォルダを作り、ここにAddressablesのアセットを格納しようと思います。
また、公開アクセスが「インターネットに公開」になっていることを確認します。

GCSの場合、フォルダのパスは以下になります。

https://storage.googleapis.com/[バケット名]/[フォルダ名]

これでアップロードするパスの準備ができました。

サーバーアップロード用ビルド


次に、Addressable用のデータをサーバーにアップロードするために、Addressable用にパッケージ化したデータをビルドします。

まずは、リモート用から読み出す用のビルドデータを作成する設定を行います。

パスの設定

[Window] > [AssetManagement] >[Addressables] > [Profiles]を選択します。

ここでは、リモートのパスと、ビルドしたAddressablesの出力先パスを設定します。

Catalogの設定を行う

次に[Assets] > [AddressableAssetsData] > [AddressableAssetSettings]を選択します。


Build&Load Pathsに[Remote]を設定し、BuildRemoteCatalogとEnableJsonCatalogにチェックをつけます。

これでAddressablesのビルド時に、リモートに対応したファイルが出力されるようになりました。

Addressable Groupの設定を行う

最後に、[Assets] > [AddressableAssetsData] > [AssetsGroups]
から、AddressablesGroupの設定を選択します。

次に、インスペクタから、[Build&LoadPaths]を[Remote]に設定します。


これでAddressablesの設定は完了です。

Addressablesのデータをビルド

ビルドの前にAddressableNameを修正します。このNameがAddressablesを呼び出すキーになるため、基本的に短いワードになるように設定しておきます。

AddressablesGroupsから、[Build]>[New Build] > [Default Build Script]でビルドを掛けます。

クリックすると、ビルドが始まります。もちろん中身やPCスペックによりますが、長くても3分も掛からないくらいかと思います。2回目以降は速くなります。

ビルドに成功すると、以下のようなビルドレポートが表示されます。

ビルドしたデータは、デフォルトの設定のままなら、プロジェクトルートフォルダの、[ServerData]の中に格納されています。

ビルドしたデータはこんな感じ。

拡張子が、.hashと.jsonのデータと.bundleのデータが含まれていることを確認しましょう。
.bundleはアセットの数が増えるごとに増えますが、.hashと.jsonは必ず1つです。

上記の拡張子ファイルが含まれていなければどこかしらの設定が足りていません。

また、ビルドに失敗する場合は、一度キャッシュをクリアしてから再度ビルドすると成功することがあります。

キャッシュクリア

サーバーにビルドしたAddressableを格納

これらのAddressablesビルドファイルを先程作ったGoogleCloudStorageにアップロードします。

ファイルがアップロードされました、これで準備は完了です。
データに更新を掛けたい場合は、このアップロードファイルを変更していけば、各端末側のゲーム内容を変えることができます。

サーバーからAddressableをロード

ようやくリモートサーバーからアセットをロードする準備ができました。
サーバーからロードしたScriptableObjectから、IDやサムネ画像をUIに表示し、StageのPrefabを生成する最低限のスクリプトを書きます。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using TMPro;
using UnityEngine.UI;

public class StageLoader : MonoBehaviour
{
    [Header("Addressablesで設定したアセットのキー(Address)")]
    public string stageAddress = "StageData";

    [Header("UI要素")]
    public TMP_Text stageIDText;// ステージID
    public Image thumbnailImage; // サムネ画像
    public Transform spawnPoint; // プレハブを生成する位置

    private void Start()
    {
        LoadStageData(stageAddress);
    }

    private void LoadStageData(string address)
    {
        Addressables.LoadAssetAsync<StageData>(address).Completed += OnStageDataLoaded;
    }

    private void OnStageDataLoaded(AsyncOperationHandle<StageData> handle)
    {
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            StageData stageData = handle.Result;
            Debug.Log($"ロード成功: ステージID {stageData.stageID}");

            // ステージIDをTMPに表示
            if (stageIDText != null)
            {
                stageIDText.text = $"{stageData.stageID}";
            }

            // サムネイルをImageに表示
            if (thumbnailImage != null && stageData.thumbnail != null)
            {
                thumbnailImage.sprite = stageData.thumbnail;
            }

            // プレハブをAddressablesからインスタンス
            if (stageData.prefab != null)
            {
                InstantiatePrefab(stageData.prefab);
            }
        }
        else
        {
            Debug.LogError($"StageDataのロードに失敗しました: {handle.OperationException}");
        }
    }

    private void InstantiatePrefab(AssetReference prefabReference)
    {
        prefabReference.InstantiateAsync(spawnPoint.position, Quaternion.identity).Completed += handle =>
        {
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                GameObject instance = handle.Result;
                Debug.Log($"Prefabインスタンス生成成功: {instance.name}");
            }
            else
            {
                Debug.LogError($"Prefabのインスタンス生成に失敗: {handle.OperationException}");
            }
        };
    }
}

本来こういったロード処理は非同期で記述すべきなので、UniTaskを使った非同期ロードでのバージョンでも残しておきます。
ロード開始からロードされるまでの間、ローディング表示などをしてあげると、ユーザフレンドリーかと思います(今回は省略)。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using TMPro;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading.Tasks;

public class StageLoader : MonoBehaviour
{
    [Header("Addressablesで設定したアセット名(Address)")]
    public string stageAddress = "StageData";

    [Header("UI要素")]
    public TMP_Text stageIDText;// ステージIDテキスト
    public Image thumbnailImage; // サムネ画像
    public Transform spawnPoint; // プレハブを生成する位置

    private async void Start()
    {
        // ステージデータを非同期でロード
        await LoadStageDataAsync(stageAddress);
    }

    private async UniTask LoadStageDataAsync(string address)
    {
        Debug.Log($"StageDataのロード開始: {address}");
        try
        {
            // Addressablesを使って非同期でStageDataをロード
            var handle = Addressables.LoadAssetAsync<StageData>(address);
            StageData stageData = await handle.ToUniTask();

            if (stageData == null)
            {
                Debug.LogError("StageDataが見つかりませんでした。");
                return;
            }

            Debug.Log($"ロード成功: ステージID {stageData.stageID}");

            // ステージIDをTMPに表示
            if (stageIDText != null)
            {
                stageIDText.text = $"{stageData.stageID}";
            }

            // サムネイルをImageに表示
            if (thumbnailImage != null && stageData.thumbnail != null)
            {
                thumbnailImage.sprite = stageData.thumbnail;
            }

            // プレハブを非同期でインスタンス化
            if (stageData.prefab != null)
            {
                await InstantiatePrefabAsync(stageData.prefab);
            }
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"StageDataのロードに失敗しました: {ex.Message}");
        }
    }

    private async UniTask InstantiatePrefabAsync(AssetReference prefabReference)
    {
        try
        {
            // プレハブを非同期でインスタンス化
            var handle = prefabReference.InstantiateAsync(spawnPoint.position, Quaternion.identity);
            GameObject instance = await handle.ToUniTask();

            if (instance != null)
            {
                Debug.Log($"Prefabインスタンス生成成功: {instance.name}");
            }
            else
            {
                Debug.LogError("Prefabインスタンスが生成できませんでした。");
            }
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"Prefabのインスタンス生成に失敗: {ex.Message}");
        }
    }
}

このコードを適当なGameObjectにアタッチして、適当なオブジェクトにアタッチします。

エディタの実行時は、AddressablesGroupsで、[Use Existing Build]を選択します。

これを選択しないと、リモートから実際にデータをロードしてくれません。

これでようやく、実行...!!

リモートサーバーから読み込まれたStageID、サムネ画像、StageのPrefabがロードされました!

Addressablesの補足

サーバー側のキャッシュに気をつける

サーバーにもよるのですが、サーバーのキャッシュの機能により、サーバー側のデータを変えたのに新しいデータがロードされない!ということが起きます。GCSだと特に。

対策として、サーバー側のキャッシュを無効にする設定を入れるでもいいのですが、一番手っ取り早いのはAddressableのビルドバージョンを上げてファイル名を変えてアップすることです。

デフォルトの設定だと、エディターのPlayerSettingsのバージョンとファイル名が紐づいています。

なので、Addressableのビルドの前に、プロジェクト設定のバージョンを上げてあげれば、キャッシュ問題を回避できます。

バージョンを0.1.1にあげる

アセットの検索方法

今回アセットのキーを使ってロードを行いました。

ただ、キーを使う以外にもラベルを使って検索する方法もあります。

複数のアセットをまとめてロードしたい時などは、キーではなくLabelsを使ってロードすることをおすすめします。

まとめと次回予告

以上Addressablesの機能の触りの部分の紹介でした。
次回は、Addressablesを使ってロードしてきたファイルをメモリに残さないように、アンロードする部分や、catalo.jsonの中身についてもう少し補足したいと思います(多分)。

Addressablesは大分クセがある機能なので、結構学習に手間取る人も多いと思います。私もこの記事書くにあたって2回くらい分からなくて発狂してました。
ただ、使いこなせば大いにゲーム体験を向上させてくれる機能です、ぜひ使っていきましょ~

それでは、素敵なゲーム制作ライフを!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ゲーム制作の敷居を下げ、もっと多くの人にゲーム作りを楽しんでもらうために、ゲームをカンタンに作る方法を”網羅的に”解説しています。
よかったらブックマークお願いします。
Twitter(X)もよければフォローお願いします。

コメント

コメントする


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。

目次