前篇链接:Unity之C#学习笔记(16):单例模式及单例模板类 Singleton and MonoSingleton
在游戏中,有一些生命周期很短,需要频繁创建和销毁的物体,例如射击游戏中的子弹。按一般做法,我们也需要频繁地实例化(Instantiate)和销毁(Destroy)这些物体,这其实是有一定开销的。对象池模式的思想就是创建容纳了一些物体的“池”,需要时从中取一个,用完了再放回去,循环利用,减少生成和销毁物体的次数,优化性能。
来看一个例子:我们现在有子弹的prefab,按下空格键时,生成一个子弹,子弹过一秒自动消失。传统的做法是:
GameManager:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public GameObject bulletPrefab;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Instantiate(bulletPrefab);
}
}
}
Bullet:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
void Start()
{
Destroy(this.gameObject, 1.0f); // 创建后延迟1秒销毁
}
}
实验一下会发现,即使按最快速度按空格,场景中大概也只能生成7个左右的子弹。也就是说,我们只需要一个容纳7个子弹实体的池就足够了。在实际场景中,池的容量自然是要根据游戏逻辑确定。创建PoolManager脚本,用List实现对象池。在Start函数中初始化7个GameObject并放在List中:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PoolManager : MonoSingleton<PoolManager>
{
public GameObject bulletPrefab;
private List<GameObject> _bulletPool;
void Start()
{
_bulletPool = GenerateBullets(7);
}
List<GameObject> GenerateBullets(int amount)
{
List<GameObject> pool = new List<GameObject>();
for (int i = 0; i < amount; i++)
{
GameObject bullet = Instantiate(bulletPrefab);
bullet.SetActive(false);
pool.Add(bullet);
}
return pool;
}
PoolManager使用单例模式,关于单例模式和单例模板类,可以参考上一篇文章
PoolManager对外提供获取对象的接口。获取对象时,从List中找到一个空闲状态的GameObject并返回。如果找不到空闲的,说明池子中所有对象都被占用了,需要扩充容量,创建新的对象放入池子。在实现中,我们不需要真的模拟将对象从池子中“取走”,“放回”的过程,而可以用一个状态量判断某个对象是否空闲。例如这里最简单的,我们用Active属性决定。
public GameObject RequestBullet()
{
foreach (var bullet in _bulletPool)
{
if (bullet.activeInHierarchy == false) // 找到一个未使用的对象
{
bullet.SetActive(true); // Active设为true,表示被占用
return bullet;
}
}
// 池子内的对象用光了!创建一个新的
// 如果不够用的情况经常发生,考虑一次扩充更多的容量,例如一次性将池子扩充为1.5倍
GameObject newBullet = Instantiate(bulletPrefab);
_bulletPool.Add(newBullet);
return newBullet;
}
在外部获取一个对象:
if (Input.GetKeyDown(KeyCode.Space))
{
GameObject bullet = PoolManager.Instance.RequestBullet();
// 对bullet对象做一些其它操作,例如移动,旋转...
}
回收对象时,由于本例中我们是使用物体的Active属性判断是否空闲,故只需SetActive(false)即可。
Bullet:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
private void OnEnable() // 对象设为Active时被调用
{
Invoke("Die", 1); // 延迟1秒调用Die
}
void Die()
{
this.gameObject.SetActive(false);
}
}
实际应用中,管理的对象会更复杂,分配和回收可能涉及其他相关资源的处理。占用和回收的条件也不能只用简单的SetActive实现,需要设计更合适的数据结构。