本篇中使用的API:gameObject.CompareTag("标签")、UnityEvent()事件管理、ObjectPool<GameObject>()对象池
参照unity官方教程:Hi ObjectPool
目录
1. 应用场景
2. 对象池的原理
3. 查看资源消耗情况
4. 不使用对象池实现的情形
5. 使用对象池
6. 是否使用对象池的比较
1. 应用场景
对象池管理的作用场景是当短时间出现大量对象时,合理管理对象的出现、消亡,从而优化内存、GPU性能管理,不至于出现卡顿等现象,这在基于移动平台的场景中尤为明显。比如说下面这个游戏结束时,大量奖品突然涌现的画面:
另外,还有在游戏中经常看到的,使用到大量子弹、受到大量怪兽攻击等的画面。
2. 对象池的原理
在上面的场景中,如果在短时间内大量地创建和销毁重复的对象,就会消耗大量资源,并产生大量的垃圾。在垃圾回收的时候,会中断执行程序代码,直到回收完成。这个时间可能持续1ms~几百ms,在PC上可能直观感受不明显,但到移动端、VR一体机或者其他低端终端运行时就会看到游戏的卡顿。
因此,现在的Unity使用了ObjectPool的思想,即把场景中需要大量出现的GameObject存入一个数据集(字典或数组),就像一个池子一样。需要在场景中呈现时就激活它——SetActive(true),而不是重新去创建,不用了就让它失活——SetActive(false),而不是销毁它。
3. 查看资源消耗情况
以下比较使用和不使用对象池的情况,对于运行时资源占用的情况,可以在菜单Windows->Analysis->Profiler查看,可以选择Scripts(脚本)和GarbageCollector(垃圾回收),重点查看这两项:
4. 不使用对象池实现的情形
如果不使用对象池管理,那么在Update()中会不断地使用Instantiate实例化物体,并且用Destroy()销毁物体,会让系统不断地分配内存和回收内存。下面写一个不使用对象池管理的例子,包含Trophy.cs和TrophyManager.cs两段代码。
(1)建立简单测试场景:在场景中建立一个空节点(Trophy),把需要大量显示的对象都放在它底下,这些对象每一个都要假设Rigidbody和Collider,等下需要它们触地消失。
(2)Trophy代码:动态地挂在每个奖品上,其中:
定义一个事件destroyEvent,用于触发TrophyManager中的销毁(Destroy)回调;
使用一个OnCollisionEnter控制奖品接触其他碰撞器后, 激活destroyEvent事件。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;//使用事件时,需要加载这个命名空间
//对象:控制每个奖品,动态地挂载在每一个礼物上
//作用:让礼物碰到其他碰撞器就消失
public class Trophy : MonoBehaviour
{
public UnityEvent destroyEvent=new UnityEvent();
//公开定义一个用于销毁的事件,在TrophyManager中监听它并调用Destroy
public bool isDestroy = false;
//判断是否已经销毁,以免重复触碰不同的Collider多次销毁
public void OnEnable()
{//Trophy有效时(不管是激活的还是新创建的),先把isDestroy标记置为false
isDestroy = false;
}
private void OnCollisionEnter(Collision collision)
{//碰撞事件
if(collision.gameObject.CompareTag("Ground") &&!isDestroy)
{//如果碰到了标签为"Ground"的碰撞器,且Trophy本身也是有效状态
isDestroy = true;
destroyEvent?.Invoke(); //触发销毁事件
}
}
}
(3)TrophyManager代码:这里只要做两个步骤:第一步,创建新的Trophy;第二步:监听到销毁事件后,销毁Trophy。
public class TrophyManager : MonoBehaviour
{
public GameObject[] trophies; //建立一个数组,用于存放所有的trophy对象
public int number = 50; //定义在场上新建的trophy对象的数量
void Update()
{
for(int i=0;i<number;i++)
{
var trophy=Instantiate(trophies[Random.Range(0, trophies.Length)], transform);
//实例化对象到父物体(挂载TrophyManager脚本的对象)下
trophy.transform.localPosition= Random.insideUnitSphere;
//Random.insideUnitSphere返回半径为1的球体内的一个随机点
trophy.AddComponent<Trophy>().destroyEvent.AddListener(() =>
{ //监听Trophy脚本中的销毁事件
Destroy(Trophy);
});
这个脚本挂在所有奖品的父节点上,并将所有奖品对象拖入数组当中。这样就能执行礼物喷涌的效果。
5. 使用对象池
使用Unity定义的对象池,需要在开始使用一下命名空间:using UnityEngine.Pool;
Trophy.cs代码不做更改,TrophyManager.cs代码修改如下
public class TrophyManager : MonoBehaviour
{
public GameObject[] trophies;
public int number = 50;
private ObjectPool<GameObject> trophyPool; //定义一个对象池
void Start()
{
trophyPool = new ObjectPool<GameObject>(createFunc: () =>
{ //建立Trophy的对象池trophyPool,类型为<GameObject>类型
//设置对象池的回调
var trophy = Instantiate(trophies[Random.Range(0, trophies.Length)], transform);
trophy.AddComponent<Trophy>().destroyEvent.AddListener(call: () =>
{ //监听Trophy脚本的destroyEvent事件
trophyPool.Release(trophy); //不销毁,而是在对象池中回收
});
return trophy;
},
actionOnGet: (go) =>
{ //调用时激活
go.SetActive(true);
go.transform.localPosition = Random.insideUnitSphere;
},
actionOnRelease: (go) =>
{ //失活
go.SetActive(false);
},
actionOnDestroy: (go) =>
{ //销毁
Destroy(go);
});
}
这个λ表达式比较长,主要是因为对象池的声明就很长,包括了建立对象池、调用、释放、销毁、检查内容、初始容量、最大容量这些参数:
public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000)
6. 是否使用对象池的比较
如果在PC端运行,上面两段代码的区别并不能直观感受出来,可以打开Profiler查看一下资源占用的情况,下面左图是不使用对象池,右图使用了对象池,可以看到GC垃圾回收数量右边明显少于左边。