160439q3w3qjqj3n9jwnx6.png

本文由游戏玄学翻译社制作发布!翻译:太昊,兔角鹿,小木曾;Review: 风雨时;统筹润色:杨雍

  想象一下:你正在!!哦,不,测试你最新的和最棒的一个射击游戏。敌人在以你能掌握的最快速度来回飞行,然后,砰!卡了一帧之后,你就被凶神恶煞的外星人手打成了翔。

  这可是场横扫千军的战斗,不应该由于莫名其妙的内存尖峰左右战斗的结果。你是不是也曾经因为这个问题输掉?来来来,搬个小马扎,听我来扒一扒 对象池技术 吧。

  在这篇Unity教程中,你将学到:

 

 

  • 所有关于对象池技术的内容
  • 如何将一个game object入池
  • 如何在运行时按需扩展对象池
  • 如何扩展对象池以适应不同的对象


  在教程的最后,你会得到一个可以得到新游戏的全部代码。而且,你会懂得如何为现有的游戏改进这个代码。

  预备知识:你需要熟悉C#基础并且知道如何操作Unity的开发环境。

  什么是对象池技术?

  Instantiate() 和 Destroy() 是在游戏流程中好用而必备的方法。(通常情况下,单独调用这两个方法只占用CPU相当微小的时间。)

  然而,对于在游戏流程中生命周期短暂而且每秒大量摧毁的对象群而言,CPU进行内存分配的时间占用十分显著。

154306go00osrrvenr0g0r.png


  就是一个适合入池的GameObject的好例子。

  并且,Unity使用垃圾收集(Garbage Collection)技术来释放不需要继续使用的内存。不断调用Destroy()会频繁的激活收集,而这会拖慢CPU导致游戏流程的卡顿。

  这一行为在移动设备和网页等资源受限的环境下中尤为致命。

  对象池技术将尚未用到的对象放在游戏流程之前——比如在读取界面的时候——进行预先实例化。这样游戏可以从『池子』中重用对象,而不需在游戏流程中不断地创建和销毁。

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

  点击play按钮看看。 :]

  注意当发射时,在Hierarchy(栏)里实例化了大量的PlayerBullet(Clone)。当击中敌人或者离开屏幕时,他们会被销毁。

  更糟的是收集那些掉落的强化道具,他们迅速用的复制体填满了Hierarchy(栏),随后又在下一秒里立刻销毁它们。

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方法替换成如下代码:

 

 

  1. if (other.gameObject.tag == Boundary) {
  2. if (gameObject.tag == Player Bullet) {
  3. gameObject.SetActive(false);
  4. } else {
  5. Destroy(gameObject);
  6. }
  7. }


  这里有个坑:使用对象池模式时,注意池中对象的生命周期会和之前有所不同。

  好了,你可以点击运行了。^_^

  当你射击时,在Hierarchy中的玩家自动从未激活状态变为激活状态。它们会在移出屏幕、或击中敌机的时候,以优雅代码应有的方式变成回未激活的状态。

  干的漂亮!

154312pmeoog5zroeg4kfm.png

超级扩展对象池

  现在你将学习如何修改对象池,以在运行时按需增加池中的对象数量。

  在MonoDevelop中打开ObjectPooler脚本,并将以下代码加入到公有变量中:

 

 

public bool shouldExpand = true;

  这个代在Inspector中创建一个复选框,用来决定是否可以增加池中对象的数量。

  在GetPooledObject()中,将return null;修改为如下代码:

 

 

  1. if (shouldExpand) {
    • GameObject obj = (GameObject)Instantiate(objectToPool);
      • obj.SetActive(false);
        • pooledObjects.Add(obj);
          • return obj;
            • } else {
              • return null;
                • }


  对象池群

  Invariably, lots of bullets mean lots of enemies, lots of explosions, lots of enemy bullets and so on.

  通常情况下,大量的就意味着大量的敌人,不停的爆炸效果,敌人们大量发射的,以及其他方方面面开销。

  要为这场疯狂屠戮做准备,你需要扩展你的对象池以便于处理复杂的对象类型。接下来的一步,将实现在Inspector中的相同位置调节 不同类型自带的参数。

  在ObjectPooler类的前面加入以下代码:

 

 

  1. [System.Serializable]
    • 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

  如处理时一样,你需要改变这两类敌舰instantiate和destroy的方法。

  敌舰在执行GameController脚本时进行实例化,并在EnemyDroneController这一脚本中销毁。

  之前已经做过一次了,所以这里我们会快一点。-w-

  打开GameController脚本。在SpawnEnemyWaves()中找到enemyType1 instantiation code:

 

 

  1. Instantiate(enemyType1, spawnPosition, spawnRotation);


  何去何从

  在这个教程里,你了改写一个现有的游戏,通过给其加入对象池的方式,从可以预期的过载中拯救玩家的CPU,并解决了因此而会产生的跳帧和电池过热问题。

  你在脚本的切换和内容整合上熟练度+5。

锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛