思考并回答以下问题:
- 什么是对象缓冲池技术?是为了解决什么问题?
- 怎么使用缓冲池技术?
本章涵盖:
- 简单对象缓冲池技术
- 高级对象缓冲池技术
对象缓冲池技术是游戏开发领域的一个高级知识点,它的出现主要解决游戏开发过程中由于大量游戏道具的生成与销毁而造成系统瓶颈的问题。关于游戏项目中的性能优化策略,我们使用了整整一章项目研发常用优化策略进行详细的讨论与论述,而本章所要讨论的问题其实就是Unity引擎中针对脚本性能优化而推荐的优秀解决方案。
预加载是整个对象缓冲池技术的实现原理与实现前提,本章使用两个优秀的示例,具体讲解两类对象缓冲池的实现原理与使用方法。
概述
对象缓冲池技术不是Unity所专用的技术,它来自软件开发中对于数据库的优化方案,从“数据库连接缓冲池”经过改造与变化而来。由于在做CS/BS软件开发的过程中,我们写的软件系统都一定要与数据库打交道,而频繁地访问数据库又是一件非常消耗数据库资源的问题(数据库服务器能同时被“连接”访问的次数是有限的,而且系统申请连接数据库也是比较耗费时间的)。所以软件开发领域出现了“数据库连接缓冲池”技术,也就是系统中首先提前建立一个数据库连接访问“池”(本质就是一个“集合”,如List<>),“池”中预先存放了一定数量的连接数据库的实例,当我们在需要使用数据库连接的时候,首先查看“池”中是否有现成的“连接实例”,如果有,直接从“池”中获取即可,避免了反复申请连接与断连接等费事操作,大大提高了系统效率。
游戏开发项目中存在相同的问题,而且或许更加突出与严峻。我们的游戏项目如果要开发的“高大上”,则必然需要使用大量的“次时代”游戏道具与大量的游戏粒子特效等。但是大量的游戏道具与粒子系统的出现,必然会导致频繁的创建、克隆、销毁游戏对象,这就很大程度地消耗了大量的CPU资源,使得游戏出现明显的“卡顿”现象。
后来,聪明的游戏研发人员就考虑是否把需要销毁的游戏对象进行“隐藏”,也就是可以使用脚本“GameObject.SetActive(false)”禁用游戏对象,当我们再次需要使用此游戏对象的时候则进行启用即可(使用GameObjet.SetActive(true)),这就形成了一个优化的基本观点,使用“隐藏”代替“销毁”.
再后来,我们就考虑是否在真正的游戏场景开始之前就把需要用到的大量游戏对象与粒子系统等提前加载到一个“池”中,然后需要的时候再直接从“池”中取得,避免使用的时候进行大量“克隆”操作,影响系统效率,造成游戏操作过程中的“卡顿”现象。于是这就形成了“预加载”的概念。
把以上两种方式(“禁用”、“启用”脚本与“预加载”)结合起来,形成了我们今天要研究的“对象缓冲池”的基本原理。
简单对象缓冲池技术
如图1所示,我们使用一个“射击木箱靶墙”的小场景,结合一个最简单的对象缓冲池脚本,演示与说明简单对象缓冲池的具体实现原理和基本应用方法。
首先我们研究与解释一个最简单的对象缓冲池脚本,如代码清单1所示。
代码清单1 ObjectPoolManager.cs
1 | /* |
从ObjectPoolManager.cs脚本的头注释中可以清楚地查看到本脚本的基本原理说明:在游戏初始化的时候,生成一个初始的“池”,存放我们要复用的元素。当要用到游戏对象时,从池中取出。不再需要的时候不直接删除对象,而是把对象重新回收到“池”中,这样避免了对内存中大量对象的反复实例化与回收垃圾处理,提高了资源利用率。
脚本字段定义中比较重要的是“_totalObjList”,它是实际存储“池”元素的容器集合,而“_avaliableIndex”则是记录当前“池”中所有可以使用的“池”元素下标数值。
代码清单2
1 | /// <summary> |
代码清单2中定义的Awake()事件函数的作用是初始化“池”容器集合和初始化“池”,即expandPool()方法实例化指定数量的被禁用的“游戏对象预设”,提前放入集合中。代码清单2中的PickObj()方法是从“池”中取得一个可用的游戏对象(预设)。其原理如代码清单2中的26行代码所示,把指定id序列号的游戏对象预设“激活”(使用“SetActive(true)”)。
代码清单3
1 | /// <summary> |
代码清单3中的RecyleObj()方法回收指定id序列号的游戏对象重新回到“池”中,本质就是代码清单3中的第7行代码所示的,把指定序列号的游戏对象预设重新禁用。14-35行代码是expandPool()扩展池方法,其目的是当游戏刚开始或者“池”中的元素不够使用的时候,扩展容器类中元素的数量。23行代码使用Instantiate()方法通过复制“原型预设”(即变量“ObjPrefab”)的方法扩充新的游戏对象预设元素,加入到“_totalObjList”这个容器类集合中,其查找用的元素下标数值则存储在“_avaliableIndex”集合中。
这样,一个最简单的“对象缓冲池”脚本就解释完毕了,如何使用呢?需要读者在理解上述脚本的原理上,写其他的测试脚本,通过调用相关方法来使用。也就是说,核心的“对象缓冲池”脚本只是一个“中间件”,不能单独运行,需要我们写脚本来使用。如代码清单4所示,我们使用本书之前讲解的射击场景,运用和测试对象缓冲池的实际效用。
代码清单4 ShottingUseBufferPool.cs
1 |
代码清单4中的ShottingUseBufferPool.cs脚本是在本书之前提及的RayDemo.cs脚本基础上加入“对象缓冲池”技术改造升级之后的脚本。其中代码清单4中的24-27行代码是关于运用“对象缓冲池”新加入的字段。
代码清单5
1 |
代码清单5中,Start()事件函数的主要作用是建立“射击靶墙”,OnGUI()事件函数的作用是绘制射击光标。
代码清单6
1 |
代码清单6中的第73行代码取代原先的创建“子弹”,而是从“缓冲池”取得已经存在的“预设对象”,即调用PickObj,第76行代码SendMessage()函数的作用是传递子弹在缓冲池中的“序号”给“子弹”所在的对象脚本,目的是当子弹超出摄像机视野范围的时候主动“回收”子弹实例。
代码清单7
1 |
代码清单7中使用OnBecamelnvisible()方法实现当子弹离开摄像机视野范围时,子弹再次自动回收到“缓冲池”中的操作。代码清单7中的ReceiveBulletID()方法是接收ShottingUseBufferPool.cs脚本中用SendMessage()方法传来的子弹序号数据,用于回收子弹使用。
以上3个核心脚本介绍完毕,请读者查看随书第27章的示例项目,但程序运行之后,我们就会发现在正式开始射击之前,所有指定数量的“子弹预设”就已经创建完毕了,只不过是“禁用”状态。当开始射击的时候,会发现随着子弹的射击,其“子弹缓冲池”列表中的子弹预设对象其状态(“启用”或者“禁用”)也在不断的变化。
高级对象缓冲池技术
上面我们介绍了简单对象缓冲池的实现原理与实验项目,读者能否发现有什么不方便使用的地方,或者功能需求上的缺失呢?
之前介绍的对象缓冲池的不足之处,如下所示。
第1条:使用对象缓冲池的时候必须把一个名为“缓冲对象id号”传给“缓冲对象”所在的脚本,否则不能正确回收“对象”。
第2条:对象缓冲池不支持多“类”对象的缓冲池处理,即如果射击项目中增加“子弹”、“飞弹”、“炮弹”等不同种类的游戏对象,就不能直接应用了。
第3条:对象缓冲池中,对象必须明确回收的条件,不能自动按照时间进行“回收”处理。例如,跑酷游戏中,大量的“红宝石”等道具如果用缓冲池,就需要具备一定时间以后自动回收的功能实现。
为了解决以上简单对象缓冲池的不足,我们来研发更加高级的对象缓冲池, 目的就是既要使用简单,又要功能强大。
图27.10就是应用高级对象缓冲池的实验项目,从图中的方框中可以看出这个对象缓冲.池支持多类型对象缓冲处理,而且本实验项目中已经有了按照指定时间自动“回收”缓冲 现在就从代码的层级来介绍这个缓冲池的构成与基本原理。功能强大但脚本相对比较简单,核心仅2个脚本和4个类,结构如下。
1)脚本: “Pools. cs”
包含以下3个类。
类: Pools 作用:负责多个类型对象缓冲器的实现。
类: PoolOption 作用:负责单类型对象缓冲器的实现。
类: PoolTimeObject 作用:辅助功能,时间处理。
2)脚本: “Pool Manager. cs”
类: PoolManager 作用:负责多个类型复合对象缓冲器,使用到前面3个类的方法调用。
图27.11给出了Pools, cs脚本完整的头注释,利于相互学习与借鉴。
图27.12给出了脚本中包含的3个类的简略定义。为了使读者更好地理解,我们先从类”PoolOption” (图27.12代码中的第178行)开始介绍,这个类是负责单个类型对象缓冲器的具体代码实现。
图27.13中定义了PoolOption类的字段与“预加载” (Preload)方法,图中183行与185行代码中的”ActiveCameObjectArray”、 “InactiveCameObjectArray”是核心容器类集合,分别存放“活动游戏对象(预设)”与“非活动游戏对象(预设)”。其中, “预加载”方法的作用是克隆指定游戏对象,然后重命名后对此对象做“禁用”处理(图中代码200行),最后加入到“非活动游戏对象”集合中。
图27.14中的“激活游戏对象” (Active)是Pooloption类的重要方法。当我们需要从缓冲池中提取出一个游戏对象时,首先从“非活动游戏集合”容器中取出下标为0的游戏对象,然后经过方位调整后(即游戏对象应有的位置、旋转、缩放等信息)加入“活动池”容器中,且正式“启用”此对象(设置SetActive (true) ),然后返回。
图27. 15中的”Deactive”是“禁用游戏对象”方法,通过活动与非活动集合的操作,以及设置对象“禁用”来实现。图27. 15中的其他方法以统计集合中的数据为重点,这里为了突出基本原理,不做介绍。
关于PoolOption类的重要功能就介绍到这里。为了使此对象缓冲池可以容纳多种类型的游戏对象,我们在PoolOption类的基础上又“包裹”了一层代码,即Pools类,从而实现对多种游戏对象类型的支持,代码如图27.16所示。
图27.16中的第33行代码就是包含PoolOption类的泛型集合,从而实现多种类型对象缓冲的支持。图中第40行代码中的PeloadGameObjet()是预加载方法,即多种类游戏对象的预加载。
图27.17中的PreLoadGameObject ()与BirthGameObject()分别是Pools类中关于“多模”对象缓冲池中的“预加载”与“生成单个游戏对象”的方法,其基本原理是通过调用Pool-Option中的相关方法来实现的
图27, 18中的RecoverGameObject ( )方法是“多模”状态下的“同收”缓冲池对象
方法。
下面我们来介绍Pools类中关于定时回收对象的基本原理,如图27.19所示。
图27. 19中的ProcessCameObject NameTime ( )是Pools类中的“时间截”方法,即系统根据开发人员的要求指定特定种类游戏对象的存活时间,到时间就会自动回收缓冲池对象的机制。
图27.21中的”PoolManager”与子对象”RedDiamend”、”_Tree”都是建立的空对象,”PoolManager”对象上赋值”PoolManager. cs”脚本,两个子空对象赋值”Pools. cs”脚本。
我们使用前面学习的“跑酷”项目作为测试实验项目,其调用方式如图27.22和图27.23所示。
图27.22中的第76行代码表明使用缓冲池的方法,首先确定缓冲池的类型名称,这里用”strPoolsTypeObjName”动态获得,然后调用(Pools类中) BirthGameObject ()方法生成缓冲池对象,当然,这是对应于初始缓冲池中的对象不够用时才进行调用的。
图27.23中,第51行代码定义了回收缓冲池游戏对象的方法,写法与生成游戏对象的写法类似。
本章练习与总结
本章我们学习了对象缓冲池技术,此技术是基于Unity引擎中非常重要的性能优化手段。首先我们介绍了“预加载”的概念,然后介绍了对象缓冲池的来历与基本原理。在此基础上我们提供了两种截然不同、功能高、低搭配的两种缓冲池技术实现,使用两个不同的示例,演示了两种缓冲池在不同项目中的具体部署与代码使用方式。