mcpField
U
unity-scene-assets

unity-scene-assets

github.com/nice-wolf-studio開発4065 回インストール6 週間前に更新
リポジトリ

unity-scene-assets について

シーン構成、Addressables の読み込み方式、ローディング画面、アセットのライフサイクルを扱う Unity スキルです。非同期読み込みやメモリ解放、Resources からの移行を支援します。

主な機能

  1. 01単一、レベル別、アディティブなシーン構成
  2. 02直接参照、Resources、Addressables の比較
  3. 03レベル、種類、頻度別の Addressables グループ編成
  4. 04非同期シーン読み込みと進捗画面の調整
  5. 05プリロード、遅延読み込み、ハンドル解放の指針
  6. 06Resources から Addressables への移行手順

活用シーン

  1. 01永続シーンとゲーム内容シーンの構成計画
  2. 02レベルや動的コンテンツに適した読み込み方式の選択
  3. 03進捗表示付きシーン遷移の実装
  4. 04Addressables の解放とメモリ管理の問題調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-scene-assets

インストール後、Claude Code で/unity-scene-assetsを入力して読み込まれたか確認します。

同梱ファイルreferences/addressables-scene-patterns.md

詳細

シーンとアセットの管理 -- 判断パターン

前提となるスキル: unity-foundations/references/prefabs-and-scenes.md(SceneManager API、加算読み込み)、unity-async-patterns(Addressables のハンドルのライフサイクル、非同期読み込み)、unity-game-architecture(ブートストラップパターン)

これらのパターンでは、最もよくあるアセット管理の失敗に対処します。Claude が Resources.Load をハードコードし、非同期読み込みを無視して、メモリのライフサイクルを考慮しない問題です。


パターン:シーンアーキテクチャの戦略

適用場面:プロトタイプではなく、実際のゲームのシーンを構成するとき

判断:

  • 単一シーン -- ゲームジャム、プロトタイプ、小規模なゲーム向けです。すべてを1つのシーンにまとめます。読み込みも複雑さもありません。すぐに手狭になります。
  • レベルごとにシーンを分ける -- 直線的に進行するゲーム(プラットフォーマー、パズルゲーム)向けです。レベル間では LoadScene(name, LoadSceneMode.Single) を使います。明確に分離できますが、DontDestroyOnLoad を使わない限り共有状態はありません。
  • 加算シーン構成 -- オープンワールド、永続的な HUD、共有システム向けです。「Boot」シーンまたは「Persistent」シーンを読み込んだままにし、ゲームプレイや UI のシーンを加算読み込みします。最も柔軟ですが、最も複雑です。

ひな形(加算シーンコーディネーター):

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneCoordinator : MonoBehaviour
{
    [SerializeField] private string persistentSceneName = "Persistent";
    private string _currentContentScene;

    public static SceneCoordinator Instance { get; private set; }

    void Awake()
    {
        if (Instance != null) { Destroy(gameObject); return; }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void ResetStatic() => Instance = null;

    /// <summary>
    /// Load a content scene additively, unloading the previous one.
    /// The persistent scene stays loaded.
    /// </summary>
    public async Awaitable LoadContentScene(string sceneName)
    {
        // Unload previous content scene
        if (!string.IsNullOrEmpty(_currentContentScene))
        {
            await SceneManager.UnloadSceneAsync(_currentContentScene);
        }

        // Load new content scene
        await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
        _currentContentScene = sceneName;

        // Set active scene for lighting and new object spawning
        SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneName));
    }
}

注意点:SceneManager.SetActiveScene によって、どのシーンのライティング設定を適用するか、新しくインスタンス化されたオブジェクトをどこに配置するかが決まります。これを忘れると、オブジェクトが永続シーンに生成されてしまいます(ライトマップやナビメッシュが不適切になります)。永続シーンは、Build Settings のインデックス 0 に配置する必要があります。すべての加算シーンも Build Settings に追加する必要があります。


パターン:アセット読み込みの戦略

適用場面:実行時にアセットを読み込む方法を選ぶとき

判断:

  • 直接参照([SerializeField]) -- すべてのアセットを常にメモリに読み込んでおける小規模なプロジェクト向けです。最も簡単な方法です。アセットはシーンとともに読み込まれ、手動でライフサイクルを管理する必要はありません。
  • Resources.Load -- レガシーな方法です。新規プロジェクトでは避けてください。起動時に Resources フォルダー全体のインデックスが作成され(時間がかかります)、ビルドにも含まれます(サイズが膨らみます)。
  • Addressables -- 中規模から大規模のプロジェクト、動的コンテンツ、DLC、リモートアセット向けです。非同期で、参照カウント方式に対応し、ラベルを付けられます。ライフサイクルを学ぶ必要があります。

ひな形(AssetReference パターン):

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private AssetReference enemyPrefabRef; // Assign in Inspector

    private AsyncOperationHandle<GameObject> _handle;

    public async Awaitable<GameObject> SpawnEnemy(Vector3 position)
    {
        // Load the asset (ref-counted -- safe to call multiple times)
        _handle = enemyPrefabRef.LoadAssetAsync<GameObject>();
        var prefab = await _handle.Task;

        return Instantiate(prefab, position, Quaternion.identity);
    }

    void OnDestroy()
    {
        // Release when no longer needed
        if (_handle.IsValid())
            Addressables.Release(_handle);
    }
}

注意点:Resources.Load は同期処理であり、Resources フォルダー内のすべてのアセット(未使用のものも含む)をビルドに含めます。また、アンロードの戦略もありません。移行するには、Resources.Load<T>("path") を Addressables.LoadAssetAsync<T>("path") に置き換えます。Inspector の AssetReference フィールドを使うと、文字列キーを使わずに Addressable アセットを選択できます。文字列を使った読み込みより、こちらを優先してください。


パターン:Addressables グループの戦略

適用場面:ビルドと読み込みのために、アセットを Addressables グループに整理するとき

判断:

  • シーン/レベルごと -- レベルベースのゲーム向けです。レベルのアセットをまとめて読み込み、レベルを離れるときにアンロードします。メモリのライフサイクルを明確に管理できます。
  • 種類ごと -- キャラクター、VFX、オーディオをそれぞれ別のグループにまとめます。複数のシーンで共有されるアセットに適しています。
  • 使用頻度ごと -- コア(常に読み込み:UI、プレイヤー、共通 VFX)、オンデマンド(敵のバリエーション、レベル固有)、低頻度(ボス、カットシーン、季節限定)。

ひな形(ラベルベースの読み込み):

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Collections.Generic;

public class LevelAssetLoader : MonoBehaviour
{
    private AsyncOperationHandle<IList<GameObject>> _levelAssetsHandle;

    public async Awaitable PreloadLevel(string levelLabel)
    {
        // Load all assets tagged with the level label
        _levelAssetsHandle = Addressables.LoadAssetsAsync<GameObject>(
            levelLabel,
            asset => Debug.Log($"Loaded: {asset.name}")
        );
        await _levelAssetsHandle.Task;
    }

    public void UnloadLevel()
    {
        if (_levelAssetsHandle.IsValid())
            Addressables.Release(_levelAssetsHandle);
    }
}

注意点:小さなグループを作りすぎると、カタログのオーバーヘッドが増えます(グループごとにバンドルのメタデータが必要です)。大きなグループが少なすぎると、不要なアセットまで読み込むことになります。ほとんどのプロジェクトでは、5~20 グループを目安にしてください。Addressables Event Viewer(Window > Asset Management > Addressables > Event Viewer)でプロファイリングし、読み込みとアンロードのタイミングを確認してください。


パターン:ロード画面の連携

WHEN:アセットのロードを伴うゲームプレイセクション間の遷移

DECISION:

  • AsyncOperation.allowSceneActivation -- シンプルなフェードアウト。90%までロードし、ロード画面を表示して、準備ができたら有効化します。単一シーンでのアプローチです。
  • 加算ロード方式のロード画面シーン -- 専用のシーンに永続的なロード UI を配置します。プログレスバーやヒントを表示する複雑な遷移に適しています。

SCAFFOLD(進捗状況を表示するシーン遷移):

public class SceneTransition : MonoBehaviour
{
    [SerializeField] private float minimumLoadScreenTime = 1f; // Prevent flash

    public async Awaitable TransitionTo(string sceneName, System.Action<float> onProgress = null)
    {
        // Show loading screen
        await SceneManager.LoadSceneAsync("LoadingScreen", LoadSceneMode.Additive);

        float startTime = Time.realtimeSinceStartup;

        // Begin loading target scene (paused at 90%)
        var loadOp = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
        loadOp.allowSceneActivation = false;

        // Unload current gameplay scene in parallel
        if (!string.IsNullOrEmpty(_currentContentScene))
        {
            var unloadOp = SceneManager.UnloadSceneAsync(_currentContentScene);
            while (!unloadOp.isDone)
            {
                await Awaitable.NextFrameAsync(destroyCancellationToken);
            }
        }

        // Wait for target to reach 90% (ready to activate)
        while (loadOp.progress < 0.9f)
        {
            // Normalize: AsyncOperation.progress maxes at 0.9 when allowSceneActivation=false
            float normalizedProgress = Mathf.Clamp01(loadOp.progress / 0.9f);
            onProgress?.Invoke(normalizedProgress);
            await Awaitable.NextFrameAsync(destroyCancellationToken);
        }

        // Enforce minimum display time
        float elapsed = Time.realtimeSinceStartup - startTime;
        if (elapsed < minimumLoadScreenTime)
        {
            await Awaitable.WaitForSecondsAsync(
                minimumLoadScreenTime - elapsed, destroyCancellationToken);
        }

        onProgress?.Invoke(1f);

        // Activate the scene
        loadOp.allowSceneActivation = true;
        while (!loadOp.isDone)
            await Awaitable.NextFrameAsync(destroyCancellationToken);

        _currentContentScene = sceneName;
        SceneManager.SetActiveScene(SceneManager.GetSceneByName(sceneName));

        // Unload loading screen
        await SceneManager.UnloadSceneAsync("LoadingScreen");
    }

    private string _currentContentScene;
}

GOTCHA:allowSceneActivation = false の場合、AsyncOperation.progress の最大値は 0.9 です。正規化するには、Mathf.Clamp01(op.progress / 0.9f) を使用してください。allowSceneActivation を true に設定すると、シーンは直ちに有効化されます。追加の遅延はありません。最小ロード時間には Time.time ではなく Time.realtimeSinceStartup を使用してください(Time.time は timeScale の影響を受けます)。


パターン:アセットのライフサイクル連携

WHEN:ゲームプレイ開始前にアセットをロードし、不要になったらアンロードする場合

DECISION:

  • シーンに入るときにプリロード -- ロードフェーズですべての必要なアセットをロードし、シーンを出るときに解放します。メモリ使用量を予測しやすく、実行時の引っかかりもありません。レベルベースのゲームに最適です。
  • 必要に応じて遅延ロード -- 初めて必要になったときにロードし、参照をキャッシュして、シーンを出るときに解放します。初期ロード時間は短縮できますが、初回使用時にフレームの引っかかりが発生する可能性があります。オープンワールドに最適です。

SCAFFOLD(進捗状況を表示するアセットプリローダー):

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using System.Collections.Generic;

public class AssetPreloader : MonoBehaviour
{
    private readonly List<AsyncOperationHandle> _handles = new();

    /// <summary>
    /// Preload a list of Addressable keys. Reports progress 0-1.
    /// </summary>
    public async Awaitable Preload(
        IList<string> keys,
        System.Action<float> onProgress = null,
        CancellationToken token = default)
    {
        int loaded = 0;
        int total = keys.Count;

        foreach (string key in keys)
        {
            token.ThrowIfCancellationRequested();

            var handle = Addressables.LoadAssetAsync<Object>(key);
            _handles.Add(handle);
            await handle.Task;

            loaded++;
            onProgress?.Invoke((float)loaded / total);
        }
    }

    /// <summary>Release all preloaded assets.</summary>
    public void ReleaseAll()
    {
        foreach (var handle in _handles)
        {
            if (handle.IsValid())
                Addressables.Release(handle);
        }
        _handles.Clear();
    }

    void OnDestroy() => ReleaseAll();
}

GOTCHA:インスタンス化されたオブジェクトがロード済みアセットを参照している状態で Addressable ハンドルを解放すると、マテリアルがピンク色になる、または見つからなくなることがあります。最悪の場合はクラッシュします。アセットハンドルを解放する前に、必ずすべてのインスタンスを破棄してください。自動追跡を行う場合は、手動で LoadAssetAsync + Instantiate を使用する代わりに Addressables.InstantiateAsync を使用し、クリーンアップには Addressables.ReleaseInstance を使用してください。


Resources から Addressables への移行チェックリスト

手順アクション
1Addressables パッケージ(com.unity.addressables)をインストールする
2Window > Asset Management > Addressables > Groups を開く
3プロンプトが表示されたら、デフォルト設定を作成する
4Resources/ フォルダーから通常のアセットフォルダーにアセットを移動する
5アセットを Addressable としてマークする(Inspector のチェックボックス、またはグループにドラッグする)
6Resources.Load<T>("path") を Addressables.LoadAssetAsync<T>("path") に置き換える
7ハンドルの追跡と Addressables.Release() の呼び出しを追加する
8Resources.LoadAll<T>() をラベルベースの Addressables.LoadAssetsAsync に置き換える
9空の Resources/ フォルダーを削除する
10Addressables Event Viewer でテストし、リークがないことを確認する

Resources に残すもの: エディター専用アセット、テスト用フィクスチャ、Addressables の初期化前に必要なアセット(スプラッシュ画面)。

関連スキル

  • unity-foundations/references/prefabs-and-scenes.md -- SceneManager API、加算ロード(重複を避けてください)
  • unity-async-patterns -- Addressables AsyncOperationHandle のライフサイクル、非同期キャンセル(重複を避けてください)
  • unity-game-architecture -- ブートシーンの初期化、シーン管理用の Service Locator
  • unity-performance -- メモリプロファイリング、Addressables のメモリ追跡

追加リソース

コメント