• Unity——对象池


    对象池是一种朴素的优化思想。在遇到需要大量创建和销毁同类物体的情景时,可以考虑使用对象池技术优化游戏性能。


    一、为什么要使用对象池

    在很多类型的游戏中都会创建和销毁大量同样类型的物体。例如,飞行射击游戏中有大量子弹,某些动作游戏中有大量敌人,还有游戏中反复出现和消失的粒子特效等。

     而创建和销毁物体本身属于比较消耗资源的操作,创建时不仅需要引擎的处理,而且还会分配大量内存,这些内存在物体销毁时还需要回收,这给虚拟机带来了垃圾回收的压力。

    也就是说,场景中的物体数量较多不一定是对性能影响最大的因素。影响最大的因素有可能是一段时间内有太多物体创建和销毁。例如,在飞行射击类游戏中,每秒有许多子弹创建,稍后又有同样多的子弹因击中敌人或距离过远而销毁。而无论多麽简单的物体,在其创建和销毁的过程中都会消耗一定的资源。对象池优化所针对的正是这一类问题。

    对象池技术的思想非常简答。例如,某种物体需要大量创建和销毁,那么久实现把它创建完成,放在玩家看不到的地方或隐藏起来。在需要创建的时候,直接从事先创建的物体中取出即可,而销毁的时候也不会真的销毁,只是放回了原处。这种统一管理大量物体的“池子”就叫对象池。

    二、简易对象池实例 

    先思考最简单的情况:一个对象池只管理同一种物体(如同一种子弹或同一种粒子)。对象池作为一个脚本组件,挂在场景中的一个空物体上以便编辑。而“池子”中的多个物体可以作为其子物体,方便管理。

    对象池中的大量物体,如果用一个容器管理,则用C#队列(Queue)最合适。队列的简单实用方法如下:

    1. Queue<int> queue = new Queue<int>();
    2. queue.Enqueue(1); //将数字1加入队列
    3. queue.Enqueue(2); //将数字2加入队列
    4. queue.Enqueue(3); //将数字3加入队列
    5. int n = queue.Dequeue(); //最早加入的数字1出队,n为1
    6. Debug.Log(queue.Count); //获得队列长度

    保存游戏物体使用Queue即可。用这种思路编写对象池完整代码如下,脚本名称为SimplePool

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. //只存放一种数据类型物体的简单对象池
    5. public class SimplePool : MonoBehaviour
    6. {
    7. //私有字段加SerializeField,可以在编辑器中编辑,但其他脚本不可访问
    8. [SerializeField]
    9. private GameObject _prefab;
    10. //队列,与List类似的容器,先进先出
    11. private Queue _pooledInstanceQueue = new Queue();
    12. //通过对象池创建物体
    13. public GameObject Creat()
    14. {
    15. if (_pooledInstanceQueue.Count > 0)
    16. {
    17. //如果队列中有,直接取出一个
    18. GameObject go = _pooledInstanceQueue.Dequeue();
    19. go.SetActive(true);
    20. return go;
    21. }
    22. //如果队列空了,就创建一个
    23. return Instantiatae(_prefab);
    24. }
    25. public void Destroy(GameObject go)
    26. {
    27. //将不再使用的物体放回队列
    28. _pooledInstanceQueue.Enqueue(go);
    29. go.SetActive(false);
    30. //为了方便管理,所有的物体都以对象池为父物体
    31. go.transform.SetParent(gameObject.transform);
    32. }
    33. }

     三、对象池测试方法

    这里我们来举例介绍

    1、编写测试脚本

    1. 创建一个空物体并命名为pool,挂上之前的对象池脚本SimplePool
    2. 创建一个球体作为子弹,并挂载脚本Bullet。该脚本的用途是让子弹原理原点移动,超出一定范围则销毁自身。根据是否设置了对象池pool,决定是否通过对象池销毁。其脚本内容如下:
      1. using System.Collections;
      2. using System.Collections.Generic;
      3. using UnityEngine;
      4. public class Bullet : MonoBehaviour
      5. {
      6. public SimplePool pool;
      7. void Update()
      8. {
      9. //每一帧,朝远离原点的方向移动
      10. Vector3 dir = transform.position - Vector3.zero;
      11. transform.position += dir.normalized * 5.0f * Time.deltaTime;
      12. //离原点一定距离以后就销毁
      13. if (Vector3.Distance(transform.position, Vector3.zero) >= 17)
      14. {
      15. //如果设置有对象池,则用对象池回收
      16. if (pool)
      17. {
      18. pool.Destroy(gameObject);
      19. }
      20. else
      21. {
      22. Destroy(gameObject);
      23. }
      24. }
      25. }
      26. }
    3. 将挂在了Bullet脚本的子弹做成预制
    4. 创建一个空物体并命名为Spawner,用于生成子弹。挂载Spawner脚本,内容如下:
      1. using System.Collections;
      2. using System.Collections.Generic;
      3. using UnityEngine;
      4. public class Spawner : MonoBehaviour
      5. {
      6. //是否使用预制
      7. public bool usePool = true;
      8. //预制
      9. public GameObject prefab;
      10. SimplePool pool;
      11. void Start()
      12. {
      13. pool = GameObject.Find("pool").GetComponent();
      14. }
      15. void Update()
      16. {
      17. if (Input.GetButtonDown("Jump"))
      18. {
      19. for(int i=0;i<1000;i++) //每次创建1000个子弹
      20. {
      21. GameObject go;
      22. if (usePool)
      23. {
      24. //通过对象池创建
      25. go = pool.Creat();
      26. //对Bullet脚本组件设置pool,用于销毁
      27. go.GetComponent().pool = pool;
      28. }
      29. else
      30. {
      31. //通过对象池创建
      32. go = Instantiate(prefab);
      33. //不通过对象池销毁,也需要设置
      34. go.GetComponent().pool = null;
      35. }
      36. go.transform.position = Random.onUnitSphere * 5;
      37. go.transform.parent = transform;
      38. }
      39. }
      40. }
      41. }

    5. 确认Spawner脚本和对象池脚本都设置了子弹预制体,创建物体是会用到

    2、测试方法

    以上脚本可以切换是否使用对象池,主开关在Spawner脚本。Spwaner脚本有一个Use Pool选项,代表是否启用对象池。如果启用,则在子弹的创建和回收时都需要通过对象池进行;如果不启用,则使用常规的创建和销毁方法。

    运行游戏,按下空格见,就可以创建大量物体了,如图:

    Game窗口的右上方有一个Stats按钮,单机就可以打开一个性能统计界面,从而可以看到游戏帧率等统计信息。从统计信息中可以大致看出游戏运行时的性能表现,但对于分析对象池效果来说这些信息过于简单,不足以体现优化前后的效果。

  • 相关阅读:
    GO语言-包的使用
    JAVA学习之前端知识掌握Day001
    TCP客户端发送结构体数据
    Kafka基本讲解
    欧莱雅SAP系统成功合并
    开发多个项目之后的一些感想....
    07_c/c++开源库protobuf序列化
    数额结构(6.1~6.8)
    对于开发而言,用户体验才是最终的王道(性能优化篇)
    基于模型的系统工程(MBSE)与设计(MBD)的关系思考
  • 原文地址:https://blog.csdn.net/m0_63024355/article/details/132872495