Unity3D 进阶教程 - 06 进阶对象池与 Addressables 资源管理

概述

入门系列中我们实现了基础对象池——一个队列管理子弹的复用。但在真实项目中,对象池需要处理多种类型、预热策略、异步加载、跨场景生命周期等问题。

Addressables 是 Unity 的现代资源管理系统,取代了传统的 Resources.Load 和 Asset Bundle 手动管理。它与对象池结合,可以构建完整的资源生命周期管理方案。

1. 对象池进阶

1.1 通用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}

public interface IObjectPool<T> where T : class
{
T Get();
void Return(T obj);
void Prewarm(int count);
void Clear();
}

所有可池化对象实现 IPoolable,在出入池时执行自己的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Bullet : MonoBehaviour, IPoolable
{
private ObjectPool<Bullet> _pool;

public void Init(ObjectPool<Bullet> pool)
{
_pool = pool;
}

public void OnSpawn()
{
gameObject.SetActive(true);
transform.position = Vector3.zero;
_rigidbody.velocity = Vector3.zero;
}

public void OnDespawn()
{
gameObject.SetActive(false);
}

private void OnDisable()
{
CancelInvoke();
}

public void ReturnToPool()
{
_pool?.Return(this);
}
}

1.2 通用对象池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ObjectPool<T> : IObjectPool<T> where T : MonoBehaviour, IPoolable
{
private readonly Func<T> _factory;
private readonly Stack<T> _pool = new();
private readonly HashSet<T> _active = new();
private readonly int _maxSize;

public ObjectPool(
Func<T> factory,
int prewarmCount = 0,
int maxSize = 100)
{
_factory = factory;
_maxSize = maxSize;

if (prewarmCount > 0)
Prewarm(prewarmCount);
}

public T Get()
{
T obj;
if (_pool.Count > 0)
{
obj = _pool.Pop();
}
else
{
obj = _factory();
}

_active.Add(obj);
obj.OnSpawn();
return obj;
}

public void Return(T obj)
{
if (!_active.Remove(obj)) return;

obj.OnDespawn();

if (_pool.Count < _maxSize)
_pool.Push(obj);
else
GameObject.Destroy(obj.gameObject);
}

public void Prewarm(int count)
{
var temp = new List<T>();
for (int i = 0; i < count; i++)
{
var obj = _factory();
obj.OnDespawn();
obj.gameObject.SetActive(false);
temp.Add(obj);
}
foreach (var obj in temp)
_pool.Push(obj);
}

public void Clear()
{
foreach (var obj in _pool)
GameObject.Destroy(obj.gameObject);
_pool.Clear();
_active.Clear();
}
}

2. Addressables 基础

2.1 配置 Addressables

  1. 通过 Window > Asset Management > Addressables > Groups 打开面板
  2. 点击 Create Addressables Settings
  3. 将资源拖入 Group,设置 Addressable Name(资源唯一标识)

2.2 加载资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

private async UniTask<GameObject> LoadEnemyPrefabAsync()
{
var handle = Addressables.LoadAssetAsync<GameObject>("Enemy");
await handle.ToUniTask();
return handle.Result;
}

private async UniTask LoadGameSceneAsync()
{
await Addressables.LoadSceneAsync("GameScene").ToUniTask();
}

2.3 Addressables + 对象池

两者的结合是最佳实践——Addressables 负责异步加载预制体,对象池负责复用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class AddressablePool<T> where T : MonoBehaviour, IPoolable
{
private readonly string _addressableKey;
private readonly Stack<T> _pool = new();
private readonly HashSet<T> _active = new();
private GameObject _prefab;
private bool _isLoaded;
private readonly int _maxSize;

public AddressablePool(string addressableKey, int maxSize = 100)
{
_addressableKey = addressableKey;
_maxSize = maxSize;
}

public async UniTask InitializeAsync()
{
if (_isLoaded) return;
var handle = Addressables.LoadAssetAsync<GameObject>(_addressableKey);
await handle.ToUniTask();
_prefab = handle.Result;
_isLoaded = true;
}

public async UniTask<T> GetAsync()
{
if (!_isLoaded)
await InitializeAsync();

T obj;
if (_pool.Count > 0)
{
obj = _pool.Pop();
obj.gameObject.SetActive(true);
}
else
{
var go = Addressables.InstantiateAsync(_addressableKey).WaitForCompletion();
obj = go.GetComponent<T>();
}

_active.Add(obj);
obj.OnSpawn();
return obj;
}

public void Return(T obj)
{
if (!_active.Remove(obj)) return;

obj.OnDespawn();
if (_pool.Count < _maxSize)
{
obj.gameObject.SetActive(false);
_pool.Push(obj);
}
else
{
Addressables.ReleaseInstance(obj.gameObject);
}
}
}

3. 资源依赖与分组策略

Addressables Groups 的合理分组直接影响加载性能:

分组 内容 加载策略
CoreAssets 基础 UI、字体、着色器 启动时预加载,常驻内存
Characters_Shared 角色共享资源(骨架、材质) 按需加载,使用后释放
Level_01 关卡特有资源 进入关卡时加载,离开时卸载
Effects 特效预制体 通过对象池懒加载
1
2
3
4
5
6
7
8
9
private async UniTask InitializeCoreAssetsAsync()
{
var handle = Addressables.LoadAssetsAsync<GameObject>(
new List<string> { "CoreAssets" },
null,
Addressables.MergeMode.Union
);
await handle.ToUniTask();
}

4. 常见陷阱

4.1 确保释放

1
2
3
4
5
// 错误:只 Destroy 不 Release
Destroy(go);

// 正确
Addressables.ReleaseInstance(go);

4.2 场景加载时的实例处理

场景切换时,Addressables 实例化的对象不会被自动释放:

1
2
3
4
5
6
7
8
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
var instances = FindObjectsByType<AddressableInstance>(FindObjectsSortMode.None);
foreach (var inst in instances)
{
Addressables.ReleaseInstance(inst.gameObject);
}
}

总结

  • 对象池 是性能基石,通过通用接口和多类型管理器适应复杂需求
  • Addressables 是资源管理的现代方案,替代 Resources 和手动 AssetBundle
  • Addressables + 对象池 的组合实现完整的异步加载到复用到自动释放生命周期
  • 合理分组 Addressables Groups,按需加载和卸载

下一篇将探索 URP 渲染管线深入,了解可编写渲染管线的核心机制。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: