アセットプロバイダー

Addressables 上の非同期アセットロード。キーごとの参照カウントとスコープ単位の一括クリーンアップ付き。

1 行で書ける非同期アセットロード。共有は自動、クリーンアップはリークを許しません。

インターフェースIAssetProvider
オフスイッチNullAssetProvider
アセンブリCommonGameSystem.Assets(オプション — Addressables パッケージが必要)
起動起動時に自動登録 — セットアップ不要

概要

アセットプロバイダーは、Unity の Addressables システムをシンプルな参照カウント付きインターフェースの背後にラップします。アドレス文字列でアセットをロードすると、同じキーを使うすべての呼び出し元が同一のロード済みアセットを共有します — 参照カウントが保持者の数を追跡し、Release(key) は最後の呼び出し元が解放したときにだけアンロードします。これにより「このテクスチャは誰がアンロードするのか?」という調整問題は、フレームワークが肩代わりする帳簿付けに変わります。

ロードをスコープにまとめることもできます:CreateScope() で作成し、それを通してロードすれば、スコープを破棄するだけでロードしたすべてが 1 回の呼び出しで解放されます。スコープは「この画面が必要としたすべて」の自然な単位です。

メモリバジェットも自動追い出しもありません — ロードし、参照を数え、スコープでクリーンアップするだけです。保持しているものが、そのままメモリに残るものです。

このサービスはオプションの CommonGameSystem.Assets アセンブリにあります。プロジェクトから Addressables パッケージを削除すると、このアセンブリは自動的に除外され、フレームワークの残りはそのままコンパイルされ続けます。

クイックスタート

Awake でサービスを解決してキャッシュします:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyGameManager : MonoBehaviour
{
    private IAssetProvider _assets;

    private void Awake()
    {
        _assets = ServiceLocator.Resolve<IAssetProvider>();
    }

    private async void OnEnable()
    {
        var prefab = await _assets.LoadAsync<GameObject>("assets/my-prefab");
        if (prefab != null)
            Instantiate(prefab);
    }
}

短いエイリアスも使えます:IAssetProvider assets = SL.Resolve<IAssetProvider>();

API リファレンス

Load / Release(参照カウント付き)

  • Task<T> LoadAsync<T>(string key, CancellationToken ct = default) — 指定アドレスのアセットを型 T(GameObjectScriptableObjectSpriteTextAssetAudioClip など)としてロードします。同じキーを再度ロードすると同じアセットが返り、参照カウントが増えます。返される Task<T> が null になることはありません。ランタイム障害 — キーが見つからない、ロードの失敗 — は default(T) を返して警告をログします。決して例外は投げません。
  • void Release(string key) — キーの参照カウントを 1 減らします。カウントが 0 に達するとアセットがアンロードされます。すでに 0 の場合は警告をログして何もしません。例外は投げません。

Preload / Query

  • Task PreloadAsync<T>(string key, CancellationToken ct = default) — アセットを返さずにロードしてキャッシュに保持します。LoadAsync<T> と同じ参照カウントを使うので、後の Release(key) と対にしてください。現在のシーンをプレイ中に次のシーンのアセットをウォームアップするのに便利です。
  • bool IsLoaded(string key) — キーの参照カウントが 0 より大きければ true を返します。例外もログも発生しません。
  • int LoadedCount { get; } — 現在保持している異なるキーの数。これはリークのシグナルです:数えるのはキーであって、全参照カウントの合計ではありません。

スコープ

  • IAssetScope CreateScope() — スコープを作成します。スコープを通してロードすれば、Dispose() がスコープのロードしたすべてを解放します — 記録されたロード 1 件につき Release(key) 1 回です。
  • IAssetScope — 同じ LoadAsync<T>PreloadAsync<T>IsLoadedLoadedCount メンバーに加え、一括クリーンアップ用の Dispose() を提供します。

使用例

シーンのプレハブをロードし、作業が終わったらまとめてクリーンアップします:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class ScenePopulator : MonoBehaviour
{
    private IAssetProvider _assets;

    private async void Start()
    {
        _assets = ServiceLocator.Resolve<IAssetProvider>();
        using var scope = _assets.CreateScope();

        var player = await scope.LoadAsync<GameObject>("assets/player");
        var ui = await scope.LoadAsync<GameObject>("assets/ui-root");

        if (player != null) Instantiate(player, transform);
        if (ui != null) Instantiate(ui);

        // When the scope is disposed, it releases both
        // "assets/player" and "assets/ui-root".
    }
}

共有キーに対する参照カウントの振る舞い:

var go1 = await _assets.LoadAsync<GameObject>("assets/coin");  // reference count 1
var go2 = await _assets.LoadAsync<GameObject>("assets/coin");  // reference count 2, SAME asset
_assets.Release("assets/coin");  // reference count 1
_assets.Release("assets/coin");  // reference count 0 — the asset unloads

無効化する

ServiceLocator.Replace<IAssetProvider>(new NullAssetProvider());

これですべてのロードがミュートされます。LoadAsync<T>()default(T) を保持する完了済みタスクを返し、IsLoaded は常に falseLoadedCount は常に 0 です。Addressables には一切触れません。テストや、アセット機能を丸ごと無効にしたい場合に便利です。構築時に 1 回だけ警告をログするので(呼び出しごとではありません)、有効になっていることが分かります。

よくある落とし穴

  • 参照カウントが唯一のアンロードトリガーです。 メモリバジェットも LRU 追い出しもアンロードタイマーもありません。ロードして解放しなければ、アセットはメモリに残り続けます。リークの発見には LoadedCount を監視してください。
  • LoadAsync<T> はコンパイル時ジェネリックメソッドです。 IL2CPP ビルドでは、ロードするアセット型(GameObjectScriptableObjectSpriteTextAssetAudioClip)がプロジェクトの link.xml で保存されている必要があります。正確なエントリはパッケージ README の IL2CPP セクションにあります。
  • スコープは実行中のロードも捕捉します。 ロードの実行中にスコープが破棄された場合、そのロードは通常どおり完了した後、即座に解放されます。孤児になるものはありません。
  • 型の不一致は安全です。 あるキーを最初に LoadAsync<GameObject>("key") としてロードし、後から LoadAsync<Sprite>("key") を呼ぶと、2 回目の呼び出しは null を返して警告をログします — 例外にはなりません。参照カウントは型ではなくキー名に属します。
  • null や空のキーはコード側のバグとして扱われます。 LoadAsync(null)ArgumentNullException を、空白のみのキーは ArgumentException を投げます。これらはプログラミングエラーであり、決して例外を投げない上記のランタイムデータ障害とは異なります。
  • メインスレッド専用です。 ReleaseIsLoadedCreateScope、スコープの Dispose を含むすべての公開メンバーは、メインスレッドで実行しなければなりません。ワーカースレッドからは、スケジューラーPost() を使って呼び出しをメインスレッドに戻してください。

関連ページ