Compreensão e uso simples do modo de pool de objetos (implementado na unidade)

Um, o conceito de pool de objetos

O modelo de pool de objetos não é um modelo de design exclusivo para o desenvolvimento de jogos.Suas idéias de design são as mesmas dos pools de conexão de banco de dados e pools de threads em outros desenvolvimentos.

A ideia central não é excluí-lo logo após o uso, mas colocá-lo de volta na piscina e retirá-lo quando necessário. O surgimento do modo de pool de objetos otimiza principalmente dois pontos:

1. Impedir que objetos sejam criados e excluídos com frequência, resultando em instabilidade de memória e GC frequente (coleta de lixo)

2. O custo de inicialização do objeto é alto

 

Mas, como os objetos do desenvolvimento de software tradicional são geralmente leves a médios, a sobrecarga de alocar / liberar objetos é insignificante, portanto, há relativamente poucos aplicativos de pool de objetos ingênuos no desenvolvimento de software tradicional. Geralmente, objetos específicos são otimizados para ②, como pool de conexão de banco de dados e pool de thread, que resolve a reutilização e o número de conexões ao mesmo tempo. Mas, no processo de desenvolvimento do jogo, como muitos objetos do jogo são criados e excluídos com frequência, e os objetos do jogo contêm muitos objetos, até mesmo a tecnologia de pool de objetos simples tem mais cenários de aplicação.

Dois, operações de pool de objetos

A seguir está uma introdução textual às operações básicas do pool de objetos

Empréstimo: Em termos leigos, é obter um objeto do pool.Se for a primeira vez que obter um objeto, o pool deve ser inicializado. Se não houver mais nenhum objeto no pool, crie um

Retorno: Em termos leigos, o item foi originalmente usado para ser excluído, mas depois que o pool de objetos é aplicado, o objeto é devolvido ao pool, desde que o número no pool não seja maior do que o número máximo predefinido (para evitar muita memória para explodir), se o número no pool for maior que o número máximo predefinido, exclua-o diretamente

Aquecimento: Significa pré-carregar um certo número de objetos.Pessoalmente, acho que esta é uma das partes mais essenciais do pool de objetos. Se você não fizer o pré-aquecimento, na primeira vez que criar um objeto, ele envolverá diretamente a inicialização. É fácil entender que os jogadores preferem esperar 1 segundo a mais na interface de carregamento, ao invés de protelar por 0,1 segundo no jogo. Especialmente em jogos competitivos, os jogadores vão querer quebrar o computador (risos). Então eu acho que se você não fizer a otimização do pool de objetos de aquecimento, apenas metade estará feita.

Redução: é quase como aquecer ao contrário. Ao retornar, ele disse que se ultrapassar o limite definido, não será devolvido ao pool, mas será excluído diretamente, mas, na verdade, a exclusão também pode gerar custos de tempo, para que você possa excluí-lo primeiro e, em seguida, excluir e reduzir o pool de memória sempre que passar pela interface de carregamento no meio do jogo. Se você tem medo de que a memória estoure antes de carregar a interface, você pode definir um limite adicional que deve ser excluído, e seu efeito é o mesmo que o escrito acima ao retornar. (Eu não fiz esta função no meu DEMO)

Redefinir: cada objeto recém-criado deve ser igual ao recém-criado quando é "novo" e não pode obviamente ter o estado do último usado, portanto, toda vez que o objeto sai do pool, deve ser tratado com efeitos colaterais Coloque a redefinição. Na unidade, o conteúdo da inicialização manual do objeto é escrito no OnEnable () do objeto, incluindo a força para limpar o corpo rígido, etc. A diferença entre OnEnable () e Start () é que Start () é somente a primeira quando o objeto for habilitado pela primeira vez. Frame run, OnEnable será executado toda vez que o objeto for habilitado novamente.

Três, experimentos específicos

O que se segue é um pequeno experimento DEMO que eu mesmo fiz. Por ser apenas um pequeno experimento de demonstração, não é muito robusto. Também o expressei nos comentários, então, se for sobre integridade e solidez, não reclame muito . Mas se houver um problema de princípio em meu código que leva a uma grande otimização, indique e aprenda.

Abaixo está o script de ObjectPool. As funções são projetadas para torná-los mais parecidos com os nativos Instantiate and Destroy do Unity.

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

public class ObjectPool : MonoBehaviour
{
    //自身单例
    public static ObjectPool me;

    //池的存储
    //TODO 此处value使用自定义封装类型而不是单纯的queue更健全
    private Dictionary<string, Queue<GameObject>> pool;

    //每个池中最大数量
    //TODO 应该每个池设置每个池单独的数量
    private int maxCount = int.MaxValue;
    public int MaxCount
    {
        get { return maxCount; }
        set
        {
            maxCount = Mathf.Clamp(value, 0, int.MaxValue);
        }
    }

    //初始化
    void Awake()
    {
        me = this;
        pool = new Dictionary<string, Queue<GameObject>>();

    }

    /// <summary>
    /// 从池中获取物体
    /// </summary>
    /// <param name="go">需要取得的物体</param>
    /// <param name="position"></param>
    /// <param name="rotation"></param>
    /// <returns></returns>
    public GameObject GetObject(GameObject go,Vector3 position,Quaternion rotation)
    {
        //如果未初始化过 初始化池
        if(!pool.ContainsKey(go.name))
        {
            pool.Add(go.name, new Queue<GameObject>());
        }
        //如果池空了就创建新物体
        if(pool[go.name].Count == 0)
        {
            GameObject newObject = Instantiate(go, position, rotation);
            newObject.name = go.name;/*
            确认名字一样,防止系统加一个(clone),或序号累加之类的
            实际上为了更健全可以给每一个物体加一个key,防止对象的name一样但实际上不同
             */

            return newObject;
        }
        //从池中获取物体
        GameObject nextObject=pool[go.name].Dequeue();
        nextObject.SetActive(true);//要先启动再设置属性,否则可能会被OnEnable重置
        nextObject.transform.position = position;
        nextObject.transform.rotation = rotation;
        return nextObject;
    }

    /// <summary>
    /// 把物体放回池里
    /// </summary>
    /// <param name="go">需要放回队列的物品</param>
    /// <param name="t">延迟执行的时间</param>
    /// TODO 应该做个检查put的gameobject的池有没有创建过池
    public void PutObject(GameObject go,float t)
    {
        if (pool[go.name].Count >= MaxCount)
            Destroy(go,t);
        else
            StartCoroutine(ExecutePut(go,t));
    }

    private IEnumerator ExecutePut(GameObject go, float t)
    {
        yield return new WaitForSeconds(t);
        go.SetActive(false);
        pool[go.name].Enqueue(go);
    }

    /// <summary>
    /// 物体预热/预加载
    /// </summary>
    /// <param name="go">需要预热的物体</param>
    /// <param name="number">需要预热的数量</param>
    /// TODO 既然有预热用空间换时间 应该要做一个清理用时间换空间的功能
    public void Preload(GameObject go,int number)
    {
        if (!pool.ContainsKey(go.name))
        {
            pool.Add(go.name, new Queue<GameObject>());
        }
        for (int i = 0; i < number; i++)
        {
            GameObject newObject = Instantiate(go);
            newObject.name = go.name;//确认名字一样,防止系统加一个(clone),或序号累加之类的
            newObject.SetActive(false);
            pool[go.name].Enqueue(newObject);
        }
    }

}

Depois, há o script de teste GameManager

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

public class GameManager : MonoBehaviour
{

    public GameObject testObject;
    // Start is called before the first frame update
    void Start()
    {
        //预热
       ObjectPool.me.Preload(testObject, 500);
    }

    //无对象池测试
    public void TestOfNotOP()
    {
        StartCoroutine(CreateOfNotOP());
    }


    private IEnumerator CreateOfNotOP()
    {
        //统计500帧所用时间
        float t = 0.0f;
        //每一帧生成一个对象,定时2秒后自动消除
        for (int i = 0; i < 500; i++)
        {
            int x = Random.Range(-30, 30);
            int y = Random.Range(-30, 30);
            int z = Random.Range(-30, 30);
            GameObject newObject=Instantiate(testObject, new Vector3(x, y, z),Quaternion.identity);
            Destroy(newObject, 2.0f);

            yield return null;
            t += Time.deltaTime;
        }
        Debug.Log("无对象池500帧使用秒数:"+t);
    }

    //使用对象池测试
    public void TestOfOP()
    {

        StartCoroutine(CreateOfOP());
    }

    private IEnumerator CreateOfOP()
    {
        //统计500帧所用时间
        float t = 0.0f;
        //每一帧生成一个对象,定时2秒后自动消除
        for (int i = 0; i < 500; i++)
        {
            int x = Random.Range(-30, 30);
            int y = Random.Range(-30, 30);
            int z = Random.Range(-30, 30);
            GameObject newObject = ObjectPool.me.GetObject(testObject, new Vector3(x, y, z), Quaternion.identity);
            ObjectPool.me.PutObject(newObject, 2.0f);
            yield return null;
            t += Time.deltaTime;
        }
        Debug.Log("使用对象池500帧使用秒数:"+t);
    }
}

Fiz dois botões em um Canvas e, em seguida, criei um GameObject vazio usado como GameManager, vinculado ObjectPool (script de pool de objetos) e GameManager (script de teste), deixe os eventos OnClick dos dois botões ouvirem TestOfNotOP respectivamente () e TestOfOP ()

Por fim, use um efeito de partícula baixado do armazenamento de recursos para testar

(Veja minha estação B para um vídeo de teste detalhado)

Pode-se ver que reproduzir 500 quadros (um efeito de partícula é gerado por quadro) leva cerca de 9,8-9,9 segundos se o pool de objetos não for usado, e reproduzir 500 frames após a aplicação do pool de objetos leva cerca de 9,4-9,5 segundos. Ainda há resultados otimizados. Se for a versão antiga da unidade, pode ser mais otimizado. Isso faz muito tempo, Yusong também reclamou sobre http://www.xuanyusong.com/archives/2925 Agora, a inicialização dos efeitos de partículas de unidade Deve ser otimizado. Em geral, todas as técnicas de otimização, custos de otimização e resultados devem ser medidos.

Acho que você gosta

Origin blog.csdn.net/sun124608666/article/details/112602188
Recomendado
Clasificación