Unity之C#学习笔记(17):对象池模式 Object Pooling

前篇链接:Unity之C#学习笔记(16):单例模式及单例模板类 Singleton and MonoSingleton

在游戏中,有一些生命周期很短,需要频繁创建和销毁的物体,例如射击游戏中的子弹。按一般做法,我们也需要频繁地实例化(Instantiate)和销毁(Destroy)这些物体,这其实是有一定开销的。对象池模式的思想就是创建容纳了一些物体的“池”,需要时从中取一个,用完了再放回去,循环利用,减少生成和销毁物体的次数,优化性能。

来看一个例子:我们现在有子弹的prefab,按下空格键时,生成一个子弹,子弹过一秒自动消失。传统的做法是:

GameManager:

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

public class GameManager : MonoBehaviour
{
    public GameObject bulletPrefab;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(bulletPrefab);
        }
    }
}

Bullet:

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

public class Bullet : MonoBehaviour
{
    void Start()
    {
        Destroy(this.gameObject, 1.0f); // 创建后延迟1秒销毁
    }
}

实验一下会发现,即使按最快速度按空格,场景中大概也只能生成7个左右的子弹。也就是说,我们只需要一个容纳7个子弹实体的池就足够了。在实际场景中,池的容量自然是要根据游戏逻辑确定。创建PoolManager脚本,用List实现对象池。在Start函数中初始化7个GameObject并放在List中:

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

public class PoolManager : MonoSingleton<PoolManager>
{
    public GameObject bulletPrefab;

    private List<GameObject> _bulletPool;

    void Start()
    {
        _bulletPool = GenerateBullets(7);
    }

    List<GameObject> GenerateBullets(int amount)
    {
        List<GameObject> pool = new List<GameObject>();
        for (int i = 0; i < amount; i++)
        {
            GameObject bullet = Instantiate(bulletPrefab);
            bullet.SetActive(false);
            pool.Add(bullet);
        }
        return pool;
    }

PoolManager使用单例模式,关于单例模式和单例模板类,可以参考上一篇文章

PoolManager对外提供获取对象的接口。获取对象时,从List中找到一个空闲状态的GameObject并返回。如果找不到空闲的,说明池子中所有对象都被占用了,需要扩充容量,创建新的对象放入池子。在实现中,我们不需要真的模拟将对象从池子中“取走”,“放回”的过程,而可以用一个状态量判断某个对象是否空闲。例如这里最简单的,我们用Active属性决定。

    public GameObject RequestBullet()
    {
        foreach (var bullet in _bulletPool)
        {
            if (bullet.activeInHierarchy == false) // 找到一个未使用的对象
            {
                bullet.SetActive(true); // Active设为true,表示被占用
                return bullet;
            }
        }
        // 池子内的对象用光了!创建一个新的
        // 如果不够用的情况经常发生,考虑一次扩充更多的容量,例如一次性将池子扩充为1.5倍
        GameObject newBullet = Instantiate(bulletPrefab);
        _bulletPool.Add(newBullet);
        return newBullet;
    }

在外部获取一个对象:

        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject bullet = PoolManager.Instance.RequestBullet();
            // 对bullet对象做一些其它操作,例如移动,旋转...
        }

回收对象时,由于本例中我们是使用物体的Active属性判断是否空闲,故只需SetActive(false)即可。

Bullet:

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

public class Bullet : MonoBehaviour
{
    private void OnEnable() // 对象设为Active时被调用
    {
        Invoke("Die", 1); // 延迟1秒调用Die
    }

    void Die()
    {
        this.gameObject.SetActive(false);
    }
}

实际应用中,管理的对象会更复杂,分配和回收可能涉及其他相关资源的处理。占用和回收的条件也不能只用简单的SetActive实现,需要设计更合适的数据结构。

上一篇:离散仿真引擎基础作业与练习


下一篇:Unity之光标默认聚焦在InputField