mcpField
U
unity-scene-assets

unity-scene-assets

github.com/nice-wolf-studio开发4065 次安装6 周前更新
代码仓库

关于 unity-scene-assets

面向 Unity 项目的场景与资源管理模式技能,涵盖场景架构、Addressables 加载策略、加载界面和资源生命周期。帮助开发者处理异步加载、内存释放及从 Resources 迁移。

主要功能

  1. 01单场景、分关卡与叠加场景策略
  2. 02比较直接引用、Resources 与 Addressables
  3. 03按关卡、类型或频率组织 Addressables 组
  4. 04协调异步场景加载与进度界面
  5. 05管理预加载、延迟加载及句柄释放
  6. 06提供 Resources 迁移至 Addressables 的步骤

适用场景

  1. 01规划持久场景与游戏内容场景结构
  2. 02为关卡或动态内容选择资源加载方式
  3. 03实现带进度显示的场景切换
  4. 04排查 Addressables 资源释放与内存生命周期

安装

复制下面的命令,在终端中运行。

$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、忽略异步加载,也不考虑内存生命周期。


模式:场景架构策略

适用情况:为真实游戏(而非原型)规划项目场景结构

决策:

  • 单场景——游戏创作马拉松、原型、小型游戏。所有内容都在一个场景中。无需加载,也没有复杂性。但很快就会不够用。
  • 每关一个场景——线性进度(平台游戏、益智游戏)。在关卡之间调用 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 组策略

适用情况:将资源整理到 Addressable 组中,以便构建和加载

决策:

  • 按场景/关卡划分——适用于基于关卡的游戏。一起加载某关卡的所有资源,离开时卸载。内存生命周期清晰。
  • 按类型划分——将所有角色、VFX、音频分别放入不同组。适合在多个场景间共享的资源。
  • 按使用频率划分——核心(始终加载:UI、玩家、常见 VFX)、按需加载(敌人变体、关卡专属资源)、稀有(Boss、过场动画、季节性内容)。

脚手架(基于标签的加载):

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);
    }
}

注意:组太小、数量过多会增加目录开销(每个组都会产生 AssetBundle 元数据)。组太少、规模过大则会迫使你加载不需要的资源。大多数项目以 5–20 个组为目标。使用 Addressables Event Viewer(Window > Asset Management > Addressables > Event Viewer)进行性能分析,确认加载和卸载时机。


模式:加载画面协调

适用场景:在游戏章节之间切换并加载资源时

决策:

  • AsyncOperation.allowSceneActivation —— 简单的淡出至黑屏。加载至 90%,显示加载画面,并在准备就绪后激活。采用单场景方案。
  • Additive loading screen scene —— 将持久化的加载界面置于独立场景中。适用于带进度条和提示信息的复杂切换。

脚手架(带进度的场景切换):

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;
}

注意事项:当 allowSceneActivation = false 时,AsyncOperation.progress 的最大值为 0.9。对其进行归一化:Mathf.Clamp01(op.progress / 0.9f)。将 allowSceneActivation 设为 true 时,场景会立即激活——不会有额外延迟。最短加载时间应使用 Time.realtimeSinceStartup(而不是受 timeScale 影响的 Time.time)。


模式:资源生命周期协调

适用场景:确保资源在游戏开始前加载,并在不再需要时卸载

决策:

  • 进入场景时预加载 —— 在加载阶段加载所有必需资源,并在退出场景时释放。内存占用可预测,不会在运行时卡顿。最适合基于关卡的游戏。
  • 按需延迟加载 —— 首次需要时加载,缓存引用,并在退出场景时释放。初始加载时间更短,但首次使用时可能出现帧卡顿。最适合开放世界。

脚手架(带进度的资源预加载器):

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();
}

注意事项:如果已实例化的对象仍引用已加载的资源,此时释放 Addressable 句柄,轻则导致材质变成粉色或丢失,重则导致崩溃。释放资源句柄前,务必销毁所有实例。如果需要自动跟踪,请使用 Addressables.InstantiateAsync,而不是手动执行 LoadAssetAsync + Instantiate;之后使用 Addressables.ReleaseInstance 清理。


从 Resources 迁移到 Addressables 的检查清单

步骤操作
1安装 Addressables 包(com.unity.addressables)
2打开 Window > Asset Management > Addressables > Groups
3如果出现提示,则创建默认设置
4将资源从 Resources/ 文件夹移至常规资源文件夹
5将资源标记为 Addressable(勾选 Inspector 复选框或拖入组中)
6将 Resources.Load<T>("path") 替换为 Addressables.LoadAssetAsync<T>("path")
7添加句柄跟踪和 Addressables.Release() 调用
8将 Resources.LoadAll<T>() 替换为基于标签的 Addressables.LoadAssetsAsync
9删除空的 Resources/ 文件夹
10使用 Addressables 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 内存跟踪

其他资源

评论

支持 Markdown