unity加载资源学习笔记

发布于:2025-06-07 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、安装Addressables包

1.在 Unity Package Manager 中添加:Addressables

2. 创建 Addressables 设置

  • 第一次使用时,点击菜单 Window > Asset Management > Addressables > Groups

  • 点击Create Addressables Settings按钮,系统会自动创建 AddressableAssetSettings 文件夹和默认的 Default Local Group

二、如何加载 Addressables Groups 下的资源

Addressables Groups 窗口

Unity 的 Addressables Groups 窗口,用于配置和管理通过 Addressables 系统加载的资源。

作用
Group Name / Addressable Name 资源所属的 Group 名称及 Addressable Key 名(用于加载)
Path 资源在工程中的物理路径
Labels 标签,可用于分类筛选和批量加载

1.Group(资源组):每个 Group 是一个逻辑资源包,打包时会生成一个 AssetBundle,Group 内的资源可以被打包在一起、独立加载或标记为远程资源。

2.Path(物理路径):表示资源在工程中的实际文件路径。虽然我们用 Addressables 加载,但底层其实还是引用这个资源。

3.Labels(标签):是给资源打的标签(Label),可以在代码中用标签来批量加载资源。

大致步骤:

Addressables 初始化→分类加载资源→资源异步加载与进度回调→ 资源预热与缓存→资源获取与对象池管理

步骤一:Addressables 初始化

Addressables 需要初始化,才能正确管理资源定位、缓存、异步加载等功能。初始化后才能进行后续的资源加载操作。

private void InitializeAddressables()
{
    string cachePath = Path.Combine(Application.persistentDataPath, CACHE_PATH);
    LogCachePath(cachePath);

    Addressables.ClearResourceLocators();
    Addressables.InitializeAsync().Completed += handle =>
    {
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            StartCoroutine(LoadResources());
        }
        else
        {
            HandleLoadingError("Failed to initialize Addressables");
        }
    };
}

代码分析:

1.Addressables.ClearResourceLocators();

清除 Addressables 资源定位器(Locator),重新初始化 Addressables 前的清理步骤,确保没有残留的资源定位配置。

2.Addressables.InitializeAsync().Completed += handle =>

Unity Addressables 初始化是异步的,这一步会:

  • 加载资源定位表(catalog)

  • 连接远程/本地的资源清单

  • 准备好资源加载系统完成

这里的 handle 是异步初始化操作的完成句柄(回调参数),它的类型是:

 AsyncOperationHandle<IResourceLocator>

3.这个 handle 是 Unity Addressables 初始化过程的“结果封装”,可以通过它获取以下信息

成员 作用
handle.Status 表示操作是否成功完成(SucceededFailed 等)
handle.Result 如果成功,这里是 IResourceLocator,可用于资源定位
handle.OperationException 如果失败,这里是异常信息
handle.IsDone 是否完成(同步/异步都适用)
handle.PercentComplete 加载进度(0 到 1)

这里通过handle.Status来判断是否初始化成功。

步骤二:分批加载资源

2.1 资源加载主流程

private IEnumerator LoadResources()
        {
            yield return LoadResourceCategory("Fonts", "Loading fonts...");
            yield return LoadResourceCategory("Configs", "Loading configurations...");
            yield return LoadResourceCategory("FairyGUI", "Loading UI resources...");
            yield return LoadResourceCategory("Pages", "Loading pages...");
            yield return LoadResourceCategory("Map_Common", "Loading map resources...");

            
            MyLogger.Info($"DataMgr.Inst.Init...");
            DataMgr.Inst.Init();
            MyLogger.Info("DataMgr initialized");
            DataMgr.Inst.InitTables();
            try
            {
                LoadFirstScene();
            }
            catch (Exception e)
            {
                HandleLoadingError($"Resource loading failed: {e.Message}");
            }
        }

分析:

  • 分批加载:每一类资源(如字体、配置、UI、地图等)单独加载,便于管理和进度反馈。
  • 协程串行:用 yield return 确保每类资源完成再加载下一类。

2.2 单类资源加载细节

根据标签批量加载一类资源,并在加载完成后执行预热操作

  private IEnumerator LoadResourceCategory(string category, string loadingMessage)
        {
            var resources = new Dictionary<string, UnityEngine.Object>();
            SetDescription(loadingMessage);
            
            yield return ResMgr.Inst.LoadAssetsWithProgress<UnityEngine.Object>(category, resources, () =>
            {
                foreach (var resource in resources)
                {
                    ResMgr.Inst.RunTimeWarmUp(resource.Key, resource.Value);
                }
                SetDescription($"Finished loading {category}");
            }, _SetProgress);
        }

分析:

  • 显示进度描述:SetDescription(loadingMessage) 更新 UI,提示当前加载内容。
  • 调用资源管理器加载:通过 ResMgr.Inst.LoadAssetsWithProgress 异步加载该类别下所有资源。
  • 资源预热:加载完成后,遍历所有资源,调用 RunTimeWarmUp 缓存到资源管理器。
  • 进度条更新:每次加载都会回调 _SetProgress,实时更新进度条。

步骤三:异步加载与进度回调

3.1 Addressables 异步加载

public IEnumerator LoadAssetsWithProgress<T>(string key, IDictionary<string, T> dic, Action callback, Action<float> ProgressCallback) where T : UnityEngine.Object
{
    AsyncOperationHandle<IList<T>> handle = Addressables.LoadAssetsAsync<T>(key,
        (obj) =>
        {
            if (obj != null && !dic.ContainsKey(obj.name))
                dic.Add(obj.name, obj);
        });

    while (!handle.IsDone)
    {
        ProgressCallback?.Invoke(handle.PercentComplete);
        yield return null;
    }

    ProgressCallback?.Invoke(1.0f);
    yield return handle;

    if (handle.OperationException != null)
        Debug.LogError("HandleException:" + handle.OperationException.Message);

    _GetLoadScene(key).CacheHandler(handle);
    callback?.Invoke();
}

代码解析:

  • 异步加载所有符合 key 的资源。

  • 每加载到一个资源,都会调用 lambda 回调,把其加入到 dic 中(避免重复)。

  • 每帧回调 ProgressCallback,实时反馈加载进度。

  • 将加载句柄缓存,便于后续释放。

步骤四:资源预热与缓存

4.1 资源缓存机制

public void RunTimeWarmUp(string path, UnityEngine.Object resource) 
{
    if(resource == null)
    {
        MyLogger.Error($"RunTimeWarmUp failed {path}");
        return;
    }
    
    path = path.Trim();
    if (path.Contains('/'))
    {
        path = path.Substring(path.LastIndexOf('/') + 1);
    }
    
    if (!m_Resources.ContainsKey(path))
    {
        m_Resources[path] = resource;
        MyLogger.Debug($"load {path}, {resource}, Count = {m_Resources.Count}");
    }
    else
    {
        MyLogger.Error($"{path} already exist");
    }
}

代码解析

  • 去除路径前缀:只保留资源名,避免路径不同但资源相同导致重复缓存。
  • 资源字典缓存:用 m_Resources 字典缓存所有已加载资源,key 为资源名,value 为资源对象。
  • 重复检测:如果已存在同名资源,报错提示。

步骤五:资源获取与对象池管理

5.1 资源获取接口

public T GetResource<T>(string name) where T : UnityEngine.Object
{
    if (name.Contains('/'))
    {
        name = name.Substring(name.LastIndexOf('/') + 1);
    }

    name = name.Trim();
    if (!m_Resources.TryGetValue(name, out var resource))
    {
        MyLogger.Error($"GetResource failed {name}");
        return null;
    }

    return resource as T;
}

代码解析:

  • 这是一个泛型方法,可以返回任何类型 T 的 Unity 资源(比如 GameObject, Texture, AudioClip 等)。
  • where T : UnityEngine.Object 表示 T 必须是 Unity 引用类型资源。
  • 去除前后空白字符,避免因为误输入空格导致查找失败。
  • 将资源强制转换为指定的类型 T 并返回。
  • 如果实际类型不匹配,返回 null。

三、切换场景时资源加载流程

以切换副本场景为例

 public void Change2Fuben(int sceneId, Action callBack, Action<float> progressCallback)
        {
            //记录副本id
            DataMgr.Inst.ReFubenSceneId(sceneId);
            
            var sceneData = DataMgr.Inst.m_ExcelData.GetSceneData(sceneId); // 从配置表中读取场景数据
            ResMgr.Inst.LoadScene(sceneData.AssetPkgName, () =>
            {
                var curScene = CacheObjectMgr.Inst.Fetch<FubenScene>();// 从对象池获取一个副本场景对象(FubenScene)
                curScene.InitData(sceneData.PrefabName, sceneData.SceneType);// 初始化该场景数据(传入 prefab 名和类型)
                ChangeScene(curScene); // 切换到该场景
                // 输出日志,标记切换成功
                MyLogger.Debug($"副本事件mapName = {sceneData.PrefabName}, sceneId = {sceneId}");

                foreach (var id in sceneData.PreloadSceneId)
                {
                    ResMgr.Inst.PreLoadScene(id);
                }
                
                callBack?.Invoke();
            }, progressCallback);
        }

切换场景的大致步骤为:从配置表中取场景信息(sceneData)→ResMgr 异步加载资源包→加载完成后:切换场景和预加载关联场景。

1.异步加载资源包

通过LoadScene启动协程 _LoadSceneImp

  public void LoadScene(string mapName, Action endCallBack, Action<float> progressCallback)
        {
            StartCoroutine(_LoadSceneImp(mapName, endCallBack, progressCallback));
        }

 public IEnumerator _LoadSceneImp(string mapName, Action callBack, Action<float> progressCallback)
        { 
            var list = _GetLoadScene(mapName);
            if (list.HasLoad())
            {
                callBack?.Invoke();
                progressCallback?.Invoke(1.0f);
                MyLogger.Debug($"has load {mapName}");
                yield break;
            }

            MyLogger.Info("load Map_MainSence begin");

            var dic = new Dictionary<string, UnityEngine.Object>();
            yield return LoadAssetsWithProgress<UnityEngine.Object>(mapName, dic, ()=>
            {
                foreach (var kv in dic)
                {
                    list.Add(kv.Key);
                    RunTimeWarmUp(kv.Key, kv.Value);// 预热资源
                }
                MyLogger.Info("load Map_MainSence end");
                
                callBack.Invoke();

            }, progressCallback);
        }

对于_LoadSceneImp函数:

1.通过addressable的label标签来获取场景资源list,再判断该场景资源是否已经加载过

2.已经加载:

  • 立即调用 callBack 通知加载完成。

  • 把进度设置为 100%。

  • 使用 yield break 终止协程(不再重复加载资源,避免浪费)。

3.未加载

  • 创建存放资源的临时字典

  • 通过 LoadAssetsWithProgress 异步加载资源

  • 加载完成后的遍历临时字典,用于记录加载的资源名将加载的资源预热

2.预加载关联场景

 public void PreLoadScene(int sceneId)
        {
            var sceneData = DataMgr.Inst.m_ExcelData.GetSceneData(sceneId); // 从数据表中获取主线地图的场景数据
            MyLogger.Info($"Pre LoadScene {sceneData.AssetPkgName} begin");
            StartCoroutine(_LoadSceneImp(sceneData.AssetPkgName));
        }

        
  public IEnumerator _LoadSceneImp(string mapName)
        {
            var list = _GetLoadScene(mapName);
            if (list.HasLoad())
            {
                MyLogger.Info($"has load {mapName}");
                yield break;
            }
            
            MyLogger.Info($"LoadScene {mapName} begin");
            var dic = new Dictionary<string, UnityEngine.Object>();
            yield return LoadAssetsWithProgress<UnityEngine.Object>(mapName, dic, ()=>
            {
                foreach (var kv in dic)
                {
                    list.Add(kv.Key);
                    RunTimeWarmUp(kv.Key, kv.Value);// 预热资源
                }
                MyLogger.Info($"load {mapName} end");

            }, null);
        }

通过 PreLoadScene 启动一个协程_LoadSceneImp异步加载指定场景的资源,_LoadSceneImp通过addressable的label标签来获取场景资源list,再判断该场景资源是否已经加载过

2.已经加载:

  • 使用 yield break 终止协程(不再重复加载资源,避免浪费)。

3.未加载

  • 创建存放资源的临时字典。

  • 通过 LoadAssetsWithProgress 异步加载资源。

  • 加载完成后的遍历临时字典,用于记录加载的资源名将加载的资源预热。

四、资源回收

1.回收 GameObject 类型的资源

  public void Recyle(GameObject go)
        {
            if (go != null)
            {
                go.FastSetActive(false);

                if (m_HasRecycle.Contains(go))
                {
                    MyLogger.Error($"go {go.name} has already been recycled");
                    return;
                }

                m_HasRecycle.Add(go);

                if (m_Id2Name.TryGetValue(go.GetInstanceID(), out string name))
                {
                    _GetPool(name).Enqueue(go);
                }
                else
                {
                    MyLogger.Error($"Recyle error, no such {go.name}");
                }
            }
        }

完整流程:

  1. 检查对象是否为 null。

  2. 将对象隐藏(SetActive false)。

  3. 检查是否重复回收,防止逻辑错误。

  4. 找到该对象对应的资源名。

  5. 将其加入对应的对象池,供下次复用。

  6. 如果没有找到资源名,报错提示。

2.释放场景资源

public void ReleaseScene(int sceneId)
{
    var sceneData = DataMgr.Inst.m_ExcelData.GetSceneData(sceneId);
    MyLogger.Info($"Release Scene {sceneData.AssetPkgName} begin");
    
    if (m_LoadScene.TryGetValue(sceneData.AssetPkgName, out var loadSceneData))
    {
        loadSceneData.Recycle();
        m_LoadScene.Remove(sceneData.AssetPkgName);
        MyLogger.Info($"Release Scene {sceneData.AssetPkgName} end");
    }
    else
    {
        MyLogger.Debug($"Scene {sceneData.AssetPkgName} not loaded, nothing to release");
    }
    
    foreach (var id in sceneData.PreloadSceneId)
    {
        PreLoadScene(id);
    }
   
    Resources.UnloadUnusedAssets();
}

1.获取场景数据,通过资源包名判断该场景资源是否加载;

已加载:

  1. 调用 loadSceneData.Recycle() 释放场景资源
  2. 从 m_LoadScene 字典中移除场景

2. 遍历该场景配置中需要预加载的场景 ID 列表,对每一个预加载场景调用 PreLoadScene(id),提前异步加载其资源;

3.调用 Resources.UnloadUnusedAssets();强制释放未被引用的资源内存;


网站公告

今日签到

点亮在社区的每一天
去签到