Unity中的对象池
Tulenber ⸱
10 April, 2020 ⸱
Intermediate ⸱
4 min ⸱
2019.3.8f1 ⸱
这篇文章是关于一种模式的,它将帮助您将所有对象收集到一个池中。
可以在文章“对象池版本2”中找到更新的实现。
在上一篇文章《 Unity中无尽的2D背景》中,我们展示了一个技巧,该技巧说明了如何使用一张图片制作无尽的背景,这几乎与游戏中涉及的所有内容都使用了相同的方法。游戏越大,实时模拟所有游戏对象就越困难,因此通常会隐藏并删除超出屏幕的所有内容,以节省如此有限且宝贵的计算能力。 同时,分配和释放内存是用于计算的最昂贵的操作之一。 同样,在具有垃圾收集器(C#)的语言中,这对于它的服务也成为时间问题。 因此,在有足够数量的可用RAM的情况下,一种有趣的方法是重用已创建但未使用的对象,而不是删除它们。 这就是所谓的对象池化模式,我们将在本文中介绍。
理论
理论部分已进行了足够详细的描述,您可以参考这本书。
实践
我们实现的主要任务将是简单性。
该实现基于前一篇关于无尽背景的文章,因此,如果您想重复此示例并且不知道要做什么,则可以从该示例开始并进行以下更改。
- 预制中使用的此脚本负责所有对象池(可能有多个)
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
|
using System;
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
// Inspector不支持Dictionary类型,因此让我们使用它
[Serializable]
public class PrefabData
{
public string name;
public GameObject prefab;
}
[SerializeField] private List<PrefabData> prefabDatas = null;
// 用于新对象实例的预制件
private Dictionary<string, GameObject> prefabs = new Dictionary<string, GameObject>();
// 免费对象池
private Dictionary<string, Queue<GameObject>> pools = new Dictionary<string, Queue<GameObject>>();
private void Awake()
{
// 将有关我们池的信息放入字典
foreach (var prefabData in prefabDatas)
{
prefabs.Add(prefabData.name, prefabData.prefab);
pools.Add(prefabData.name, new Queue<GameObject>());
}
prefabDatas = null;
}
public GameObject GetObject(string poolName)
{
// 如果池中有空闲对象,则将其返回
if(pools[poolName].Count > 0)
{
return pools[poolName].Dequeue();
}
// 如果对象池为空,则返回新实例
return Instantiate(prefabs[poolName]);
}
public void ReturnObject(string poolName, GameObject poolObject)
{
// 将对象返回池
pools[poolName].Enqueue(poolObject);
}
}
|
- 池对象示例
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
|
using UnityEngine;
public class Projectile : MonoBehaviour
{
[SerializeField] private float speed = 0;
// 链接到池处理程序对象
[SerializeField] public ObjectPool objectPool = null;
// 泳池名称
public const string PoolName = "Projectiles";
private Vector2 _moveVector = Vector2.zero;
public Vector2 MoveVector
{
set => _moveVector = value;
}
void Update()
{
transform.Translate(_moveVector.x * Time.deltaTime * speed, _moveVector.y * Time.deltaTime * speed, 0, Space.World);
}
void OnBecameInvisible() {
// 对象离开屏幕后,我们将对象返回池中
gameObject.SetActive(false);
objectPool.ReturnObject(PoolName, gameObject);
}
}
|
- 从池接收对象的示例
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
|
using UnityEngine;
public class Fire : MonoBehaviour
{
// 对场景对象的引用
[SerializeField] private Transform leftCannon = null;
[SerializeField] private Transform rightCannon = null;
[SerializeField] private UnevirseHandler universe = null;
[SerializeField] private Transform space = null;
private bool _rightCannon = false;
// 链接到对象池
[SerializeField] private ObjectPool objectPool = null;
void Update()
{
if (Input.GetMouseButtonDown (0)) {
// 从池中获取对象
GameObject pr = objectPool.GetObject(Projectile.PoolName);
// 池中的对象需要适当的初始化
pr.transform.SetParent(space);
pr.transform.SetPositionAndRotation(_rightCannon ? rightCannon.position : leftCannon.position, _rightCannon ? rightCannon.rotation : leftCannon.rotation);
Projectile prpr = pr.GetComponent<Projectile>();
prpr.MoveVector = universe.LookVector.normalized;
prpr.objectPool = objectPool;
pr.SetActive(true);
_rightCannon = !_rightCannon;
}
}
}
|
- 该脚本与池无关,但是如果您想重复此代码,则可以使用它来更改上一篇文章中的示例
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
|
using UnityEngine;
public class UnevirseHandler : MonoBehaviour
{
// 对场景对象的引用
[SerializeField] private Camera mainCamera = null;
[SerializeField] private GameObject ship = null;
[SerializeField] private GameObject space = null;
// 可能的相机视图的半径
private float _spaceCircleRadius = 0;
// 原始背景对象尺寸
private float _backgroundOriginalSizeX = 0;
private float _backgroundOriginalSizeY = 0;
private Vector2 _shipPosition = Vector2.zero;
private Vector2 _lookVector = Vector2.zero;
private Vector2 _stvp = Vector2.zero;
public Vector2 LookVector => _lookVector;
void Start()
{
// 原始背景尺寸
SpriteRenderer sr = space.GetComponent<SpriteRenderer>();
var originalSize = sr.size;
_backgroundOriginalSizeX = originalSize.x;
_backgroundOriginalSizeY = originalSize.y;
// 相机高度等于正射影像大小
float orthographicSize = mainCamera.orthographicSize;
// 相机宽度等于正射影像尺寸乘以宽高比
float screenAspect = (float)Screen.width / (float)Screen.height;
_spaceCircleRadius = Mathf.Sqrt(orthographicSize * screenAspect * orthographicSize * screenAspect + orthographicSize * orthographicSize);
// 最终的背景精灵大小应允许您在任何方向上移动一种基本的背景纹理大小,并且在所有方向上都与摄影机的半径重叠
sr.size = new Vector2(_spaceCircleRadius * 2 + _backgroundOriginalSizeX * 2, _spaceCircleRadius * 2 + _backgroundOriginalSizeY * 2);
_shipPosition = ship.transform.position;
}
void Update()
{
// 鼠标在世界空间中的坐标
_stvp = mainCamera.ScreenToWorldPoint(Input.mousePosition);
// 鼠标指针的方向
_lookVector = _stvp - _shipPosition;
float rotZ = Mathf.Atan2(_lookVector.y, _lookVector.x) * Mathf.Rad2Deg;
ship.transform.rotation = Quaternion.Euler(0f, 0f, rotZ - 90);
}
}
|
示范
在下面的结果中,您可以看到只有在没有空闲对象的情况下才创建新对象.
结论
Unity没有为我们提供对象池的任何参考实现,并且由于这是一种普遍的模式,因此您可以找到它的多种变体。 本文中提供的代码没有预填充池这样的有用功能。 另外,它要求输入两次池名称,这增加了配置的复杂性。 通常,为了获得更多语法和功能,您可以做出大量决定。 但是,首先,我们将专注于简约版本。下次见!^_^
可以在文章“对象池版本2”中找到更新的实现。
如果您喜欢这篇文章,可以为它提供支持