Unity的C#编程教程_64_对象池 Object Pooling 详解及应用练习

目录:

Object Pooling Design Pattern

  • object polling 是设计范式的一种
    • 是一种优化的范式,尤其对于移动化来说非常重要
    • 不是生成和销毁游戏对象,而是循环利用游戏对象

假设我们创建一个射击游戏,我们射出的子弹都是游戏对象,传统的做法是不断从枪口生成,然后击中目标后不断销毁,这个利用的是垃圾回收机制,只有当前帧的垃圾回收完成后,才能开始处理下一帧,所以如果有很多的游戏对象要生成和销毁,那么游戏可能就会卡住。

替代的优化解决方案是 object polling,我们可以循环利用这些子弹,只生成一次,然后在整个程序运行过程中不断循环使用。

创建一个游戏对象 Player,并挂载同名脚本:

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

public class Player : MonoBehaviour
{
    [SerializeField]
    private GameObject _bulletPrefab;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(_bulletPrefab);
        }
    }
}

设计一个 Bullet 的 Prefab,挂载同名脚本,然后拖拽赋值:

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

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Destroy(this.gameObject, 2f); // 2 秒钟后自动销毁
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

这样我们进入游戏后,按下空格键就会创建 Bullet,过了 2 秒钟自动销毁。这个是大多数初学者的做法。

这样做的话,就是在利用垃圾回收机制。

下节我们将探讨如何转化为 object pooling

Challenge: Pool Manager

  • 任务说明:
    • 创建一个 Pool Manager class
    • 存储所有需要用的 Bullet
    • 这样我们就可以有个预设的 Bullet 列表供使用,比如 10 个,可以用来循环使用

创建游戏对象 Pool Manager,挂载同名脚本:

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

public class PoolManager : MonoBehaviour
{
    [SerializeField] // 在 inspector 中可见,用于拖拽赋值
    private GameObject _bulletPrefab; // 子弹的 prefab 需要赋值

    // 以下为 PoolManager 的 singleton 定义部分,用于简易调用
    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("no instance");
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;     
    }

    // 以下用于定义 Bullet 的对象池
    public List<GameObject> bulletList;
    int i;
    bool fullBullet;

    public void CreatBullet()
    {
        if (fullBullet == false) // 如果池子没满
        {
            GameObject obj = Instantiate(_bulletPrefab); // 生成游戏对象实例
            bulletList.Add(obj); // 添加到列表中(池子中)

            if (bulletList.Count > 9) // 池子满了
            {
                fullBullet = true; // 修改 flag
            }
        }
        else if(i < 10) // 池子满了以后就可以直接激活,而不用生成了
        {
            bulletList[i].SetActive(true); // 挨个激活
            i++;
        }
        else
        {
            i = 0; // 激活一轮以后再从头开始
        }
        
    }


    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

这里将其设计为 singleton 便于访问。

玩家 Player 脚本,控制开枪:

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

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) // 按下空格键表示开枪
        {
            PoolManager.Instance.CreatBullet(); // 调用 PoolManager 生成子弹
        }
    }
}

子弹 Bullet 的脚本,控制子弹的灭活方式:

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

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable() // 游戏对象被激活的时候调用
    {
        StartCoroutine(bulletDis()); // 启动协程
    }


    IEnumerator bulletDis() // 定义协程
    {
        yield return new WaitForSeconds(2); // 等待2秒

        this.gameObject.SetActive(false); // Bullet灭活
    }
}

同样的任务我们可以有不同的实现思路,以下是另一种稍有区别的思路,在游戏启动的时候就一下子形成整个 Bullet 列表,之前是每次按下空格生成一个直至添加满列表:

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

public class PoolManager : MonoBehaviour
{
    [SerializeField] // 在 inspector 中可见,用于拖拽赋值
    private GameObject _bulletPrefab; // 子弹的 prefab 需要赋值

    // 以下为 PoolManager 的 singleton 定义部分,用于简易调用
    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("no instance");
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;     
    }

    // 以下用于定义 Bullet 的对象池
    [SerializeField]
    private List<GameObject> _bulletList;
    [SerializeField]
    private GameObject _bulletContainer; // 生成的子弹游戏对象汇总到一个母文件下面

    private int i;

    public void CreatBullet()
    {
        if(i < 10) // 池子满了以后就可以直接激活,而不用生成了
        {
            if (_bulletList[i].activeSelf == false) // 这个修正可以确保按下发射键过快时不出现问题
            {
                _bulletList[i].SetActive(true); // 挨个激活
                i++;
            }
        }
        else
        {
            i = 0; // 激活一轮以后再从头开始
        }
        
    }

    private void CreatBulletList(int bulletCount)
    {
        for(int j = 0; j < bulletCount; j++)
        {
            GameObject bullet = Instantiate(_bulletPrefab); // 生成游戏对象实例
            bullet.transform.parent = _bulletContainer.transform; // 设置母文件夹
            bullet.SetActive(false); // 生成后即刻灭活,不显示出来

            _bulletList.Add(bullet); // 添加到列表中(池子中)
        }
    }


    // Start is called before the first frame update
    void Start()
    {
        CreatBulletList(10);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

创建游戏对象 BulletContainer,拖拽赋值到 PoolManager 脚本下面的 Bullet Container 框中。

运行后可实现预期效果。

Challenge: Request from Pool Manager

  • 任务说明:
    • 创建子弹的对象池
    • Player 每次按下空格键,从对象池获取一个未激活的子弹,并进行激活
    • 如果对象池中没有未激活的子弹,则生成一个子弹, 并额外添加到对象池中

PoolManager 脚本,挂载到 PoolManager 游戏对象上:

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

public class PoolManager : MonoBehaviour
{
    [SerializeField] // 在 inspector 中可见,用于拖拽赋值
    private GameObject _bulletPrefab; // 子弹的 prefab 需要赋值

    // 以下为 PoolManager 的 singleton 定义部分,用于简易调用
    private static PoolManager _instance;
    public static PoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("no instance");
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this;     
    }

    // 以下用于定义 Bullet 的对象池
    [SerializeField]
    private List<GameObject> _bulletList;
    [SerializeField]
    private GameObject _bulletContainer; // 生成的子弹游戏对象汇总到一个母文件下面

    public GameObject CreatBullet()
    {
        foreach(var bullet in _bulletList) // 遍历子弹池子
        {
            if (bullet.activeSelf == false) // 如果还有没被激活的子弹
            {
                bullet.SetActive(true); // 激活
                return bullet; // 跳出该方法
            }
        }

        // 如果所有子弹都被激活了还不够用,则生成新的子弹,并添加到子弹池子
        GameObject newBullet = Instantiate(_bulletPrefab); // 生成游戏对象实例,并激活
        newBullet.transform.parent = _bulletContainer.transform; // 设置母文件夹

        _bulletList.Add(newBullet); // 添加到列表中(池子中)
        return newBullet; // 跳出该方法
    }

    private void CreatBulletList(int bulletCount)
    {
        for(int j = 0; j < bulletCount; j++)
        {
            GameObject bullet = Instantiate(_bulletPrefab); // 生成游戏对象实例
            bullet.transform.parent = _bulletContainer.transform; // 设置母文件夹
            bullet.SetActive(false); // 生成后即刻灭活,不显示出来

            _bulletList.Add(bullet); // 添加到列表中(池子中)
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        CreatBulletList(10);
     }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Player 脚本,挂载到 Player 游戏对象上:

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

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) // 按下空格键表示开枪
        {
            _ = PoolManager.Instance.CreatBullet();
            // 调用 PoolManager 生成子弹
            // 游戏中如果需要对这个子弹产生位移等操作,代码也可以放在这里
        }
    }
}

Bullet 脚本,挂载到 Bullet 的 prefab 上:

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

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable() // 游戏对象被激活的时候调用
    {
        StartCoroutine(bulletDis()); // 启动协程
    }


    IEnumerator bulletDis() // 定义协程
    {
        yield return new WaitForSeconds(2); // 等待2秒

        this.gameObject.SetActive(false); // Bullet灭活
    }
}

创建游戏对象 BulletContainer,拖拽赋值到 PoolManager 脚本下面的 Bullet Container 框中。

Recycle the Pool

  • 循环使用游戏对象池的另一种方法

前面我们提到了使用协程可以达到循环使用对象池,其实我们还有别的选择:

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

public class Bullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable() // 游戏对象被激活的时候调用
    {
        Invoke("Inactive",2); // 2秒以后运行 Inactive 方法
    }


    private void Inactive() // 反激活
    {
        this.gameObject.SetActive(false);
    }
}

Tags: