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.