一、安装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 |
表示操作是否成功完成(Succeeded 、Failed 等) |
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}");
}
}
}
完整流程:
检查对象是否为 null。
将对象隐藏(SetActive false)。
检查是否重复回收,防止逻辑错误。
找到该对象对应的资源名。
将其加入对应的对象池,供下次复用。
如果没有找到资源名,报错提示。
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.获取场景数据,通过资源包名判断该场景资源是否加载;
已加载:
- 调用 loadSceneData.Recycle() 释放场景资源
- 从 m_LoadScene 字典中移除场景
2. 遍历该场景配置中需要预加载的场景 ID 列表,对每一个预加载场景调用 PreLoadScene(id)
,提前异步加载其资源;
3.调用 Resources.UnloadUnusedAssets();强制释放未被引用的资源内存;