跳到主要内容

资源管理

Unity 的资源管理是游戏开发中至关重要的环节,直接影响游戏的性能、内存占用和用户体验。本章将深入讲解 Prefab、AssetBundle 和 Addressables 三种主要的资源管理方式,帮助你选择最适合项目需求的方案。

Prefab 预制体

Prefab(预制体)是 Unity 中最基础的资源复用机制,它允许你创建、配置和存储游戏对象及其所有组件、属性值和子游戏对象,作为可重复使用的资源。

Prefab 概念

Prefab 本质上是一个蓝图或模板,存储在 Project 窗口中。当你在场景中创建一个 Prefab 实例时,Unity 会创建一个指向 Prefab 源的引用链接。这意味着:

  • 修改 Prefab 源文件,所有场景中的实例会自动更新
  • 修改实例属性,可以覆盖 Prefab 的默认值而不影响其他实例
  • 嵌套 Prefab 支持在 Prefab 中包含其他 Prefab,实现更复杂的复用

创建 Prefab

方式1:从场景创建

  1. 在 Hierarchy 中配置好游戏对象
  2. 将对象拖拽到 Project 窗口的文件夹中
  3. 原对象变为蓝色,表示已成为 Prefab 实例

方式2:创建空 Prefab

  1. 在 Project 窗口右键 > Create > Prefab
  2. 双击打开 Prefab 编辑模式
  3. 添加组件和子对象

Prefab 实例与变体

using UnityEngine;

public class PrefabExample : MonoBehaviour
{
public GameObject enemyPrefab;

void Start()
{
// 实例化 Prefab
GameObject enemy1 = Instantiate(enemyPrefab, transform.position, Quaternion.identity);

// 设置实例名称
enemy1.name = "Enemy_01";

// 修改实例属性(不影响 Prefab 源)
enemy1.transform.localScale = new Vector3(2f, 2f, 2f);

// 获取实例上的组件
var health = enemy1.GetComponent<HealthSystem>();
health.maxHealth = 200; // 覆盖默认值
}
}

Prefab 变体

Prefab 变体允许你基于现有 Prefab 创建新版本,继承父 Prefab 的所有属性:

Enemy (基础 Prefab)
├── Enemy_Fast (变体:速度更快)
├── Enemy_Tank (变体:血量更高)
└── Enemy_Flying (变体:可以飞行)

创建变体

  1. 在 Project 窗口选择源 Prefab
  2. 右键 > Create > Prefab Variant
  3. 修改变体的属性

运行时实例化

public class Spawner : MonoBehaviour
{
public GameObject prefab;
public int spawnCount = 10;
public float spawnRadius = 5f;
public float spawnInterval = 1f;

void Start()
{
StartCoroutine(SpawnRoutine());
}

IEnumerator SpawnRoutine()
{
for (int i = 0; i < spawnCount; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(spawnInterval);
}
}

void SpawnEnemy()
{
// 在随机位置生成
Vector2 randomCircle = Random.insideUnitCircle * spawnRadius;
Vector3 spawnPosition = transform.position + new Vector3(randomCircle.x, 0, randomCircle.y);

// 实例化
GameObject instance = Instantiate(prefab, spawnPosition, Quaternion.identity);

// 可选:设置父对象
instance.transform.SetParent(transform);
}
}

Prefab 实用技巧

1. Prefab 模式

双击 Prefab 进入 Prefab 模式,可以独立编辑 Prefab 而不影响场景:

  • 编辑模式:修改 Prefab 源文件
  • 覆盖管理:在 Inspector 中查看和恢复实例的属性覆盖
  • 嵌套编辑:可以编辑嵌套的子 Prefab

2. Prefab 覆盖

当修改实例属性时,会创建覆盖(Override):

// 检查实例是否有覆盖
GameObject prefabInstance; // 假设已赋值

// 查看修改的属性
var modifications = PrefabUtility.GetPropertyModifications(prefabInstance);
foreach (var mod in modifications)
{
Debug.Log($"属性: {mod.propertyPath}, 值: {mod.value}");
}

// 重置实例到 Prefab 状态
PrefabUtility.RevertObjectOverride(prefabInstance, InteractionMode.UserAction);

// 应用修改到 Prefab
PrefabUtility.ApplyObjectOverride(prefabInstance, prefabPath, InteractionMode.UserAction);

3. 运行时动态加载 Prefab

using UnityEngine;

public class ResourceLoader : MonoBehaviour
{
// 方式1:使用 Resources 文件夹(不推荐大型项目)
public void LoadFromResources()
{
GameObject prefab = Resources.Load<GameObject>("Prefabs/Enemy");
Instantiate(prefab);
}

// 方式2:使用 AssetDatabase(仅编辑器)
#if UNITY_EDITOR
public void LoadFromAssetDatabase()
{
GameObject prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(
"Assets/Prefabs/Enemy.prefab"
);
Instantiate(prefab);
}
#endif
}

AssetBundle

AssetBundle 是 Unity 的资源打包和分发系统,允许将资源打包成独立的文件,在运行时动态加载。相比 Resources 文件夹,AssetBundle 提供了更好的内存管理和更灵活的资源分发方式。

为什么使用 AssetBundle?

特性ResourcesAssetBundle
内存管理启动时加载索引,无法卸载可以精确控制加载和卸载
资源更新必须更新整个应用可独立更新资源包
初始包大小增加应用体积可作为 DLC 下载
平台适配所有平台共用可针对不同平台打包

创建 AssetBundle

1. 设置 AssetBundle 名称

在 Inspector 底部设置资源的 AssetBundle 名称和变体:

AssetBundle 名称格式: lower_case_with_underscores
例如: characters/player
变体: hd, sd (可选)

2. 构建 AssetBundle

using UnityEngine;
using UnityEditor;
using System.IO;

public class AssetBundleBuilder
{
[MenuItem("Build/Build AssetBundles")]
static void BuildAllAssetBundles()
{
// 输出路径
string assetBundleDirectory = "Assets/AssetBundles";

// 创建目录
if (!Directory.Exists(assetBundleDirectory))
{
Directory.CreateDirectory(assetBundleDirectory);
}

// 构建 AssetBundle
BuildPipeline.BuildAssetBundles(
assetBundleDirectory,
BuildAssetBundleOptions.None,
BuildTarget.StandaloneWindows64
);

Debug.Log("AssetBundles 构建完成");
}
}

3. 构建选项

// 常用构建选项
BuildAssetBundleOptions options =
BuildAssetBundleOptions.None | // 默认选项
BuildAssetBundleOptions.ChunkBasedCompression | // LZ4 压缩
BuildAssetBundleOptions.DeterministicAssetBundle; // 确定性构建

// 不同压缩方式
// None: 无压缩,最大,加载最快
// ChunkBasedCompression: LZ4 压缩,平衡大小和速度(推荐)
// DisableWriteTypeTree: 不写入类型树,减小文件大小但影响兼容性

加载 AssetBundle

同步加载

using UnityEngine;
using System.IO;

public class AssetBundleLoader : MonoBehaviour
{
public string bundleName = "characters";
public string assetName = "Player";

void Start()
{
LoadAssetSync();
}

void LoadAssetSync()
{
// AssetBundle 路径
string path = Path.Combine(Application.streamingAssetsPath, bundleName);

// 加载 AssetBundle
AssetBundle bundle = AssetBundle.LoadFromFile(path);

if (bundle == null)
{
Debug.LogError($"无法加载 AssetBundle: {bundleName}");
return;
}

// 加载资源
GameObject prefab = bundle.LoadAsset<GameObject>(assetName);

if (prefab != null)
{
Instantiate(prefab);
}

// 卸载 AssetBundle(但不卸载已加载的资源)
bundle.Unload(false);
}
}

异步加载

public class AsyncAssetBundleLoader : MonoBehaviour
{
public string bundleName;
public string assetName;

void Start()
{
StartCoroutine(LoadAssetAsync());
}

IEnumerator LoadAssetAsync()
{
string path = Path.Combine(Application.streamingAssetsPath, bundleName);

// 异步加载 AssetBundle
AssetBundleCreateRequest bundleRequest = AssetBundle.LoadFromFileAsync(path);

yield return bundleRequest;

AssetBundle bundle = bundleRequest.assetBundle;

if (bundle == null)
{
Debug.LogError("加载 AssetBundle 失败");
yield break;
}

// 异步加载资源
AssetBundleRequest assetRequest = bundle.LoadAssetAsync<GameObject>(assetName);

yield return assetRequest;

if (assetRequest.asset != null)
{
Instantiate(assetRequest.asset);
}

// 卸载
bundle.Unload(false);
}
}

从服务器加载

using UnityEngine.Networking;

public class RemoteAssetBundleLoader : MonoBehaviour
{
public string url = "https://your-server.com/assetbundles/characters";
public string assetName;

void Start()
{
StartCoroutine(LoadFromServer());
}

IEnumerator LoadFromServer()
{
// 从 URL 加载 AssetBundle
using (UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url))
{
yield return request.SendWebRequest();

if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"下载失败: {request.error}");
yield break;
}

AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);

if (bundle != null)
{
GameObject prefab = bundle.LoadAsset<GameObject>(assetName);
Instantiate(prefab);
bundle.Unload(false);
}
}
}
}

依赖管理

AssetBundle 之间的依赖关系需要手动管理:

using UnityEngine;

public class DependencyLoader : MonoBehaviour
{
void LoadWithDependencies()
{
// 加载依赖清单
AssetBundle manifestBundle = AssetBundle.LoadFromFile(
Path.Combine(Application.streamingAssetsPath, "StreamingAssets")
);

AssetBundleManifest manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

// 获取依赖
string[] dependencies = manifest.GetAllDependencies("characters");

// 先加载依赖
foreach (string dependency in dependencies)
{
string path = Path.Combine(Application.streamingAssetsPath, dependency);
AssetBundle.LoadFromFile(path);
}

// 然后加载主 AssetBundle
AssetBundle mainBundle = AssetBundle.LoadFromFile(
Path.Combine(Application.streamingAssetsPath, "characters")
);

// 加载资源
GameObject prefab = mainBundle.LoadAsset<GameObject>("Player");
Instantiate(prefab);
}
}

AssetBundle 内存管理

public class AssetBundleMemoryManager : MonoBehaviour
{
private Dictionary<string, AssetBundle> loadedBundles = new Dictionary<string, AssetBundle>();
private Dictionary<string, GameObject> loadedAssets = new Dictionary<string, GameObject>();

// 加载并缓存
public IEnumerator LoadAndCache(string bundleName, string assetName)
{
if (loadedBundles.ContainsKey(bundleName))
{
yield break; // 已加载
}

string path = Path.Combine(Application.streamingAssetsPath, bundleName);
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);

yield return request;

if (request.assetBundle != null)
{
loadedBundles[bundleName] = request.assetBundle;

AssetBundleRequest assetRequest = request.assetBundle.LoadAssetAsync<GameObject>(assetName);
yield return assetRequest;

if (assetRequest.asset != null)
{
loadedAssets[assetName] = assetRequest.asset as GameObject;
}
}
}

// 卸载指定 AssetBundle
public void UnloadBundle(string bundleName, bool unloadAllLoadedObjects = false)
{
if (loadedBundles.TryGetValue(bundleName, out AssetBundle bundle))
{
bundle.Unload(unloadAllLoadedObjects);
loadedBundles.Remove(bundleName);
}
}

// 卸载所有
public void UnloadAllBundles()
{
foreach (var bundle in loadedBundles.Values)
{
bundle.Unload(true);
}

loadedBundles.Clear();
loadedAssets.Clear();

// 强制垃圾回收
Resources.UnloadUnusedAssets();
System.GC.Collect();
}
}

Addressables 资源系统

Addressables 是 Unity 推荐的现代资源管理系统,它在 AssetBundle 之上提供了更高级的抽象,简化了资源加载、内存管理和依赖处理。

Addressables 的优势

  • 简化加载:通过地址(Address)加载资源,无需关心物理路径
  • 自动依赖管理:自动加载所有依赖资源
  • 灵活的托管位置:资源可以放在本地、远程服务器或 CDN
  • 内存管理:自动引用计数,智能卸载
  • 编辑器集成:可视化工具管理资源分组

安装 Addressables

  1. 打开 Window > Package Manager
  2. 选择 Unity Registry
  3. 搜索 Addressables
  4. 点击 Install

标记资源为 Addressable

方式1:Inspector

  1. 选择资源
  2. 在 Inspector 中勾选 Addressable
  3. 设置地址名称(默认为资源路径)

方式2:Window > Asset Management > Addressables > Groups

  1. 打开 Addressables Groups 窗口
  2. 拖拽资源到窗口中
  3. 设置标签和分组

基本加载方式

异步加载资源

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

public class AddressablesLoader : MonoBehaviour
{
// 地址可以是字符串或 AssetReference
public string assetAddress = "Assets/Prefabs/Player.prefab";

private GameObject loadedAsset;

void Start()
{
StartCoroutine(LoadAssetCoroutine());
}

IEnumerator LoadAssetCoroutine()
{
// 开始加载
AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>(assetAddress);

// 等待加载完成
yield return handle;

// 检查结果
if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedAsset = handle.Result;
Instantiate(loadedAsset);
}
else
{
Debug.LogError($"加载失败: {handle.OperationException}");
}
}

void OnDestroy()
{
// 释放资源
if (loadedAsset != null)
{
Addressables.Release(loadedAsset);
}
}
}

使用回调方式

public class AddressablesCallback : MonoBehaviour
{
public string assetAddress;

private AsyncOperationHandle<GameObject> handle;

void Start()
{
handle = Addressables.LoadAssetAsync<GameObject>(assetAddress);
handle.Completed += OnAssetLoaded;
}

void OnAssetLoaded(AsyncOperationHandle<GameObject> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(handle.Result);
}
else
{
Debug.LogError("加载失败");
}
}

void OnDestroy()
{
if (handle.IsValid())
{
Addressables.Release(handle);
}
}
}

使用 async/await

using System.Threading.Tasks;

public class AddressablesAsync : MonoBehaviour
{
public string assetAddress;

async void Start()
{
await LoadAssetAsync();
}

async Task LoadAssetAsync()
{
try
{
AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>(assetAddress);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(handle.Result);
}
}
catch (System.Exception e)
{
Debug.LogError($"加载异常: {e.Message}");
}
}
}

AssetReference 使用

AssetReference 提供了一种类型安全的方式来引用 Addressable 资源:

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

public class AssetReferenceExample : MonoBehaviour
{
// 在 Inspector 中直接拖拽赋值
public AssetReferenceGameObject playerPrefab;
public AssetReferenceTexture2D iconTexture;
public AssetReference spriteAsset;

private GameObject playerInstance;

void Start()
{
LoadPlayer();
}

async void LoadPlayer()
{
// 检查引用是否有效
if (!playerPrefab.RuntimeKeyIsValid())
{
Debug.LogError("AssetReference 无效");
return;
}

// 加载并实例化
AsyncOperationHandle<GameObject> handle = playerPrefab.InstantiateAsync(transform);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
playerInstance = handle.Result;
Debug.Log("玩家实例化成功");
}
}

// 加载 Texture
async void LoadIcon()
{
if (iconTexture.RuntimeKeyIsValid())
{
AsyncOperationHandle<Texture2D> handle = iconTexture.LoadAssetAsync();
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
// 使用纹理
GetComponent<Renderer>().material.mainTexture = handle.Result;
}
}
}

void OnDestroy()
{
// 释放实例
if (playerInstance != null)
{
Addressables.ReleaseInstance(playerInstance);
}

// 释放引用
if (iconTexture != null && iconTexture.Asset != null)
{
iconTexture.ReleaseAsset();
}
}
}

加载多个资源

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

public class LoadMultipleAssets : MonoBehaviour
{
public string label = "Enemies";

private List<GameObject> loadedAssets = new List<GameObject>();
private List<AsyncOperationHandle> handles = new List<AsyncOperationHandle>();

void Start()
{
StartCoroutine(LoadAssetsByLabel());
}

IEnumerator LoadAssetsByLabel()
{
// 按标签加载多个资源
AsyncOperationHandle<IList<GameObject>> handle =
Addressables.LoadAssetsAsync<GameObject>(label, OnAssetLoaded);

yield return handle;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log($"加载了 {handle.Result.Count} 个资源");
}

handles.Add(handle);
}

// 每个资源加载完成时调用
void OnAssetLoaded(GameObject asset)
{
GameObject instance = Instantiate(asset);
loadedAssets.Add(instance);
}

void OnDestroy()
{
// 释放实例
foreach (var instance in loadedAssets)
{
if (instance != null)
{
Destroy(instance);
}
}

// 释放句柄
foreach (var handle in handles)
{
Addressables.Release(handle);
}
}
}

场景加载

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;

public class AddressableSceneLoader : MonoBehaviour
{
public AssetReference sceneReference;
public string sceneAddress = "Assets/Scenes/GameScene.unity";

private AsyncOperationHandle<SceneInstance> sceneHandle;

// 加载场景(单模式)
public async void LoadSceneSingle()
{
sceneHandle = Addressables.LoadSceneAsync(sceneAddress, LoadSceneMode.Single);

await sceneHandle.Task;

if (sceneHandle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log("场景加载成功");
}
}

// 加载场景(附加模式)
public async void LoadSceneAdditive()
{
sceneHandle = Addressables.LoadSceneAsync(sceneAddress, LoadSceneMode.Additive);

// 监控进度
while (!sceneHandle.IsDone)
{
Debug.Log($"加载进度: {sceneHandle.PercentComplete * 100}%");
await System.Threading.Tasks.Task.Delay(100);
}

if (sceneHandle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log("附加场景加载成功");
}
}

// 卸载场景
public async void UnloadScene()
{
if (sceneHandle.IsValid())
{
AsyncOperationHandle<SceneInstance> unloadHandle =
Addressables.UnloadSceneAsync(sceneHandle);

await unloadHandle.Task;

Debug.Log("场景已卸载");
}
}
}

下载和远程资源

Addressables 支持从远程服务器下载资源:

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

public class RemoteAssetLoader : MonoBehaviour
{
public string remoteAssetAddress;

private AsyncOperationHandle handle;

void Start()
{
DownloadAndLoadAsset();
}

async void DownloadAndLoadAsset()
{
// 检查下载大小
var sizeHandle = Addressables.GetDownloadSizeAsync(remoteAssetAddress);
await sizeHandle.Task;

if (sizeHandle.Status == AsyncOperationStatus.Succeeded)
{
long downloadSize = sizeHandle.Result;
Debug.Log($"需要下载: {FormatBytes(downloadSize)}");

if (downloadSize > 0)
{
// 开始下载
handle = Addressables.LoadAssetAsync<GameObject>(remoteAssetAddress);

// 监控下载进度
while (!handle.IsDone)
{
float progress = handle.GetDownloadStatus().Percent;
Debug.Log($"下载进度: {progress * 100}%");
await System.Threading.Tasks.Task.Delay(100);
}

if (handle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(handle.Result);
}
}
}

Addressables.Release(sizeHandle);
}

string FormatBytes(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB" };
int i = 0;
float size = bytes;

while (size >= 1024 && i < suffixes.Length - 1)
{
size /= 1024;
i++;
}

return $"{size:F2} {suffixes[i]}";
}

// 清除缓存
public void ClearCache()
{
Addressables.ClearDependencyCacheAsync(remoteAssetAddress);
}
}

Addressables Groups 配置

在 Addressables Groups 窗口中可以配置资源的打包方式:

Group 设置

Group 配置项:
├── Build Path - 构建输出路径(本地/远程)
├── Load Path - 运行时加载路径(本地/远程)
├── Bundle Mode - 打包模式
│ ├── Pack Separately - 每个资源单独打包
│ ├── Pack Together - 打包到一起
│ └── Pack Together By Label - 按标签打包
└── Asset Provider - 资源提供者类型

Profile 设置

Profile 定义了构建和加载路径变量:

本地构建路径: [UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]
本地加载路径: {UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]

远程构建路径: ServerData/[BuildTarget]
远程加载路径: http://[PrivateIpAddress]:[HostingServicePort]

构建和部署

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;

public class AddressablesBuilder
{
[MenuItem("Build/Build Addressables")]
static void BuildAddressables()
{
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings;

// 设置构建目标
AddressableAssetProfileSettings profileSettings = settings.profileSettings;

// 构建
AddressableAssetSettings.BuildPlayerContent();

Debug.Log("Addressables 构建完成");
}

// 清理构建缓存
[MenuItem("Build/Clean Addressables Cache")]
static void CleanCache()
{
AddressableAssetSettings.CleanPlayerContent();
Debug.Log("Addressables 缓存已清理");
}
}
#endif

内存管理最佳实践

public class AddressablesMemoryManager : MonoBehaviour
{
// 跟踪所有加载的句柄
private Dictionary<string, AsyncOperationHandle> loadedHandles = new Dictionary<string, AsyncOperationHandle>();

// 加载并跟踪
public async System.Threading.Tasks.Task<T> LoadAssetAsync<T>(string address) where T : Object
{
if (loadedHandles.ContainsKey(address))
{
return (T)loadedHandles[address].Result;
}

AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(address);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedHandles[address] = handle;
return handle.Result;
}

return null;
}

// 释放指定资源
public void ReleaseAsset(string address)
{
if (loadedHandles.TryGetValue(address, out AsyncOperationHandle handle))
{
Addressables.Release(handle);
loadedHandles.Remove(address);
}
}

// 释放所有资源
public void ReleaseAll()
{
foreach (var handle in loadedHandles.Values)
{
Addressables.Release(handle);
}
loadedHandles.Clear();
}

// 检查内存使用
void LogMemoryUsage()
{
Debug.Log($"加载的资源数量: {loadedHandles.Count}");

// 使用 Addressables 事件查看器
// Window > Asset Management > Addressables > Event Viewer
}
}

资源管理系统对比

特性PrefabAssetBundleAddressables
学习曲线
内存控制有限完全控制自动管理
依赖管理自动手动自动
热更新不支持支持支持
远程加载不支持支持支持
适用项目小型项目大型项目所有项目

选择建议

小型项目

  • 使用 Prefab 和 Resources(简单资源)
  • 使用 Addressables(需要灵活性)

中型项目

  • 使用 Addressables 作为主要资源管理系统
  • 结合 Prefab 进行场景内复用

大型项目

  • 使用 Addressables 管理所有动态加载资源
  • 建立完善的资源分组和标签策略
  • 实现资源热更新和 CDN 分发

实践示例:完整的资源管理器

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

/// <summary>
/// 统一的资源管理器,支持 Addressables 加载和内存管理
/// </summary>
public class ResourceManager : MonoBehaviour
{
public static ResourceManager Instance { get; private set; }

// 已加载资源的缓存
private Dictionary<string, AsyncOperationHandle> loadedAssets = new Dictionary<string, AsyncOperationHandle>();

// 正在加载中的资源
private Dictionary<string, List<System.Action<Object>>> loadingCallbacks = new Dictionary<string, List<System.Action<Object>>>();

// 实例化对象的跟踪
private Dictionary<string, List<GameObject>> instantiatedObjects = new Dictionary<string, List<GameObject>>();

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

#region 同步加载(内部使用异步)

/// <summary>
/// 加载资源(异步回调)
/// </summary>
public void LoadAsset<T>(string address, System.Action<T> callback) where T : Object
{
StartCoroutine(LoadAssetCoroutine(address, callback));
}

IEnumerator LoadAssetCoroutine<T>(string address, System.Action<T> callback) where T : Object
{
// 检查是否已加载
if (loadedAssets.TryGetValue(address, out AsyncOperationHandle existingHandle))
{
callback?.Invoke(existingHandle.Result as T);
yield break;
}

// 检查是否正在加载
if (loadingCallbacks.ContainsKey(address))
{
loadingCallbacks[address].Add((obj) => callback?.Invoke(obj as T));
yield break;
}

// 开始加载
loadingCallbacks[address] = new List<System.Action<Object>>();

AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(address);
yield return handle;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedAssets[address] = handle;

// 回调
callback?.Invoke(handle.Result);

// 处理等待中的回调
if (loadingCallbacks.TryGetValue(address, out var callbacks))
{
foreach (var cb in callbacks)
{
cb?.Invoke(handle.Result);
}
}
}
else
{
Debug.LogError($"加载资源失败: {address}, 错误: {handle.OperationException}");
callback?.Invoke(null);
}

loadingCallbacks.Remove(address);
}

#endregion

#region 异步加载(async/await)

/// <summary>
/// 异步加载资源
/// </summary>
public async Task<T> LoadAssetAsync<T>(string address) where T : Object
{
// 检查缓存
if (loadedAssets.TryGetValue(address, out AsyncOperationHandle existingHandle))
{
return existingHandle.Result as T;
}

AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(address);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
loadedAssets[address] = handle;
return handle.Result;
}

Debug.LogError($"加载资源失败: {address}");
return null;
}

/// <summary>
/// 批量加载资源
/// </summary>
public async Task<IList<T>> LoadAssetsAsync<T>(string label) where T : Object
{
AsyncOperationHandle<IList<T>> handle = Addressables.LoadAssetsAsync<T>(label, null);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
// 缓存所有加载的资源
foreach (var asset in handle.Result)
{
string address = asset.name;
if (!loadedAssets.ContainsKey(address))
{
// 注意:这里无法获取单个资源的句柄
// 实际项目中可能需要调整策略
}
}
return handle.Result;
}

return null;
}

#endregion

#region 实例化

/// <summary>
/// 加载并实例化 GameObject
/// </summary>
public async Task<GameObject> InstantiateAsync(string address, Transform parent = null)
{
AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(address, parent);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
// 跟踪实例化对象
if (!instantiatedObjects.ContainsKey(address))
{
instantiatedObjects[address] = new List<GameObject>();
}
instantiatedObjects[address].Add(handle.Result);

return handle.Result;
}

return null;
}

/// <summary>
/// 释放实例化对象
/// </summary>
public void ReleaseInstance(GameObject instance)
{
// 从跟踪列表中移除
foreach (var kvp in instantiatedObjects)
{
if (kvp.Value.Remove(instance))
{
break;
}
}

Addressables.ReleaseInstance(instance);
}

#endregion

#region 释放资源

/// <summary>
/// 释放指定资源
/// </summary>
public void ReleaseAsset(string address)
{
if (loadedAssets.TryGetValue(address, out AsyncOperationHandle handle))
{
// 先销毁所有实例
if (instantiatedObjects.TryGetValue(address, out var instances))
{
foreach (var instance in instances.ToArray())
{
if (instance != null)
{
Addressables.ReleaseInstance(instance);
}
}
instantiatedObjects.Remove(address);
}

Addressables.Release(handle);
loadedAssets.Remove(address);
}
}

/// <summary>
/// 释放所有资源
/// </summary>
public void ReleaseAll()
{
// 释放所有实例
foreach (var instances in instantiatedObjects.Values)
{
foreach (var instance in instances)
{
if (instance != null)
{
Addressables.ReleaseInstance(instance);
}
}
}
instantiatedObjects.Clear();

// 释放所有句柄
foreach (var handle in loadedAssets.Values)
{
Addressables.Release(handle);
}
loadedAssets.Clear();

// 强制 GC
Resources.UnloadUnusedAssets();
System.GC.Collect();
}

#endregion

#region 场景加载

/// <summary>
/// 加载场景
/// </summary>
public async Task<SceneInstance> LoadSceneAsync(string address, LoadSceneMode mode = LoadSceneMode.Single)
{
AsyncOperationHandle<SceneInstance> handle = Addressables.LoadSceneAsync(address, mode);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
return handle.Result;
}

return default;
}

/// <summary>
/// 卸载场景
/// </summary>
public async Task UnloadSceneAsync(SceneInstance sceneInstance)
{
AsyncOperationHandle<SceneInstance> handle = Addressables.UnloadSceneAsync(sceneInstance);
await handle.Task;
}

#endregion

#region 下载管理

/// <summary>
/// 获取下载大小
/// </summary>
public async Task<long> GetDownloadSizeAsync(string address)
{
AsyncOperationHandle<long> handle = Addressables.GetDownloadSizeAsync(address);
await handle.Task;

if (handle.Status == AsyncOperationStatus.Succeeded)
{
Addressables.Release(handle);
return handle.Result;
}

return 0;
}

/// <summary>
/// 清除缓存
/// </summary>
public void ClearCache(string address)
{
Addressables.ClearDependencyCacheAsync(address);
}

#endregion

#region 调试

/// <summary>
/// 获取当前加载的资源数量
/// </summary>
public int GetLoadedAssetCount()
{
return loadedAssets.Count;
}

/// <summary>
/// 获取当前实例化的对象数量
/// </summary>
public int GetInstantiatedCount()
{
int count = 0;
foreach (var instances in instantiatedObjects.Values)
{
count += instances.Count;
}
return count;
}

void OnDestroy()
{
ReleaseAll();
}

#endregion
}

最佳实践

1. 资源规划

  • 合理分组:将经常一起使用的资源放在同一个 Group
  • 标签策略:使用标签分类资源,方便批量加载
  • 命名规范:使用一致的命名规范,如 Assets/Prefabs/Characters/Player.prefab

2. 内存优化

  • 及时释放:使用完毕后立即释放资源
  • 避免重复加载:使用缓存管理已加载的资源
  • 监控内存:使用 Addressables Event Viewer 监控资源使用

3. 加载优化

  • 预加载:在合适的时机预加载即将需要的资源
  • 异步加载:使用异步加载避免卡顿
  • 进度反馈:对于大资源,提供加载进度反馈

4. 调试技巧

// 启用 Addressables 日志
#if UNITY_EDITOR
void EnableAddressablesLogging()
{
UnityEngine.AddressableAssets.Addressables.Log += (message) =>
{
Debug.Log($"[Addressables] {message}");
};
}
#endif

// 使用 Event Viewer 监控
// Window > Asset Management > Addressables > Event Viewer
// 可以实时查看资源的加载、释放和内存使用情况

参考资源