Ao implementar padrões comuns de design de programação de jogos em seu projeto Unity, você pode criar e manter com eficiência uma base de código limpa, organizada e legível. Os padrões de design não apenas reduzem a refatoração e o tempo gasto com testes, mas também aceleram os processos de integração e desenvolvimento, contribuindo para uma base sólida para o crescimento do jogo, da equipe de desenvolvimento e dos negócios.
Pense nos padrões de design não como soluções prontas que você pode copiar e colar em seu código, mas como ferramentas extras que, quando usadas corretamente, podem ajudá-lo a criar aplicativos maiores e escalonáveis.
Esta página explica o agrupamento de objetos e como ele pode ajudar a melhorar o desempenho de seu jogo. Ele inclui um exemplo de como implementar o sistema de pooling de objetos incorporado do Unity em seus projetos.
O conteúdo aqui é baseado no livro eletrônico gratuito, Eleve o nível de seu código com padrões de programação de jogosque explica padrões de design bem conhecidos e compartilha exemplos práticos para usá-los em seu projeto Unity.
Outros artigos da série de padrões de programação de jogos do Unity estão disponíveis no hub de práticas recomendadas do Unity ou clique nos links a seguir:
O pooling de objetos é um padrão de design que pode proporcionar otimização do desempenho, reduzindo a capacidade de processamento exigida da CPU para executar chamadas repetitivas de criação e destruição. Em vez disso, com o pooling de objetos, os GameObjects existentes podem ser reutilizados várias vezes.
A principal função do pooling de objetos é criar objetos antecipadamente e armazená-los em um pool, em vez de criá-los e destruí-los sob demanda. Quando um objeto é necessário, ele é retirado do pool e usado e, quando não é mais necessário, é devolvido ao pool em vez de ser destruído.
A imagem acima ilustra um caso de uso comum de pooling de objetos, como o disparo de projéteis de uma torre de armas. Vamos analisar esse exemplo passo a passo.
Em vez de criar e depois destruir, o padrão de pool de objetos usa um conjunto de objetos inicializados mantidos prontos e aguardando em um pool desativado. O padrão então pré-instancia todos os objetos necessários em um momento específico antes do jogo. O pool deve ser ativado em um momento oportuno, quando o jogador não perceberá a gagueira, por exemplo, durante uma tela de carregamento.
Depois que os GameObjects do pool forem usados, eles serão desativados e estarão prontos para serem usados quando o jogo precisar deles novamente. Quando um objeto é necessário, seu aplicativo não precisa instanciá-lo primeiro. Em vez disso, ele pode solicitá-lo do pool, ativá-lo e desativá-lo e, em seguida, devolvê-lo ao pool em vez de destruí-lo.
Esse padrão pode reduzir o custo do trabalho pesado necessário do gerenciamento de memória para executar a coleta de lixo, conforme explicado na próxima seção.
Antes de entrarmos em exemplos de como aproveitar o pooling de objetos, vamos analisar brevemente o problema principal que ele ajuda a resolver.
A técnica de pooling não é útil apenas para reduzir os ciclos de CPU gastos em operações de instanciação e destruição. Ele também otimiza o gerenciamento de memória reduzindo a sobrecarga de criação e destruição de objetos, o que exige que a memória seja alocada e desalocada, e que os construtores e destruidores sejam chamados.
Memória gerenciada no Unity
O ambiente de script em C# do Unity oferece um sistema de memória gerenciada. Ele ajuda a gerenciar a liberação de memória, para que você não precise solicitá-la manualmente por meio do seu código. O sistema de gerenciamento de memória também ajuda a proteger o acesso à memória, garantindo que a memória que você não usa mais seja liberada e impedindo o acesso à memória que não é válida para o seu código.
O Unity usa um coletor de lixo para recuperar a memória dos objetos que seu aplicativo e o Unity não estão mais usando. No entanto, isso também afeta o desempenho do tempo de execução, porque a alocação da memória gerenciada consome tempo da CPU, e a coleta de lixo (GC) pode impedir que a CPU faça outras tarefas até que ela conclua sua tarefa.
Toda vez que você cria um novo objeto ou destrói um objeto existente no Unity, a memória é alocada e desalocada. É nesse ponto que o pooling de objetos entra em ação: Ele reduz a gagueira que pode resultar de picos de coleta de lixo. Os picos de GC geralmente acompanham a criação ou destruição de um grande número de objetos devido à alocação de memória. Além das coletas prematuras de lixo, o processo também pode causar fragmentação da memória, o que dificulta a localização de regiões de memória contíguas livres.
Ao reciclar os mesmos objetos existentes, desativando-os e ativando-os, é possível criar um efeito, como disparar centenas de balas fora da tela, quando, na realidade, você simplesmente os desativa e recicla.
Saiba mais sobre o gerenciamento de memória em nosso guia avançado de criação de perfil.
Embora você possa criar seu próprio sistema personalizado para implementar o pooling de objetos, há uma classe ObjectPool integrada no Unity que você pode usar para implementar esse padrão de forma eficiente em seu projeto (disponível no Unity 2021 LTS e em diante).
Vamos ver como aproveitar o sistema de agrupamento de objetos integrado usando a API UnityEngine.Pool com este projeto de amostra que está disponível no Github. Uma vez na página do Github, acesse Assets>7 Object Pool >Scripts > ExampleUsage2021 para obter os arquivos.
Observação: Você pode dar uma olhada neste tutorial do Unity Learn para ver um exemplo de pooling de objetos de uma versão anterior do Unity.
Este exemplo consiste em uma torre que dispara rapidamente projéteis (definidos como 10 projéteis por segundo por padrão) quando o botão do mouse é pressionado. Cada projétil viaja pela tela e precisa ser destruído quando sai da tela. Sem o pooling de objetos, isso pode gerar uma sobrecarga considerável na CPU e no gerenciamento de memória, conforme explicado na seção anterior.
Ao usar o agrupamento de objetos, parece que centenas de balas estão sendo disparadas fora da tela quando, na realidade, elas são simplesmente desativadas e recicladas várias vezes.
O código no script de exemplo ajuda a garantir que o tamanho do pool seja grande o suficiente para mostrar os objetos ativos simultaneamente, camuflando assim o fato de que os mesmos objetos estão sendo constantemente reutilizados.
Se você já usou o sistema de partículas do Unity, então tem experiência em primeira mão com um pool de objetos. O componente Particle System contém uma configuração para o número máximo de partículas. Isso recicla as partículas disponíveis, evitando que o efeito ultrapasse um número máximo. O pool de objetos funciona de forma semelhante, mas com qualquer GameObject de sua escolha.
Vamos dar uma olhada no código em RevisedGun.cs que está localizado na demonstração do Github em Assets>7 Object Pool >Scripts > ExampleUsage2021.
O primeiro aspecto a ser observado é a inclusão do namespace do pool:
using UnityEngine.Pool;
Ao usar a API UnityEngine.Pool, você obtém um ObjectPool baseada em pilha para rastrear objetos com o padrão de pool de objetos. Dependendo de suas necessidades, você também pode usar uma classe CollectionPool (List, HashSet, Dictionary, etc.)
Em seguida, você aplica configurações específicas para as características de disparo da arma, incluindo o Prefab a ser gerado (denominado projectilePrefab do tipo RevisedProjectile).
A interface ObjectPool é referenciada no RevisedProjectile.cs (que é explicado na próxima seção) e inicializada na função Awake.
private void Awake()
{
objectPool = novo ObjectPool<RevisedProjectile>(CreateProjectile,
OnGetFromPool, OnReleaseToPool,
OnDestroyPooledObject,collectionCheck, defaultCapacity, maxSize);
}
Se você explorar o construtor ObjectPool<T0>, verá que ele inclui a capacidade útil de configurar alguma lógica quando:
Primeiro, criar um item agrupado para preencher o pool
Retirar um item da piscina
Devolver um item ao pool
Destruição de um objeto agrupado (por exemplo, se você atingir um limite máximo)
Observe como a classe ObjectPool incorporada também inclui opções para o tamanho padrão e máximo do pool, sendo este último o número máximo de itens armazenados no pool. Ele é acionado quando você chama Release e, se o pool estiver cheio, ele é destruído.
Vamos ver como o exemplo de código executa várias ações que especificam como o Unity deve lidar com o pooling de objetos de forma eficiente, de acordo com seu caso de uso específico.
Primeiro, é passada a função createFunc que é usada para criar uma nova instância quando o pool está vazio, que, nesse caso, é a função CreateProjectile() que instancia um novo perfil Prefab.
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
return projectileInstance;
}
O OnGetFromPool é chamado quando você solicita uma instância do GameObject, portanto, você habilita o GameObject que está obtendo do pool por padrão.
private void OnGetFromPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(true);
}
O OnReleaseToPool é usado quando o GameObject não é mais necessário e é devolvido ao pool - neste exemplo, é simplesmente uma questão de desativá-lo novamente.
private void OnReleaseToPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(false);
}
OnDestroyPooledObject é chamado quando você excede o número máximo permitido de itens agrupados. Se o pool já estiver cheio, o objeto será destruído.
private void OnDestroyPooledObject(RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}
A collectionChecks é usada para inicializar o IObjectPool e lançará uma exceção quando você tentar liberar um GameObject que já tenha sido devolvido ao gerenciador de pool, mas essa verificação só é realizada no Editor. Ao desativá-lo, você pode economizar alguns ciclos de CPU, porém, com o risco de obter o retorno de um objeto que já tenha sido reativado.
Como o nome indica, o defaultCapacity é o tamanho padrão da pilha/lista que conterá seus elementos e, portanto, a quantidade de alocação de memória que você deseja comprometer antecipadamente. O maxPoolSize será o tamanho máximo da pilha, e os GameObjects agrupados criados nunca devem exceder esse tamanho. Isso significa que, se você devolver um item a um pool que esteja cheio, o item será destruído.
Então, em FixedUpdate(), você obterá um objeto agrupado em vez de instanciar um novo projétil toda vez que executar a lógica para disparar uma bala.
RevisedProjectile bulletObject = objectPool.Get();
É tão simples quanto isso.
Agora vamos dar uma olhada no script RevisedProjectile.cs.
Além de configurar uma referência ao ObjectPool, o que torna a liberação do objeto de volta ao pool mais conveniente, há alguns detalhes de interesse.
O timeoutDelay é usado para manter o controle de quando o projétil foi "usado" e pode ser devolvido ao pool do jogo novamente - isso acontece por padrão após três segundos.
A função Deactivate() ativa uma corrotina chamada DeactivateRoutine(float delay), que não só libera o projétil de volta para a piscina com objectPool.Release(this), mas também redefine os parâmetros de velocidade do corpo rígido em movimento.
Esse processo aborda o problema dos "itens sujos": objetos que foram usados no passado e precisam ser redefinidos devido ao seu estado indesejável.
Como você pode ver neste exemplo, a API UnityEngine.Pool torna a configuração de pools de objetos eficiente, porque você não precisa reconstruir o padrão do zero, a menos que tenha um caso de uso específico para isso.
Você não está limitado apenas a GameObjects. O pooling é uma técnica de otimização de desempenho para reutilizar qualquer tipo de entidade C#: um GameObject, um Prefab instanciado, um dicionário C# e assim por diante. O Unity oferece algumas classes alternativas de agrupamento para outras entidades, como DictionaryPool<T0,T1>, que oferece suporte a dicionários, e HashSetPool<T0>, para HashSets. Saiba mais sobre isso na documentação.
O LinkedPool usa uma lista vinculada para manter uma coleção de instâncias de objetos para reutilização, o que pode levar a um melhor gerenciamento de memória (dependendo do seu caso), pois você só usa memória para os elementos que estão realmente armazenados no pool.
Compare isso com o ObjectPool, que simplesmente usa uma pilha C# e uma matriz C# por baixo e, como tal, contém um grande pedaço de memória contígua. A desvantagem é que você gasta mais memória por item e mais ciclos de CPU para gerenciar essa estrutura de dados no LinkedPool do que no ObjectPool, onde você pode utilizar o defaultSize e o maxSize para configurar suas necessidades.
O modo como você usa os pools de objetos varia de acordo com o aplicativo, mas o padrão geralmente aparece quando uma arma precisa disparar vários projéteis, conforme ilustrado no exemplo anterior.
Uma boa regra geral é criar um perfil do seu código sempre que instanciar um grande número de objetos, pois você corre o risco de causar um pico de coleta de lixo. Se estiver detectando picos significativos que colocam sua jogabilidade em risco de gagueira, considere o uso de um pool de objetos. Lembre-se apenas de que o pooling de objetos pode aumentar a complexidade de sua base de código devido à necessidade de gerenciar os vários ciclos de vida dos pools. Além disso, você também pode acabar reservando memória que sua jogabilidade não precisa necessariamente ao criar muitos pools prematuros.
Conforme mencionado anteriormente, há várias outras maneiras de implementar o pooling de objetos fora do exemplo incluído neste artigo. Uma maneira é criar sua própria implementação, que pode ser personalizada de acordo com suas necessidades. Mas você precisará estar atento às complicações da segurança de tipos e threads, bem como definir a alocação/desalocação de objetos personalizados.
Felizmente, a Unity Asset Store oferece algumas ótimas alternativas para economizar seu tempo.
Recursos mais avançados para programação em Unity
O livro eletrônico, Aumente o nível de seu código com padrões de programação de jogosfornece um exemplo mais completo de um sistema simples de pool de objetos personalizados. O Unity Learn também oferece uma introdução ao agrupamento de objetos, que você pode encontrar aqui, e um tutorial completo para usar o novo sistema de agrupamento de objetos integrado no 2021 LTS.
Todos os e-books e artigos técnicos avançados estão disponíveis no site Melhores práticas do Unity hub. Os livros eletrônicos também estão disponíveis no site práticas recomendadas avançadas na página de documentação.