草庐IT

Unity实现简单的对象池

AlphaIcarus 2023-03-28 原文

一、简介

先说说为什么要使用对象池
在Unity游戏运行时,经常需要生成一些物体,例如子弹、敌人等。虽然Unity中有Instantiate()方法可以使用,但是在某些情况下并不高效。特别是对于那些需要大量生成又需要大量销毁的物体来说,多次重复调用Instantiate()方法和Destory()方法会造成大量的性能消耗。
这时使用对象池是一个更好的选择。
那么什么是对象池呢?
简单来说,就是在一开始创建一些物体(或对象),将它们隐藏(休眠)起来,对象池就是这些物体的集合,当需要使用的时候,就将需要的对象激活然后使用,而不是实例化生成。如果对象池中的对象消耗完了可以扩大对象池或者重新再次使用对象池中的对象。
一般情况下,一个对象池中存放的都是一类物体,我们一般希望创建多个对象池来存储不同类型的物体。
例如我们需要两个对象池来分别存储球体和立方体。
那么可以选择使用Dictionary来创建对象池,这样不仅可以创建对象池,还能指定每个对象池存储对象的类型。这样就能通过Tag来访问对象池。
至于对象池中可以使用Queue(队列)来存储具体的对象,队列不仅可以快速获取到第一个对象,能够按顺序获取对象。如果出队的对象在使用完成之后再次入队,那么这样就可以一直循环来重用对象。

二、Unity中的具体实现

新建一个Unity项目,在场景中添加一个空物体,命名为ObjectPool
同时制作一个黑色的地面便于显示和观察

新建脚本ObjectPooler添加到ObjectPool上

public class ObjectPooler : MonoBehaviour
{
    [System.Serializable]   
    public class Pool    //对象池类
    {
        public string tag;          //对象池的Tag(名称)
        public GameObject prefab;   //对象池所保存的物体类型
        public int size;            //对象池的大小
    }
    public List<Pool> pools;        
    
    Dictionary<string, Queue<GameObject>> poolDictionary;  //声明字典

    void Start()
    {
        //实例化字典                  对象池的Tag   对象池保存的物体
        poolDictionary = new Dictionary<string, Queue<GameObject>>();
    }
}

在Inspector中添加对应的数据,这里简单创建了立方体和球体并设为了预制体

然后继续修改ObjectPooler

public class ObjectPooler : MonoBehaviour
{
    [System.Serializable]   
    public class Pool
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }
    public List<Pool> pools;
    Dictionary<string, Queue<GameObject>> poolDictionary;

    public static ObjectPooler Instance;    //单例模式,便于访问对象池
    private void Awake()
    {
        Instance = this;
    }
    void Start()
    {
        poolDictionary = new Dictionary<string, Queue<GameObject>>();
        foreach (Pool pool in pools)
        {
            Queue<GameObject> objectPool = new Queue<GameObject>();     //为每个对象池创建队列
            for (int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab);
                obj.SetActive(false);   //隐藏对象池中的对象
                objectPool.Enqueue(obj);//将对象入队
            }
            poolDictionary.Add(pool.tag, objectPool);   //添加到字典后可以通过tag来快速访问对象池
        }
    }

    public GameObject SpawnFromPool(string tag, Vector3 positon, Quaternion rotation)     //从对象池中获取对象的方法
    {
        if (!poolDictionary.ContainsKey(tag))  //如果对象池字典中不包含所需的对象池
        {
            Debug.Log("Pool: " + tag + " does not exist");
            return null;
        }

        GameObject objectToSpawn = poolDictionary[tag].Dequeue();  //出队,从对象池中获取所需的对象
        objectToSpawn.transform.position = positon;  //设置获取到的对象的位置
        objectToSpawn.transform.rotation = rotation; //设置对象的旋转
        objectToSpawn.SetActive(true);                //将对象从隐藏设为激活

        poolDictionary[tag].Enqueue(objectToSpawn);     //再次入队,可以重复使用,如果需要的对象数量超过对象池内对象的数量,在考虑扩大对象池
        //这样重复使用就不必一直生成和消耗对象,节约了大量性能
        return objectToSpawn;  //返回对象
    }
}

新建脚本CubeSpanwer,来使用对象池生成物体

public class CubeSpanwer : MonoBehaviour
{
    ObjectPooler objectPooler;
    private void Start()
    {
        objectPooler = ObjectPooler.Instance;
    }
    private void FixedUpdate()
    {
        //这样会高效一点,比ObjectPooler.Instance
        objectPooler.SpawnFromPool("Cube", transform.position, Quaternion.identity);
    }
}

新建脚本Cube,添加到Cube预制体上,让其在生成时添加一个力便于观察
注意:为了方便观察这里移除了Cube上的BoxCollider

public class Cube : MonoBehaviour
{
    void Start()
    {
        GetComponent<Rigidbody>().AddForce(new Vector3(Random.Range(0f, 0.2f), 1f, Random.Range(0f, 0.2f)));
    }
}

我们发现Cube并没有向上飞起而是堆叠在一起

这时因为Cube只在生成时在Start中添加了力,只调用了一次,但马上就被隐藏放入对象池了,等到再次取出时,并没有任何方法的调用,只是单纯设置位置

我们需要让cube对象知道自己被重用了,再次调用添加力的方法
新建接口 IPooledObject

public interface IPooledObject
{
    void OnObjectSpawn();
}

然后让Cube继承该接口

public class Cube : MonoBehaviour, IPooledObject
{
    private Rigidbody rig;
    public void OnObjectSpawn()
    {
        rig = gameObject.GetComponent<Rigidbody>();
        rig.velocity = Vector3.zero;	//将速度重置为0,物体在被隐藏时仍然具有速度,不然重用时仍然具有向下的速度
        rig.AddForce(new Vector3(Random.Range(0, 0.2f), 10, Random.Range(0, 0.2f)), ForceMode.Impulse);
    }
}

然后修改ObjectPooler,让Cube在被重用时调用重用的方法

public GameObject SpawnFromPool(string tag, Vector3 positon, Quaternion rotation)     //从对象池中获取对象的方法
    {
        ......
        IPooledObject pooledObj = objectToSpawn.GetComponent<IPooledObject>();
        if (pooledObj != null)  //判断,并不是所有对象都继承了该接口,例如Cube我想让它向上飞,Sphere则让它直接生成,Sphere就不必继承IPoolObject接口
        {
            pooledObj.OnObjectSpawn();  //调用重用时的方法
        }
        poolDictionary[tag].Enqueue(objectToSpawn);
        return objectToSpawn;
    }

运行结果:

Cube从CubeSpawner不断生成,可以自行设置计时器来限制生成的速度

有关Unity实现简单的对象池的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  4. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  5. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  6. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

  7. ruby - 如何在 Rails 4 中使用表单对象之前的验证回调? - 2

    我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser

  8. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  9. ruby - 简单获取法拉第超时 - 2

    有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url

  10. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

随机推荐