本文由游戏玄学翻译社制作发布!翻译:太昊,兔角鹿,小木曾;Review: 风雨时;统筹润色:杨雍
想象一下:你正在!!哦,不,测试你最新的和最棒的一个射击游戏。敌人在以你能掌握的最快速度来回飞行,然后,砰!卡了一帧之后,你就被凶神恶煞的外星人手打成了翔。
这可是场横扫千军的战斗,不应该由于莫名其妙的内存尖峰左右战斗的结果。你是不是也曾经因为这个问题输掉?来来来,搬个小马扎,听我来扒一扒 对象池技术 吧。
在这篇Unity教程中,你将学到:
- 所有关于对象池技术的内容
- 如何将一个game object入池
- 如何在运行时按需扩展对象池
- 如何扩展对象池以适应不同的对象
在教程的最后,你会得到一个可以得到新游戏的全部代码。而且,你会懂得如何为现有的游戏改进这个代码。
预备知识:你需要熟悉C#基础并且知道如何操作Unity的开发环境。
什么是对象池技术?
Instantiate() 和 Destroy() 是在游戏流程中好用而必备的方法。(通常情况下,单独调用这两个方法只占用CPU相当微小的时间。)
然而,对于在游戏流程中生命周期短暂而且每秒大量摧毁的对象群而言,CPU进行内存分配的时间占用十分显著。
![154306go00osrrvenr0g0r.png](https://di.gameres.com/attachment/forum/201703/07/154306go00osrrvenr0g0r.png)
就是一个适合入池的GameObject的好例子。
并且,Unity使用垃圾收集(Garbage Collection)技术来释放不需要继续使用的内存。不断调用Destroy()会频繁的激活收集,而这会拖慢CPU导致游戏流程的卡顿。
这一行为在移动设备和网页等资源受限的环境下中尤为致命。
对象池技术将尚未用到的对象放在游戏流程之前——比如在读取界面的时候——进行预先实例化。这样游戏可以从『池子』中重用对象,而不需在游戏流程中不断地创建和销毁。
![154306xhp3pxp43152mhuc.png](https://di.gameres.com/attachment/forum/201703/07/154306xhp3pxp43152mhuc.png)
开始
如果还没有Unity5或者更新版本,从 Unity官网下载。
然后下载 起始项目,解压并在Unity里打开 SuperRetroShooter_Starter项目——这是一个预先建好的纵向卷轴射击项目。
注意:美术资源的版权属于 OpenGameArt的 Master484, Marcus, Luis Zuno 和 Skorpio。免授权音乐则来自于杰出的Bensound
随意看看里面的脚本吧,比如Barrier;他们都非常实用,不过这篇教程就不对它们详细阐述了。
在浏览这些时,注意在游戏流程中Hierarchy(栏)里都发生了些什么是很有帮助的。因此我建议取消Game标签(Game Tab)工具栏(* toolbar )里的Maximize on Play*选项。
![154307gnbugcqjwco6c99g.png](https://di.gameres.com/attachment/forum/201703/07/154307gnbugcqjwco6c99g.png)
点击play按钮看看。 :]
注意当发射时,在Hierarchy(栏)里实例化了大量的PlayerBullet(Clone)。当击中敌人或者离开屏幕时,他们会被销毁。
更糟的是收集那些掉落的强化道具,他们迅速用的复制体填满了Hierarchy(栏),随后又在下一秒里立刻销毁它们。
![154309iu8mw1rjg0z8oltl.png](https://di.gameres.com/attachment/forum/201703/07/154309iu8mw1rjg0z8oltl.png)
干得好!你现在拥有一个对象池了 :]
深入对象池
回到ObjectPooler脚本然后添加以下代码:
public GameObject GetPooledObject() {
//1
for (int i = 0; ipooledObjects.Count; i++) {
//2
if (!pooledObjects.activeInHierarchy) {
return pooledObjects;
}
}
//3
return null;
}
首先要注意的是这个方法现在的返回值类型是GameObject而非void。这是指别的脚本可以向GetPooledObject请求一个池中的对象,而他会返回一个GameObject作为回应。其他会发生的事情如下:
1.这个方法使用for循环遍历你的pooledObjects列表。
2.检查其中一个对象是否并非激活(active)状态。如果是激活的,循环到下一个。如果是非激活的,退出方法并把这个对象提交给对GetPooledObject的调用者。
3.如果目前没有非激活状态的对象,退出方法并且不返回任何东西。
现在,你可以向池子请求对象了,这需要替换掉原有的的实例化和销毁代码,以对象池替代。
玩家的会在ShipController脚本里的两个方法中(被)实例化。
- Shoot()
- ActivateScatterShotTurret()
在MonoDevelop里打开ShipController脚本(并找到下列代码):
Instantiate(playerBullet, turret.transform.position, turret.transform.rotation);
把两处实例都用以下代码替换:
GameObject bullet = ObjectPooler.SharedInstance.GetPooledObject();
if (bullet != null) {
bullet.transform.position = turret.transform.position;
bullet.transform.rotation = turret.transform.rotation;
bullet.SetActive(true);
}
注意: 在继续之前,确保在Shoot()和ActivateScatterShotTurret()方法中也做好了这样的替换。
在之前,这些方法遍历玩家飞船上的激活着的列表((基于加成道具)),然后在枪口位置与角度上实例化。
而现在改成了询问ObjectPooler脚本获取一个池中的对象。如果拿到了,将它设置到枪口的位置与角度,然后设为active状态来向敌人倾泻下你的弹火吧:]
归还对象池
当你不再需要使用的时候,将它们归还到池中,而不是将对象销毁。
有两个方法可以销毁不再需要的玩家:
- net当移出屏幕时,调用DestroyByBoundary中的OnTriggerExit2D() 方法移除它们。
- OnTriggerEnter2D() in the EnemyDroneController script removes the bullet when it collides and destroys an enemy.
*当碰撞并摧毁敌人时,调用EnemyDroneController中的OnTriggerEnter2D()方法移除它们。
在MonoDevelop中打开DestroyByBoundary,并将OnTriggerExit2D方法替换成如下代码:
- if (other.gameObject.tag == Boundary) {
- if (gameObject.tag == Player Bullet) {
- gameObject.SetActive(false);
- } else {
- Destroy(gameObject);
- }
- }
这里有个坑:使用对象池模式时,注意池中对象的生命周期会和之前有所不同。
好了,你可以点击运行了。^_^
当你射击时,在Hierarchy中的玩家自动从未激活状态变为激活状态。它们会在移出屏幕、或击中敌机的时候,以优雅代码应有的方式变成回未激活的状态。
干的漂亮!
![154312pmeoog5zroeg4kfm.png](https://di.gameres.com/attachment/forum/201703/07/154312pmeoog5zroeg4kfm.png)
超级扩展对象池
现在你将学习如何修改对象池,以在运行时按需增加池中的对象数量。
在MonoDevelop中打开ObjectPooler脚本,并将以下代码加入到公有变量中:
public bool shouldExpand = true;
这个代在Inspector中创建一个复选框,用来决定是否可以增加池中对象的数量。
在GetPooledObject()中,将return null;修改为如下代码:
- if (shouldExpand) {
- GameObject obj = (GameObject)Instantiate(objectToPool);
- obj.SetActive(false);
- pooledObjects.Add(obj);
- return obj;
- } else {
- return null;
- }
- return null;
- } else {
- return obj;
- pooledObjects.Add(obj);
- obj.SetActive(false);
- GameObject obj = (GameObject)Instantiate(objectToPool);
对象池群
Invariably, lots of bullets mean lots of enemies, lots of explosions, lots of enemy bullets and so on.
通常情况下,大量的就意味着大量的敌人,不停的爆炸效果,敌人们大量发射的,以及其他方方面面开销。
要为这场疯狂屠戮做准备,你需要扩展你的对象池以便于处理复杂的对象类型。接下来的一步,将实现在Inspector中的相同位置调节 不同类型自带的参数。
在ObjectPooler类的前面加入以下代码:
- [System.Serializable]
- public class ObjectPoolItem {
- }
- public class ObjectPoolItem {
点击Play来确保别的东西没有被乱动过。:]
好!现在你已经学会怎么向对象池里添加新的对象了。
将ItemsToPool 的大小改为3,然后新加入两类敌人的舰船。按照如下内容来调整ItemsToPool 的实例参数。
Element 1:
Object to Pool: EnemyDroneType1
Amount To Pool: 6
Should Expand: Unchecked
Element 2
Object to Pool: EnemyDroneType2
Amount to Pool: 6
Should Expand: Unchecked
![1543153i9g33l35l9j1098.png](https://di.gameres.com/attachment/forum/201703/07/1543153i9g33l35l9j1098.png)
如处理时一样,你需要改变这两类敌舰instantiate和destroy的方法。
敌舰在执行GameController脚本时进行实例化,并在EnemyDroneController这一脚本中销毁。
之前已经做过一次了,所以这里我们会快一点。-w-
打开GameController脚本。在SpawnEnemyWaves()中找到enemyType1 instantiation code:
- Instantiate(enemyType1, spawnPosition, spawnRotation);
何去何从
在这个教程里,你了改写一个现有的游戏,通过给其加入对象池的方式,从可以预期的过载中拯救玩家的CPU,并解决了因此而会产生的跳帧和电池过热问题。
你在脚本的切换和内容整合上熟练度+5。
![锐亚教育](http://www.insideria.cn/files/default/2017/03-19/164933d4e4bd117214.jpg)
锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛
- 还没有人评论,欢迎说说您的想法!