Unity 系列 -- 可重用的对象池

原文地址:unity中对象池的使用

前言

当我们使用一个新的游戏物品(实例)时,首先需要实例化它(Instantiate,实际上也是基于new的机制实现的),在我们使用GameObject完成后便摧毁它(Destory

但是,申请实例化时,意味着我们需要在托管堆上分配一块新的内存给Object,在回收时,我们需要等待回收机制收回该块内存,这意味着当我们使用一些短期GameObject(比如在射击游戏中的子弹,往往单个存在时间只有1s左右,但是在同一时刻场景中往往存在着大量的发射和销毁事件 )。与此同时,由于unity引擎自身引擎设计的问题,大多数UnityEngine API只能在主线程上运行,特别是在GameObject.Instantiate上,频繁调用Instantiate和Destory去实例化和销毁,在主线程上会造成卡顿

因此,对象池思想出现了。本篇文章只讨论针对unity GameObject的对象池思想。

对象池思想的核心:抛弃不必要的实例化和摧毁。当我们暂时不再需要某个单个物品时,不再使用Destory,而只是隐藏 它,即使用GameObject.SetActive(false),并将其放入一个重用字典 + 队列 中,我们称这个存储所有激活或非激活的物品的称为池子 。之后需要时,我们会先从重用数组中试着找是否当前有可用的实例并显示,如果没有再去实例化它。

简单来说:

  • 字典标记是什么东西
  • 队列放物品

对象池的实现

1. 写一个类 ObjectPool

我们现在来实现一个对象池脚本。我们用一个字典 <string(游戏物品名称)-游戏物品队列的键值> 来存储所有当前被隐藏的物品。

csharp 复制代码
public class ObjectPool
{
    private static ObjectPool instance; // 单例
    private Dictionary<string, Queue<GameObject>> objectPool = 
        new Dictionary<string, Queue<GameObject>>();
    private GameObject pool;
    public static ObjectPool Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new ObjectPool();
            }
            return instance;
        }
    }
}

2. 写一个 GetObject 方法

接下来我们实现创建物品逻辑

思路如下:先根据名称在对象池字典中寻找相应队列中是否有该物品的隐藏实例,如果有则从队列中取出该物品并将其setActive(true),如果没有则实例化它,设置对应的父物品方便在窗口中查看,最后返回该物品。

csharp 复制代码
public GameObject GetObject(GameObject prefab)
{
    GameObject gb;
    if (pool == null) // 当场景没有对象池时(第一次进入游戏或者切换了场景)
    {
        // 新建一个对象池游戏物品并清空字典
        pool = new GameObject("ObjectPool");
        objectPool = new Dictionary<string, Queue<GameObject>>();
    }
    // 如果池子里没有该物品
    if (!objectPool.ContainsKey(prefab.name) || objectPool[prefab.name].Count == 0) 
    {
        // 实例化它,加入队列
        gb = GameObject.Instantiate(prefab);
        PushObject(gb);
        GameObject childPool = GameObject.Find(prefab.name + "Pool");
        if (!childPool)
        {
            childPool = new GameObject(prefab.name + "Pool");
            childPool.transform.SetParent(pool.transform);
        }
        // 设置到相对于的子物品下,方便管理
        gb.transform.SetParent(childPool.transform);
    }
    // 从队列中提取对象,返回
    gb = objectPool[prefab.name].Dequeue();
    gb.SetActive(true);
    return gb;
}

3. 写一个 PushObject 方法

在回收物品时,我们只需要将其SetActive(false),并放入该物品的存储队列即可

csharp 复制代码
public void PushObject(GameObject prefab)
{
    //通过Instantiate实例化的物品都带有(Clone)后缀,我们将其去除再存储
    string _name = prefab.name.Replace("(Clone)", string.Empty);
    if (!objectPool.ContainsKey(_name))
        objectPool.Add(_name, new Queue<GameObject>());
    objectPool[_name].Enqueue(prefab);
    prefab.SetActive(false);
}

这样,一个简单的对象池就实现出来了,在发射子弹时:

  • 使用 ObjectPool.Instance.GetObject 代替 GameObject.Instantiate
  • 使用 ObjectPool.Instance.push 代替 GameObject.Destroy

对象池的使用

现在我们来实践一下这个对象池的用法,这里写了一个简单的功能,鼠标左键在鼠标位置生成物品1,右键生成物品2,然后每个生成的物品都会在1s后被回收:

csharp 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class ObjectPoolTest : MonoBehaviour
{
    public GameObject mouseLeft; // 一个正方形(左键生成)
    public GameObject mouseRight; // 一个圆形(右键生成)
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            var obj = ObjectPool.Instance.GetObject(mouseLeft); // 生成一个正方形
            obj.transform.position =
                Camera.main.ScreenToWorldPoint(Input.mousePosition) + new Vector3(0,0,10);
            StartCoroutine(DelayAction(() => ObjectPool.Instance.PushObject(obj), 1f)); // 1s 后正方形消失
        }
        if (Input.GetKeyDown(KeyCode.Mouse1))
        {
            var obj = ObjectPool.Instance.GetObject(mouseRight); // 生成一个圆形
            obj.transform.position =
                Camera.main.ScreenToWorldPoint(Input.mousePosition) + new Vector3(0, 0, 10);
            StartCoroutine(DelayAction(() => ObjectPool.Instance.PushObject(obj), 1f)); // 1s 后圆形消失
        }
    }
    // 定时器
    IEnumerator DelayAction(Action callback,float timer)
    {
        yield return new WaitForSeconds(timer);
        callback?.Invoke();
    }
}

这里物品1用了蓝色正方形,物品2用了红色圆形,给脚本赋值对应的物品)

实际表现效果:

Hierarchy 窗口显示:

相关推荐
码农君莫笑3 小时前
使用blazor开发信息管理系统的应用场景
数据库·信息可视化·c#·.net·visual studio
可喜~可乐6 小时前
C# WPF开发
microsoft·c#·wpf
666和77710 小时前
C#的单元测试
开发语言·单元测试·c#
小码编匠11 小时前
WPF 星空效果:创建逼真的宇宙背景
后端·c#·.net
向宇it13 小时前
【从零开始入门unity游戏开发之——unity篇02】unity6基础入门——软件下载安装、Unity Hub配置、安装unity编辑器、许可证管理
开发语言·unity·c#·编辑器·游戏引擎
yngsqq14 小时前
一键打断线(根据相交点打断)——CAD c# 二次开发
windows·microsoft·c#
TENET信条15 小时前
day53 第十一章:图论part04
开发语言·c#·图论
anlog16 小时前
C#在自定义事件里传递数据
开发语言·c#·自定义事件
向宇it18 小时前
【从零开始入门unity游戏开发之——unity篇01】unity6基础入门开篇——游戏引擎是什么、主流的游戏引擎、为什么选择Unity
开发语言·unity·c#·游戏引擎
仰望大佬00718 小时前
Avalonia实例实战五:Carousel自动轮播图
数据库·microsoft·c#