Unity 预加载系统设计与实现

概述

最近在开发一个微信小游戏,因为是 3D + 关卡制的,并且希望保证一个流畅的游戏体验,希望在关卡切换的时候没有 loading 过程,因此设计了一个预加载系统。

系统架构设计

核心组件

预加载系统主要由三个核心组件构成:

  1. PreloadConfig - 预加载配置类
  2. PreloadPoolItem - 预加载池项目类
  3. PreloadManager - 预加载管理器(核心)

设计原则

  • 低 GC 设计:避免频繁的内存分配,采用对象池复用机制
  • 异步加载:使用 UniTask 实现异步加载,避免主线程卡顿
  • 按需启动:协程按需启动,避免不必要的资源消耗
  • 智能清理:自动清理长时间未使用的对象

核心功能实现

1. 预加载配置系统

1
2
3
4
5
6
7
8
9
[System.Serializable]
public class PreloadConfig
{
public string location; // 资源路径
public int preloadCount = 1; // 预加载数量
public int priority = 0; // 优先级
public bool autoExpand = false; // 自动扩容
public int maxCount = 10; // 最大数量限制
}

配置系统支持:

  • 灵活的预加载数量控制
  • 优先级排序机制
  • 自动扩容功能
  • 最大数量限制

2. 对象池项目管理

1
2
3
4
5
6
7
8
public class PreloadPoolItem
{
public GameObject gameObject;
public bool isInUse;
public float createTime;
public float lastUseTime;
public int useCount;
}

每个池项目包含:

  • 对象引用和使用状态
  • 创建时间和最后使用时间
  • 使用次数统计

3. 预加载机制

系统采用队列式预加载,支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 添加预加载配置
public void AddPreloadConfig(PreloadConfig config)
{
_preloadQueue.Enqueue(config);
StartPreloadCoroutineIfNeeded();
}

// 按需启动预加载协程
private void StartPreloadCoroutineIfNeeded()
{
if (!_isPreloadCoroutineRunning && _preloadQueue.Count > 0)
{
_isPreloadCoroutineRunning = true;
PreloadCoroutine().Forget();
}
}

4. 对象获取与归还

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
// 从池中获取对象
public GameObject GetFromPool(string location, Transform parent = null)
{
if (!_preloadPools.TryGetValue(location, out var pool))
return null;

foreach (var item in pool)
{
if (!item.isInUse)
{
item.isInUse = true;
item.useCount++;
item.lastUseTime = Time.time;

var go = item.gameObject;
go.SetActive(true);
// 重置Transform状态
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;

return go;
}
}
return null;
}

性能优化策略

1. 分帧加载

1
2
3
4
5
// 每加载一定数量后等待一帧
if (i % maxLoadPerFrame == maxLoadPerFrame - 1)
{
await UniTask.Yield();
}

通过控制每帧最大加载数量,避免加载过程中的卡顿。

2. 自动清理机制

1
2
3
4
5
6
7
8
9
10
11
12
private void CleanupIdleObjects()
{
var currentTime = Time.time;
foreach (var item in pool)
{
if (!item.isInUse && (currentTime - item.createTime) > maxIdleTime)
{
// 清理长时间未使用的对象
itemsToRemove.Add(item);
}
}
}

定期清理长时间未使用的对象,防止内存泄漏。

3. 协程状态管理

1
2
private bool _isPreloadCoroutineRunning = false;
private bool _isCleanupCoroutineRunning = false;

通过状态标记避免重复启动协程,提高系统效率。

调试与监控

系统提供完整的调试信息:

1
2
3
4
5
6
7
8
9
10
public class PreloadManagerDebugInfo
{
public int totalPoolCount;
public int totalObjectCount;
public int inUseCount;
public int queueCount;
public bool isPreloadCoroutineRunning;
public bool isCleanupCoroutineRunning;
public List<PreloadPoolDebugInfo> pools;
}

支持:

  • 实时池状态监控
  • 对象使用情况统计
  • 协程运行状态跟踪
  • 详细的调试信息输出

使用示例

基本使用

1
2
3
4
5
6
7
8
9
10
11
// 添加预加载配置
var config = new PreloadConfig("UI/Button", 5, 1);
PreloadManager.Instance.AddPreloadConfig(config);

// 获取预加载对象
var button = PreloadManager.Instance.GetFromPool("UI/Button", parent);
if (button == null)
{
// 回退到直接加载
button = await ResourceModule.Instance.LoadGameObjectAsync("UI/Button", parent);
}

批量预加载

1
2
3
4
5
6
7
var configs = new List<PreloadConfig>
{
new PreloadConfig("UI/Button", 10, 1),
new PreloadConfig("Effect/Explosion", 5, 2),
new PreloadConfig("Audio/BGM", 3, 0)
};
PreloadManager.Instance.AddPreloadConfigs(configs);

立即预加载

1
2
// 立即预加载指定资源
await PreloadManager.Instance.PreloadImmediately("UI/Dialog", 3);

完整代码

并不能复制粘贴直接使用,代码只做示例

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace EF
{
/// <summary>
/// 预加载项配置
/// </summary>
[System.Serializable]
public class PreloadConfig
{
public string location; // 资源路径
public int preloadCount = 1; // 预加载数量
public int priority = 0; // 优先级,数值越大越优先
public bool autoExpand = false; // 是否自动扩容
public int maxCount = 10; // 最大数量限制

public PreloadConfig(string location, int preloadCount = 1, int priority = 0)
{
this.location = location;
this.preloadCount = preloadCount;
this.priority = priority;
}
}

/// <summary>
/// 预加载池项目
/// </summary>
public class PreloadPoolItem
{
public GameObject gameObject;
public bool isInUse;
public float createTime;
public float lastUseTime;
public int useCount;

public PreloadPoolItem(GameObject go)
{
gameObject = go;
isInUse = false;
createTime = Time.time;
lastUseTime = Time.time;
useCount = 0;
}
}

/// <summary>
/// 预加载管理器
/// </summary>
public class PreloadManager : BehaviourSingleton<PreloadManager>
{

[Header("预加载设置")]
[SerializeField] private int maxLoadPerFrame = 1; // 每帧最大加载数量
[SerializeField] private float loadInterval = 0.016f; // 加载间隔(秒)
[SerializeField] private bool enableAutoCleanup = false; // 是否启用自动清理
[SerializeField] private float cleanupInterval = 60f; // 清理间隔(秒)
[SerializeField] private float maxIdleTime = 300f; // 最大空闲时间(秒)

// 预加载池字典 location -> List<PreloadPoolItem>
private Dictionary<string, List<PreloadPoolItem>> _preloadPools = new();

// 预加载队列
private Queue<PreloadConfig> _preloadQueue = new();

// 正在预加载的位置集合
private HashSet<string> _loadingLocations = new();

// 预加载根节点
private Transform _preloadRoot;

// 协程状态跟踪
private bool _isPreloadCoroutineRunning = false;
private bool _isCleanupCoroutineRunning = false;

// 性能统计
public int TotalPreloadedCount => GetTotalPreloadedCount();
public int InUseCount => GetInUseCount();

private void Awake()
{
if (_instance == null)
{
_instance = this;
InitializePreloadRoot();
}
else if (_instance != this)
{
Destroy(gameObject);
}
}

private void InitializePreloadRoot()
{
var rootGO = new GameObject("PreloadPool");
rootGO.transform.SetParent(transform);
rootGO.SetActive(false); // 隐藏预加载对象
_preloadRoot = rootGO.transform;
}

/// <summary>
/// 启动预加载协程(按需启动)
/// </summary>
private void StartPreloadCoroutineIfNeeded()
{
if (!_isPreloadCoroutineRunning && _preloadQueue.Count > 0)
{
_isPreloadCoroutineRunning = true;
PreloadCoroutine().Forget();
Log.Info("Started PreloadCoroutine");
}
}

/// <summary>
/// 启动清理协程(按需启动)
/// </summary>
private void StartCleanupCoroutineIfNeeded()
{
if (enableAutoCleanup && !_isCleanupCoroutineRunning && HasObjectsToCleanup())
{
_isCleanupCoroutineRunning = true;
CleanupCoroutine().Forget();
Log.Info("Started CleanupCoroutine");
}
}

/// <summary>
/// 检查是否有需要清理的对象
/// </summary>
private bool HasObjectsToCleanup()
{
return _preloadPools.Count > 0 && TotalPreloadedCount > 0;
}

#region 公共API

/// <summary>
/// 添加预加载配置
/// </summary>
public void AddPreloadConfig(PreloadConfig config)
{
if (string.IsNullOrEmpty(config.location))
{
Log.Warning("PreloadConfig location is null or empty");
return;
}

// 如果已经在加载队列中,跳过
if (_loadingLocations.Contains(config.location))
{
return;
}

_preloadQueue.Enqueue(config);
Log.Info($"Added preload config for {config.location}, count: {config.preloadCount}");

// 检查是否需要启动预加载协程
StartPreloadCoroutineIfNeeded();
}

/// <summary>
/// 批量添加预加载配置
/// </summary>
public void AddPreloadConfigs(List<PreloadConfig> configs)
{
foreach (var config in configs)
{
AddPreloadConfig(config);
}
}

/// <summary>
/// 尝试从预加载池获取对象
/// </summary>
public GameObject GetFromPool(string location, Transform parent = null)
{
if (!_preloadPools.TryGetValue(location, out var pool) || pool.Count == 0)
{
return null;
}
List<PreloadPoolItem> items = new List<PreloadPoolItem>();

// 查找未使用的对象
foreach (var item in pool)
{
if (!item.isInUse)
{
item.isInUse = true;
item.useCount++;
item.lastUseTime = Time.time;

var go = item.gameObject;
go.SetActive(true);

if (parent != null)
{
go.transform.SetParent(parent);
}
else
{
go.transform.SetParent(null);
}

// 重置位置和旋转
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;

Log.Info($"从预加载池获取对象: {location}");

// 检查是否需要启动清理协程
StartCleanupCoroutineIfNeeded();
items.Add(item);
return go;
}
}

// 使用后直接直接丢弃,不保留引用。防止外部destroy导致无法回收
foreach (var item in items)
{
_preloadPools[location].Remove(item);
}

return null;
}

/// <summary>
/// 归还对象到预加载池
/// </summary>
public void ReturnToPool(string location, GameObject go)
{
// 如果归还对象 新增到预加载池
if (_preloadPools.TryGetValue(location, out var pool))
{
pool.Add(new PreloadPoolItem(go));
}
}

/// <summary>
/// 立即预加载指定资源
/// </summary>
public async UniTask PreloadImmediately(string location, int count = 1)
{
var config = new PreloadConfig(location, count, int.MaxValue);
await PreloadAsset(config);

// 预加载完成后检查是否需要启动清理协程
StartCleanupCoroutineIfNeeded();
}

/// <summary>
/// 清理指定资源的预加载池
/// </summary>
public void ClearPool(string location)
{
if (_preloadPools.TryGetValue(location, out var pool))
{
foreach (var item in pool)
{
if (item.gameObject != null)
{
Destroy(item.gameObject);
}
}
pool.Clear();
_preloadPools.Remove(location);
Log.Info($"Cleared preload pool: {location}");
}
}

/// <summary>
/// 清理所有预加载池
/// </summary>
public void ClearAllPools()
{
foreach (var kvp in _preloadPools)
{
foreach (var item in kvp.Value)
{
if (item.gameObject != null)
{
Destroy(item.gameObject);
}
}
}
_preloadPools.Clear();
_loadingLocations.Clear();
Log.Info("Cleared all preload pools");
}

/// <summary>
/// 获取池中可用对象数量
/// </summary>
public int GetAvailableCount(string location)
{
if (!_preloadPools.TryGetValue(location, out var pool))
return 0;

int count = 0;
foreach (var item in pool)
{
if (!item.isInUse)
count++;
}
return count;
}

/// <summary>
/// 获取池中总对象数量
/// </summary>
public int GetTotalCount(string location)
{
return _preloadPools.TryGetValue(location, out var pool) ? pool.Count : 0;
}

#endregion

#region 内部逻辑

private async UniTaskVoid PreloadCoroutine()
{
while (_preloadQueue.Count > 0)
{
var config = _preloadQueue.Dequeue();
await PreloadAsset(config);

// 控制加载频率
await UniTask.Delay((int)(loadInterval * 1000));
}

// 队列为空,停止协程
_isPreloadCoroutineRunning = false;
Log.Info("PreloadCoroutine stopped - queue is empty");

// 预加载完成后检查是否需要启动清理协程
StartCleanupCoroutineIfNeeded();
}

private async UniTask PreloadAsset(PreloadConfig config)
{
if (_loadingLocations.Contains(config.location))
return;

_loadingLocations.Add(config.location);

try
{
if (!_preloadPools.ContainsKey(config.location))
{
_preloadPools[config.location] = new List<PreloadPoolItem>();
}

var pool = _preloadPools[config.location];
int currentCount = pool.Count;
int targetCount = config.preloadCount;

// 如果当前数量已经足够,跳过
if (currentCount >= targetCount)
{
Log.Info($"Preload pool already has enough objects: {config.location} ({currentCount}/{targetCount})");
return;
}

int loadCount = targetCount - currentCount;

Log.Info($"Start preloading {config.location}, count: {loadCount}");

for (int i = 0; i < loadCount; i++)
{
try
{
var go = await ResourceModule.Instance.LoadGameObjectAsync(config.location, _preloadRoot);
if (go != null)
{
go.SetActive(false);
var poolItem = new PreloadPoolItem(go);
pool.Add(poolItem);
}
}
catch (Exception e)
{
Log.Error($"Failed to preload {config.location}: {e.Message}");
}

// 每加载一定数量后等待一帧,避免卡顿
if (i % maxLoadPerFrame == maxLoadPerFrame - 1)
{
await UniTask.Yield();
}
}

Log.Info($"Preload completed: {config.location}, loaded: {loadCount}");
}
finally
{
_loadingLocations.Remove(config.location);
}
}

private async UniTaskVoid CleanupCoroutine()
{
while (HasObjectsToCleanup())
{
await UniTask.Delay((int)(cleanupInterval * 1000));

if (HasObjectsToCleanup())
{
CleanupIdleObjects();
}
}

// 没有需要清理的对象,停止协程
_isCleanupCoroutineRunning = false;
Log.Info("CleanupCoroutine stopped - no objects to cleanup");
}

private void CleanupIdleObjects()
{
var currentTime = Time.time;
var locationsToRemove = new List<string>();

foreach (var kvp in _preloadPools)
{
var location = kvp.Key;
var pool = kvp.Value;
var itemsToRemove = new List<PreloadPoolItem>();

foreach (var item in pool)
{
// 清理长时间未使用的对象
if (!item.isInUse && (currentTime - item.createTime) > maxIdleTime)
{
itemsToRemove.Add(item);
}
}

foreach (var item in itemsToRemove)
{
pool.Remove(item);
if (item.gameObject != null)
{
Destroy(item.gameObject);
}
}

if (pool.Count == 0)
{
locationsToRemove.Add(location);
}
}

foreach (var location in locationsToRemove)
{
_preloadPools.Remove(location);
}

if (locationsToRemove.Count > 0)
{
Log.Info($"Cleanup completed, removed {locationsToRemove.Count} empty pools");
}
}

private int GetTotalPreloadedCount()
{
int total = 0;
foreach (var pool in _preloadPools.Values)
{
total += pool.Count;
}
return total;
}

private int GetInUseCount()
{
int inUse = 0;
foreach (var pool in _preloadPools.Values)
{
foreach (var item in pool)
{
if (item.isInUse)
inUse++;
}
}
return inUse;
}

#endregion

private void OnDestroy()
{
_isPreloadCoroutineRunning = false;
_isCleanupCoroutineRunning = false;
ClearAllPools();
}

#region Debug信息

/// <summary>
/// 预加载池调试信息
/// </summary>
public class PreloadPoolDebugInfo
{
public string location;
public int totalCount;
public int availableCount;
public int inUseCount;
public List<PreloadItemDebugInfo> items;
public PreloadConfig config;
}

/// <summary>
/// 预加载项调试信息
/// </summary>
public class PreloadItemDebugInfo
{
public bool isInUse;
public float createTime;
public float lastUseTime;
public int useCount;
public string gameObjectName;
public bool isGameObjectNull;
}

/// <summary>
/// 预加载管理器调试信息
/// </summary>
public class PreloadManagerDebugInfo
{
public int totalPoolCount;
public int totalObjectCount;
public int inUseCount;
public int queueCount;
public bool isPreloadCoroutineRunning;
public bool isCleanupCoroutineRunning;
public List<PreloadPoolDebugInfo> pools;
public List<string> loadingLocations;
public bool enableAutoCleanup;
public float cleanupInterval;
public float maxIdleTime;
}

/// <summary>
/// 获取详细调试信息
/// </summary>
public PreloadManagerDebugInfo GetDetailedDebugInfo()
{
var info = new PreloadManagerDebugInfo
{
totalPoolCount = _preloadPools.Count,
totalObjectCount = TotalPreloadedCount,
inUseCount = InUseCount,
queueCount = _preloadQueue.Count,
isPreloadCoroutineRunning = _isPreloadCoroutineRunning,
isCleanupCoroutineRunning = _isCleanupCoroutineRunning,
pools = new List<PreloadPoolDebugInfo>(),
loadingLocations = _loadingLocations.ToList(),
enableAutoCleanup = enableAutoCleanup,
cleanupInterval = cleanupInterval,
maxIdleTime = maxIdleTime
};

foreach (var kvp in _preloadPools)
{
var poolInfo = new PreloadPoolDebugInfo
{
location = kvp.Key,
totalCount = kvp.Value.Count,
availableCount = GetAvailableCount(kvp.Key),
inUseCount = kvp.Value.Count(item => item.isInUse),
items = new List<PreloadItemDebugInfo>()
};

foreach (var item in kvp.Value)
{
var itemInfo = new PreloadItemDebugInfo
{
isInUse = item.isInUse,
createTime = item.createTime,
lastUseTime = item.lastUseTime,
useCount = item.useCount,
gameObjectName = item.gameObject != null ? item.gameObject.name : "null",
isGameObjectNull = item.gameObject == null
};
poolInfo.items.Add(itemInfo);
}

info.pools.Add(poolInfo);
}

return info;
}

/// <summary>
/// 获取简单调试信息(向后兼容)
/// </summary>
public string GetDebugInfo()
{
var info = $"预加载池统计:\n";
info += $"总池数量: {_preloadPools.Count}\n";
info += $"总对象数: {TotalPreloadedCount}\n";
info += $"使用中: {InUseCount}\n";
info += $"排队中: {_preloadQueue.Count}\n";
info += $"预加载协程运行中: {_isPreloadCoroutineRunning}\n";
info += $"清理协程运行中: {_isCleanupCoroutineRunning}\n\n";

foreach (var kvp in _preloadPools)
{
var available = GetAvailableCount(kvp.Key);
var total = kvp.Value.Count;
info += $"{kvp.Key}: {available}/{total}\n";
}

return info;
}

/// <summary>
/// 获取所有池的名称列表
/// </summary>
public List<string> GetAllPoolNames()
{
return _preloadPools.Keys.ToList();
}

/// <summary>
/// 获取指定池的详细信息
/// </summary>
public PreloadPoolDebugInfo GetPoolDebugInfo(string location)
{
if (!_preloadPools.TryGetValue(location, out var pool))
return null;

var poolInfo = new PreloadPoolDebugInfo
{
location = location,
totalCount = pool.Count,
availableCount = GetAvailableCount(location),
inUseCount = pool.Count(item => item.isInUse),
items = new List<PreloadItemDebugInfo>()
};

foreach (var item in pool)
{
var itemInfo = new PreloadItemDebugInfo
{
isInUse = item.isInUse,
createTime = item.createTime,
lastUseTime = item.lastUseTime,
useCount = item.useCount,
gameObjectName = item.gameObject != null ? item.gameObject.name : "null",
isGameObjectNull = item.gameObject == null
};
poolInfo.items.Add(itemInfo);
}

return poolInfo;
}

#endregion
}
}

总结

这个预加载系统通过以下特性实现了高效的资源管理:

  1. 低 GC 设计:对象池复用机制减少内存分配
  2. 异步加载:UniTask 确保主线程流畅
  3. 智能管理:按需启动协程,自动清理闲置对象
  4. 灵活配置:支持优先级、数量限制等多种配置
  5. 完整监控:提供详细的调试和监控信息

Unity 预加载系统设计与实现
https://lshgame.com/2025/07/14/Unity_Preload_System_Design_and_Implementation/
作者
SuHang
发布于
2025年7月14日
许可协议