Unity——对象池

2023-09-21 00:04:27

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


一、为什么要使用对象池

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

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

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

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

二、简易对象池实例 

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

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

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

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//只存放一种数据类型物体的简单对象池
public class SimplePool : MonoBehaviour
{
    //私有字段加SerializeField,可以在编辑器中编辑,但其他脚本不可访问
    [SerializeField]
    private GameObject _prefab;

    //队列,与List类似的容器,先进先出
    private Queue<GameObject> _pooledInstanceQueue = new Queue<GameObject>();

    //通过对象池创建物体
    public GameObject Creat()
    {
        if (_pooledInstanceQueue.Count > 0)
        {
            //如果队列中有,直接取出一个
            GameObject go = _pooledInstanceQueue.Dequeue();
            go.SetActive(true);
            return go;
        }
        //如果队列空了,就创建一个
        return Instantiatae(_prefab);
    }
    public void Destroy(GameObject go)
    {
        //将不再使用的物体放回队列
        _pooledInstanceQueue.Enqueue(go);
        go.SetActive(false);
        //为了方便管理,所有的物体都以对象池为父物体
        go.transform.SetParent(gameObject.transform);
    }
   
}

 三、对象池测试方法

这里我们来举例介绍

1、编写测试脚本

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

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

2、测试方法

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

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

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

更多推荐

容器管理工具 Docker生态架构及部署

目录一、Docker生态架构1.1DockerContainersAreEverywhere1.2生态架构1.2.1DockerHost1.2.2Dockerdaemon1.2.3Registry1.2.4Dockerclient1.2.5Image1.2.6Container1.2.7DockerDashboard1

VL系列 Exchanging-based Multimodal Fusion with Transformer 论文阅读笔记

多模态融合Exchanging-basedMultimodalFusionwithTransformer论文阅读笔记一、Abstract二、引言三、相关工作3.1深度多模态融合四、方法4.1低维投影和embedding归一化低维投影Embedding归一化4.2多模态交换Transformer基础CrossTransf

<git>如何快速上手并高效协同

git是什么?Git是一种分布式版本控制系统,用于跟踪计算机文件的变化和协调多个人之间的工作。它最初由LinusTorvalds于2005年创建,旨在管理Linux内核的开发。Git可以在本地计算机上存储完整的版本历史记录,并允许用户在不同的分支上进行开发和合并。它还提供了许多工具和命令,用于管理代码库、协作开发、解决

ClickHouse与Elasticsearch比较总结

目录背景分布式架构存储架构写入链路设计Elasticsearch再谈Schemaless查询架构计算引擎数据扫描再谈高并发性能测试日志分析场景access_log(数据量197921836)trace_log(数据量569816761)官方Ontime测试集用户画像场景(数据量262933269)二级索引点查场景(数据

分享从零开始学习网络设备配置--任务3.4 利用单臂路由实现部门间网络互访

任务描述某公司的管理员对部门划分了VLAN后,发现两个部门之间无法通信,但有时两个部门的员工需要进行通信,管理员现要通过简单的方法来实现此功能。划分VLAN之后,VLAN之间是不能通信的,使用路由器的单臂路由功能可以解决这个问题。任务要求(1)利用单臂路由实现部门间网络互访,网络拓扑图如图。(2)在交换机SWA上划分V

Canal实现Mysql数据同步至Redis、Elasticsearch

文章目录1.Canal简介1.1MySQL主备复制原理1.2canal工作原理2.开启MySQLBinlog3.安装Canal3.1下载Canal3.2修改配置文件3.3启动和关闭4.SpringCloud集成Canal4.1Canal数据结构![在这里插入图片描述](https://img-blog.csdnimg.

CGI与FastCGI的区别在哪里,FastCGI的应用场景讲解

🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年6月CSDN上海赛道top4。🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。🎉欢迎👍点赞✍评论⭐收藏文章目录1.CGI和FastCGI1.1CGI1.2FastCGI1.3对比2.Fas

MySQL的高级SQL语句

目录一、高级SQL语句1、select查询表中一个或多个字段的数据2、distinct不显示重复的数据记录3、where有条件查询4、and与or且与或5、in显示在某个范围值内的字段的信息6、between显示两个值范围内的数据记录7、orderby对字段进行排序8、groupby对字段进行分组汇总9、having用

【操作系统】聊聊磁盘IO是如何工作的

磁盘机械磁盘主要是由盘片和读写磁头组成。数据存储在盘片的的环状磁道上,读写数据前需要移动磁头,先找到对应的磁道,然后才可以访问数据。如果数据都在同一磁道上,不需要在进行切换磁道,这就是连续IO,可以获得更好的性能。而随机IO性能就比较差。固态磁盘固态磁盘不需要寻找磁道,所以随机IO和连续IO性能都不错。连续IO的性能其

【Linux】自制shell

本期我们利用之前学过的知识,写一个shell命令行程序目录一、初始代码二、使用户输入的ls指令带有颜色分类三、解决cd指令后用户所在路径不变化问题3.1chdir函数四、关于环境变量的问题一、初始代码#include<stdio.h>#include<unistd.h>#include<stdlib.h>#includ

模块化开发_php中使用redis

redis介绍和安装redis数据库,支持数据持久化,常用与分布式锁,支持事务,持久化,非关心型数据库区别:关系型数据库:硬盘,安全,结构简单,易于理解,浪费空间非关系型数据库:内存,断电丢失数据,读写速度快,内存的速度是硬盘的100倍redis:用于缓存压力,提升网站访问速度三种类型:持久化(将数据保存到硬盘中,再开

热文推荐