Unity的C#编程教程_64_对象池 Object Pooling 详解及应用练习
目录:
Challenge: Pool Manager
Challenge: Request from Pool Manager
Recycle the Pool
Object Pooling Design Pattern
- object polling 是设计范式的一种
- 是一种优化的范式,尤其对于移动化来说非常重要
- 不是生成和销毁游戏对象,而是循环利用游戏对象
假设我们创建一个射击游戏,我们射出的子弹都是游戏对象,传统的做法是不断从枪口生成,然后击中目标后不断销毁,这个利用的是垃圾回收机制,只有当前帧的垃圾回收完成后,才能开始处理下一帧,所以如果有很多的游戏对象要生成和销毁,那么游戏可能就会卡住。
替代的优化解决方案是 object polling,我们可以循环利用这些子弹,只生成一次,然后在整个程序运行过程中不断循环使用。
创建一个游戏对象 Player,并挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField]
private GameObject _bulletPrefab;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Instantiate(_bulletPrefab);
}
}
}
设计一个 Bullet 的 Prefab,挂载同名脚本,然后拖拽赋值:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Destroy(this.gameObject, 2f); // 2 秒钟后自动销毁
}
// Update is called once per frame
void Update()
{
}
}
这样我们进入游戏后,按下空格键就会创建 Bullet,过了 2 秒钟自动销毁。这个是大多数初学者的做法。
这样做的话,就是在利用垃圾回收机制。
下节我们将探讨如何转化为 object pooling
Challenge: Pool Manager
- 任务说明:
- 创建一个 Pool Manager class
- 存储所有需要用的 Bullet
- 这样我们就可以有个预设的 Bullet 列表供使用,比如 10 个,可以用来循环使用
创建游戏对象 Pool Manager,挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoBehaviour
{
[SerializeField] // 在 inspector 中可见,用于拖拽赋值
private GameObject _bulletPrefab; // 子弹的 prefab 需要赋值
// 以下为 PoolManager 的 singleton 定义部分,用于简易调用
private static PoolManager _instance;
public static PoolManager Instance
{
get
{
if (_instance == null)
{
Debug.LogError("no instance");
}
return _instance;
}
}
private void Awake()
{
_instance = this;
}
// 以下用于定义 Bullet 的对象池
public List<GameObject> bulletList;
int i;
bool fullBullet;
public void CreatBullet()
{
if (fullBullet == false) // 如果池子没满
{
GameObject obj = Instantiate(_bulletPrefab); // 生成游戏对象实例
bulletList.Add(obj); // 添加到列表中(池子中)
if (bulletList.Count > 9) // 池子满了
{
fullBullet = true; // 修改 flag
}
}
else if(i < 10) // 池子满了以后就可以直接激活,而不用生成了
{
bulletList[i].SetActive(true); // 挨个激活
i++;
}
else
{
i = 0; // 激活一轮以后再从头开始
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
这里将其设计为 singleton 便于访问。
玩家 Player 脚本,控制开枪:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) // 按下空格键表示开枪
{
PoolManager.Instance.CreatBullet(); // 调用 PoolManager 生成子弹
}
}
}
子弹 Bullet 的脚本,控制子弹的灭活方式:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnEnable() // 游戏对象被激活的时候调用
{
StartCoroutine(bulletDis()); // 启动协程
}
IEnumerator bulletDis() // 定义协程
{
yield return new WaitForSeconds(2); // 等待2秒
this.gameObject.SetActive(false); // Bullet灭活
}
}
同样的任务我们可以有不同的实现思路,以下是另一种稍有区别的思路,在游戏启动的时候就一下子形成整个 Bullet 列表,之前是每次按下空格生成一个直至添加满列表:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoBehaviour
{
[SerializeField] // 在 inspector 中可见,用于拖拽赋值
private GameObject _bulletPrefab; // 子弹的 prefab 需要赋值
// 以下为 PoolManager 的 singleton 定义部分,用于简易调用
private static PoolManager _instance;
public static PoolManager Instance
{
get
{
if (_instance == null)
{
Debug.LogError("no instance");
}
return _instance;
}
}
private void Awake()
{
_instance = this;
}
// 以下用于定义 Bullet 的对象池
[SerializeField]
private List<GameObject> _bulletList;
[SerializeField]
private GameObject _bulletContainer; // 生成的子弹游戏对象汇总到一个母文件下面
private int i;
public void CreatBullet()
{
if(i < 10) // 池子满了以后就可以直接激活,而不用生成了
{
if (_bulletList[i].activeSelf == false) // 这个修正可以确保按下发射键过快时不出现问题
{
_bulletList[i].SetActive(true); // 挨个激活
i++;
}
}
else
{
i = 0; // 激活一轮以后再从头开始
}
}
private void CreatBulletList(int bulletCount)
{
for(int j = 0; j < bulletCount; j++)
{
GameObject bullet = Instantiate(_bulletPrefab); // 生成游戏对象实例
bullet.transform.parent = _bulletContainer.transform; // 设置母文件夹
bullet.SetActive(false); // 生成后即刻灭活,不显示出来
_bulletList.Add(bullet); // 添加到列表中(池子中)
}
}
// Start is called before the first frame update
void Start()
{
CreatBulletList(10);
}
// Update is called once per frame
void Update()
{
}
}
创建游戏对象 BulletContainer,拖拽赋值到 PoolManager 脚本下面的 Bullet Container 框中。
运行后可实现预期效果。
Challenge: Request from Pool Manager
- 任务说明:
- 创建子弹的对象池
- Player 每次按下空格键,从对象池获取一个未激活的子弹,并进行激活
- 如果对象池中没有未激活的子弹,则生成一个子弹, 并额外添加到对象池中
PoolManager 脚本,挂载到 PoolManager 游戏对象上:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoBehaviour
{
[SerializeField] // 在 inspector 中可见,用于拖拽赋值
private GameObject _bulletPrefab; // 子弹的 prefab 需要赋值
// 以下为 PoolManager 的 singleton 定义部分,用于简易调用
private static PoolManager _instance;
public static PoolManager Instance
{
get
{
if (_instance == null)
{
Debug.LogError("no instance");
}
return _instance;
}
}
private void Awake()
{
_instance = this;
}
// 以下用于定义 Bullet 的对象池
[SerializeField]
private List<GameObject> _bulletList;
[SerializeField]
private GameObject _bulletContainer; // 生成的子弹游戏对象汇总到一个母文件下面
public GameObject CreatBullet()
{
foreach(var bullet in _bulletList) // 遍历子弹池子
{
if (bullet.activeSelf == false) // 如果还有没被激活的子弹
{
bullet.SetActive(true); // 激活
return bullet; // 跳出该方法
}
}
// 如果所有子弹都被激活了还不够用,则生成新的子弹,并添加到子弹池子
GameObject newBullet = Instantiate(_bulletPrefab); // 生成游戏对象实例,并激活
newBullet.transform.parent = _bulletContainer.transform; // 设置母文件夹
_bulletList.Add(newBullet); // 添加到列表中(池子中)
return newBullet; // 跳出该方法
}
private void CreatBulletList(int bulletCount)
{
for(int j = 0; j < bulletCount; j++)
{
GameObject bullet = Instantiate(_bulletPrefab); // 生成游戏对象实例
bullet.transform.parent = _bulletContainer.transform; // 设置母文件夹
bullet.SetActive(false); // 生成后即刻灭活,不显示出来
_bulletList.Add(bullet); // 添加到列表中(池子中)
}
}
// Start is called before the first frame update
void Start()
{
CreatBulletList(10);
}
// Update is called once per frame
void Update()
{
}
}
Player 脚本,挂载到 Player 游戏对象上:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) // 按下空格键表示开枪
{
_ = PoolManager.Instance.CreatBullet();
// 调用 PoolManager 生成子弹
// 游戏中如果需要对这个子弹产生位移等操作,代码也可以放在这里
}
}
}
Bullet 脚本,挂载到 Bullet 的 prefab 上:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnEnable() // 游戏对象被激活的时候调用
{
StartCoroutine(bulletDis()); // 启动协程
}
IEnumerator bulletDis() // 定义协程
{
yield return new WaitForSeconds(2); // 等待2秒
this.gameObject.SetActive(false); // Bullet灭活
}
}
创建游戏对象 BulletContainer,拖拽赋值到 PoolManager 脚本下面的 Bullet Container 框中。
Recycle the Pool
- 循环使用游戏对象池的另一种方法
前面我们提到了使用协程可以达到循环使用对象池,其实我们还有别的选择:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnEnable() // 游戏对象被激活的时候调用
{
Invoke("Inactive",2); // 2秒以后运行 Inactive 方法
}
private void Inactive() // 反激活
{
this.gameObject.SetActive(false);
}
}