1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】Addressablesのhandle管理と解放忘れに悩んだので管理モジュールを作ってみた

1
Posted at

はじめに

Unity の Addressables の読み込みから解放までを一括で管理するモジュール MAH-AddressableManager を作り、GitHub で公開しました。
UPM(git URL)で導入できます。

この記事では、作った理由・使い方・内部の実装で工夫したところを紹介します。

動作環境

  • Unity 6000.0 以降
  • Addressables 2.9.0 以降

できること

  • LoadAsync<T> と Release だけで扱える(handle と参照カウントはモジュールが管理)
  • 同じアセットを何度 Load しても、実際の読み込みは1回
  • 解放し忘れた分は、Load のときに指定した寿命(Manual / Scene / Owner)の終わりに自動で回収
  • Label を指定して、型の違うアセットをまとめて読み込み・まとめて解放
  • 更新データの確認(件数・容量)とダウンロード(進捗・リトライつき)
  • [AddressablePath] / [AddressableLabel] 属性で、Inspector のプルダウンからアドレス・Label を選べる
  • リークや参照カウントを確認できるデバッグウィンドウなどの Editor ツールつき

使い方

ここでは簡単に説明します。
詳しくは、GitHubの使用方法をご覧ください。

準備

  1. Create > MAH > Addressable > RuntimeSettings で設定アセットを作る
  2. 起動シーンの GameObject に AddressableManager を付け、設定アセットを割り当てる

Manager は DontDestroyOnLoad で常駐し、起動時に Addressables の初期化を自動で始めます(設定で手動にもできます)。

読み込みと解放

操作の入口は AddressableManager.AccInstance.AccHandler の1つだけです。

using System;
using MAH.AddressableManager;
using MAH.AddressableManager.Attribute;
using UnityEngine;

public sealed class EnemyView : MonoBehaviour
{
    // Inspector のプルダウンからアドレスを選ぶ
    [SerializeField, AddressablePath] private string _spriteAddress;

    private AssetTicket<Sprite> _ticket;
    private bool _hasTicket;

    private async void Start()
    {
        var handler = AddressableManager.AccInstance.AccHandler;
        try
        {
            // 読み込んだアセット本体と、解放用のチケットが返る
            _ticket = await handler.LoadAsync<Sprite>(
                AssetKey.FromPath(_spriteAddress), AssetLifetime.Manual, destroyCancellationToken);
            _hasTicket = true;
            GetComponent<SpriteRenderer>().sprite = _ticket.AccAsset;
        }
        catch (OperationCanceledException)
        {
            // 読み込み中に破棄された。チケットは発行されないので解放は不要
        }
    }

    private void OnDestroy()
    {
        // 使い終わったらチケットを返して解放する
        if (_hasTicket) AddressableManager.AccInstance?.AccHandler.Release(_ticket);
    }
}

Release には handle ではなくチケットを渡します。同じアセットを何か所から Load しても、それぞれが自分のチケットを返せばよいので、「何回 Release すればいいか」を考えなくて済みます。同じチケットを2回 Release しても、警告が出るだけで例外にはなりません。

寿命で解放忘れに備える

Load のときに寿命を指定すると、Release されなかった分がその寿命の終わりに自動で回収されます。

寿命 自動で回収されるタイミング
AssetLifetime.Manual 自動では回収しない
AssetLifetime.Scene() 呼び出した時点のシーンがアンロードされたとき
AssetLifetime.Owner(this) 指定した GameObject が破棄されたとき
// この GameObject が破棄されたら、Release を忘れていても回収される
var ticket = await handler.LoadAsync<Sprite>(
    AssetKey.FromPath(_address), AssetLifetime.Owner(this), ct);

自動回収は安全網で、基本は Release を呼ぶ前提です。自動回収が起きると、Editor では確保した場所のスタックトレースつきで警告が出るので、解放忘れの場所をすぐ見つけられます。

Label でまとめて読み込む

Load のときは型を指定せず、取り出すときに型を指定します。

var group = await handler.LoadByLabelAsync(_label, AssetLifetime.Owner(this), ct);

if (group.TryGetAsset<Sprite>("icon_sword", out var sprite))
    _image.sprite = sprite;

var clips = group.GetAssets<AudioClip>();
for (int i = 0; i < clips.Count; i++)
    Register(clips[i]);

// まとめて解放
handler.Release(group);

更新データのダウンロード

確認ダイアログに出す件数・容量を先に取り、同意を得てからダウンロードできます。

var result = await handler.CheckUpdateAsync(ct);
if (result.AccHasDownload && await ShowConfirmDialogAsync(result.AccBundleCount, result.AccTotalBytes))
{
    try
    {
        await handler.DownloadAsync(Progress.Create<DownloadStatus>(OnProgress), ct);
    }
    catch (AddressableDownloadException ex)
    {
        ShowRetryDialog(ex); // ex.AccPhase で失敗した段階が分かる
    }
}

Editor ツール

「今何が読み込まれているのか分からない」「Addressables への登録が面倒」を解消するための Editor ツールもいくつか作成しました。

ツール 開き方 できること
デバッグウィンドウ MAH/Addressable/Debug Window Play 中の読み込み状況・リーク・Owner の回収を確認
Dependency Check MAH/Addressable/Dependency Check 未登録のまま使われているアセットを、Play もビルドもせずに一覧
自動登録 MAH/Addressable/Auto Registration ルールに従い、インポート時に Addressables へ自動登録
手動一括登録 Project ウィンドウで右クリック → MAH → Register to Addressables... 選んだフォルダの中身をまとめて登録

デバッグウィンドウと Dependency Check は読み取り専用です。Addressables の設定を書き換えるのは、登録の2つ(自動登録・手動一括登録)だけで、それも自分で指示したときに限ります。

デバッグウィンドウ

Play 中に AddressableManager の状態を、タブを切り替えて確認できます。

  • 実行時状態:読み込み済みのアセットを一覧で表示します。アセット名・参照カウント・読み込みの状態(読み込み中/完了/失敗)に加え、寿命ごとの確保数(Manual / Scene / Owner)と、確保した場所のスタックトレースが見られます。スタックトレースの行をクリックすると、そのコードにジャンプします
  • リーク:Release されずに、寿命の終わりに自動で回収されたものを、確保した場所ごとに集計します。Console のログは流れて見逃しやすいので、直すべき場所をここでまとめて確認できます。テキストファイルへの出力もできます
  • Owner:Owner 寿命の監視と回収が正しく動いているかを確認します

リークとして記録されるのは、Scene / Owner 寿命で確保して、チケットを受け取ったのに Release しなかったものです。Editor 上だけの機能で、ビルドしたアプリでは記録しません。

Dependency Check

Addressables に登録したアセットが使っている、未登録のアセットを一覧にします。未登録のアセットは、それを使っている登録済みアセットのバンドルに暗黙的に含まれます。
そのため、複数のグループから使われていると、ビルドしたときに重複して入る可能性があります。

  1. ウィンドウを開いて Scan を押す
  2. 未登録のアセットが1行ずつ表示される(名前・型・サイズ・使っているグループの数など)
  3. 2つ以上のグループから使われているものが強調される。展開すると、使っている親のアセットとグループが分かる
  4. 行をクリックすると、Project ウィンドウでそのアセットが選択される

グループの分け方を見直したいときに手軽に使えます。

自動登録(Auto Registration)

アセットを Addressables に登録する作業(グループに入れる、アドレスを決める、Label を付ける)を、ルールに従って自動化します。ルールは ScriptableObject で、対象フォルダごとに作ります。

項目 内容
Target Folder 対象のフォルダ(GUID で保持するので、移動や名前変更で壊れない)
Extensions 登録する拡張子
Address Template アドレスの付け方。{Asset.Name} {Folder.Name} などのマクロが使える
Target Group 登録先のグループ。無ければ自動で作成
Labels 付ける Label。マクロが使える
  • ルールを作るまでは何も起きないので、パッケージを入れただけでグループが書き換わることはありません
  • 同じアセットとルールなら、結果は常に同じです。チームの全員の環境で、同じ登録結果になります
  • 複数のルールが同じアセットに当てはまる場合は、競合として登録せずにエラーを出します
  • 既存のアセットには、ウィンドウの 当て直す(Reapply)で適用できます。書き込む前に、変更内容を一覧で確認できます

手動一括登録(Register to Addressables)

ルールを作るほどではないときや、登録のタイミングを自分で決めたいときのためのツールです。

  1. Project ウィンドウでフォルダを右クリックし、MAH → Register to Addressables... を選ぶ
  2. 配下のアセットが一覧に出る(登録済みのものは除く)
  3. 拡張子とチェックボックスで絞り込み、登録先のグループと Label を選ぶ
  4. 登録する を押す。確認ダイアログで件数と新規グループを確かめてから登録される

そのほか

  • [AddressablePath] / [AddressableLabel] 属性を付けた string フィールドは、Inspector でアドレス・Label をプルダウンから選べます(タイポの防止)
  • Editor ツールの表示は、日本語と英語を切り替えられます(Edit > Preferences > MAH Addressable > Language)。JSON ファイルを用意すれば、ほかの言語も追加できます

おわりに

Addressables の handle 管理や解放忘れに悩んでいる方の参考になればうれしいです。
詳しい使い方は GitHub の README と Docs/usage.md(英語)にまとめています。不具合や要望は Issue でお気軽にどうぞ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?