绝大部分游戏需要涉及到同一个预制体的反复生成和销毁,比如
如果使用Instantiate(GameObject)
生成对象,需要从硬盘或缓存中拷贝对应的预制体到内存中。如果使用Destroy(GameObject)
销毁对象,同样需要清空对应的内存区域,这些操作会增加CPU,内存和硬盘的消耗,使游戏性能降低。
实际上,如果使用上述方式管理对象,每个对象只会被使用一次。如果能够重复使用这些对象,就能降低生成和销毁的次数,进而减少性能消耗。
对象池就是基于这个思想的设计。如果要生成对象,不使用Instantiate(GameObject)
直接生成,而是从对象池中复用对应的对象,对象池为空时再生成新对象。如果要销毁对象,不使用Destroy(GameObject)
直接销毁,而是将对象放进对象池中,对象池满了再销毁旧对象。
一般来说,对象池有通用池和专用池两种类型
两种方式各有优劣,通用池需要单独设计查找和缓存算法,性能上略低于专用池,但扩展容易,适合需要长时间运营和更新的游戏。
专用池不需要设计查找和缓存算法,设计起来比较简单,但每一个对象都要设置专用的对象池,扩展比较麻烦,适合独立游戏或玩法固定的游戏。
Instantiate(GameObject)
生成的对象相比存在差异。Start()
函数从而产生一些未初始化的问题这里以专用池为例子,以简述对象池的需求。
在场景中建立空物体,并挂载对象池类,其中这个空物体的子级作为对象池。
对象池类需要有以下功能:
Instantiate(GameObject)
生成新对象初始化函数可以使用OnEnable实现,这个函数在物体启用时自动执行。
对象池类,需要挂载在对应的对象池空物体上。
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public int MaxSize = 128; //对象池的上限
public GameObject gameObjectType; //对象池用来管理的对象
public int CacheSize = 16; //对象池预先缓存的对象
private int size;
// 在Start函数中生成预缓存对象
void Start()
{
size = CacheSize;
if (CacheSize > MaxSize)
Debug.LogError(string.Format("缓存大小Cache{0}超出对象池大小MaxSize{1},请指定小于{1}的值!", CacheSize, MaxSize));
for (int i = 0; i < CacheSize; i++)
{
GameObject obj = Instantiate(gameObjectType);
obj.transform.parent = transform;
obj.SetActive(false);
}
}
public GameObject Instantiate(Vector3 position, Quaternion rotation)
{
GameObject obj0;
if (size == 0) //对象池没有对象,添加新对象
{
obj0 = Instantiate(gameObjectType, position, rotation);
}
else //对象池中有对象,把对象释放出来
{
obj0 = transform.GetChild(0).gameObject;
obj0.transform.position = position;
obj0.transform.rotation = rotation;
obj0.SetActive(true);
obj0.transform.parent = null;
size--;
}
return obj0;
}
public void Destroy(GameObject obj)
{
if (size == MaxSize)
{
Destroy((Object)transform.GetChild(0).gameObject);
size--;
}
obj.SetActive(false);
obj.transform.parent = transform;
size++;
}
}
使用起来非常简单,只需要给涉及生成和销毁的对象挂载对象池脚本,给需要使用对象池的对象所属脚本重写OnEnable()
,再替换原本的Instantiate(GameObject)
和Destroy(GameObject)
函数即可。
挂在枪口上的脚本
using UnityEngine;
public class Gun: MonoBehaviour
{
public Transform bullet;
void Start(){}
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0))
Instantiate(bullet,transform.position, transform.rotation);
}
}
挂在子弹预制体上的脚本
using UnityEngine;
public class Bullet: MonoBehaviour
{
public float speed;
public Transform gun;
void Start(){
gun = GameObject.Find("这里写枪的名字").transform;
}
void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
void FixedUpdate() //超出300米左右销毁,这里使用包围盒检测
{
if (new Bounds(gun.position, new Vector3(300, 300, 300)).Contains(transform.position))
return;
Destroy(gameObject);
}
private void OnTriggerEnter(Collider other)
{
if(other.tag == "Enemy")
{
Destroy(gameObject);
}
}
挂在枪口上的脚本
using UnityEngine;
public class Gun: MonoBehaviour
{
public Transform bullet;
public ObjectPool pool;
void Start(){
//初始化操作
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse0))
pool.Instantiate(transform.position, transform.rotation);
}
}
挂在子弹预制体上的脚本
using UnityEngine;
public class Bullet: MonoBehaviour
{
public float speed;
public Transform gun;
public ObjectPool pool;
void Start(){
//初始化操作
}
//重写OnEnabled()以实现对象池插入的初始化功能
void OnEnable()
{
Start();
}
void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
void FixedUpdate() //超出300米左右销毁,这里使用包围盒检测
{
if (new Bounds(gun.position, new Vector3(300, 300, 300)).Contains(transform.position))
return;
pool.Destroy(gameObject);
}
private void OnTriggerEnter(Collider other)
{
if(other.tag == "Enemy")
{
pool.Destroy(gameObject);
}
}
2022-4-8日投稿
2022-4-12日第一次修订,简化了初始化的过程