Pesquisar em Unity

Trabalhe otimamente com estruturas de dados C# e os API Unity

Last updated: December 2018

What you will get from this page: solutions for working optimally with Unity APIs and data structures. Challenges covered include recursive iteration with Particle System APIs; frequent memory allocation; APIs that return Arrays; choosing incorrect data structures, and too much overhead from the misuse of dictionaries.

A lot of these tips will introduce additional complexity, which results in higher maintenance overhead and the possibility for more bugs. Therefore, profile your code first before applying some of these solutions.

All of these tips are from the Unite talk, Squeezing Unity: Dicas para aumentar o desempenho,. For updated tips from Ian that take into account the evolution of Unity’s architecture to data-oriented design, see Evolução das melhores práticas.

Unity APIs

Problema: iteração recursiva com APIs do sistema de partículas:

Quando você chama uma das API principais do sistema de partículas, como Start, Stop, IsAlive (), ele itera recursivamente, por defeito, através de todos os filhos na hierarquia de um sistema de partículas:

  • A API encontra todos os filhos no Transform de um sistema de partículas, chama GetComponent em cada Transform, e invoca o método interno apropriado se existe um Sistema de Partículas.
  • Se esses filhos Transforms tiverem seus próprios filhos, se recorrerá todo o caminho até o final da hierarquia Transform - cada filho, neto e assim por diante é processado. Isso pode ser um problema com uma hierarquia Transform profunda.

Solução:

Estas APIs têm um parâmetro withChildren , que é verdadeiro por defeito. Configure-o como falso para interromper o comportamento recursivo. Se withChildren estiver configurado como falso, somente o sistema de partículas que você está chamando diretamente será alterado.

Problema: múltiplos sistemas de partículas que começam e param ao mesmo tempo

É comum que os artistas criem efeitos visuais que tenham sistemas de partículas dispersos por vários filhos Transforms, os quais você pode querer começar e parar ao mesmo tempo.

Solução:

Crie um MonoBehaviour que armazena em cache a lista de Sistemas de Partículas chamando a lista GetComponent no tempo de inicialização. Então, quando os Sistemas de Partículas precisam ser alterados, chame Start, Stop, etc., em cada um deles, e certifique-se de que "falso" esteja configurado como o parâmetro withChildren .

Problema: Alocação de memória frequente

Quando uma closure em C# fecha sobre uma variável local, o tempo de execução C# deve atribuir uma referência ao heap para acompanhar essa variável. Nas versões Unity 5.4 até 2017.1, existem algumas APIs de Sistema de Partículas que usam closures internamente. Nessas versões Unity, todas as chamadas para Stop and Simulate alocam memória mesmo se o sistema de partículas já parou.

Solução:

Todas as APIs do Sistema de Partículas, como a maioria das APIs públicas da Unity, são apenas funções do wrapper C# em torno de funções internas que executam o trabalho real da API. Algumas dessas funções internas são C# puro, mas a maioria acaba chamando o núcleo C++ do engine Unity. No caso das APIs do Sistema de Partículas, todas as APIs internas são convenientemente denominadas "Internal_", seguidas do nome da função pública, como por exemplo:

work-optimally-with-Unity-APIs-table-graph

...e assim por diante.

Em vez de permitir que as APIs públicas da Unity recuperem seus ajudantes internos por meio de Closures, você pode escrever um método de extensão ou endereçar o método interno via Reflection e armazenar em cache a referência para uso posterior. Estas são as assinaturas de função relevantes:

work-optimally-with-Unity-APIs-functions-signatures

Todos os argumentos têm os mesmos significados que a API pública, exceto a primeira, que é uma referência ao Sistema de Partículas que deseja parar ou simular.

Problema: APIs que retornam Matrizes

Cada vez que você acessa uma API Unity, que devolve uma matriz, esta atribui uma nova cópia dessa matriz. Isso ocorre tanto ao usar funções quanto ao usar propriedades que devolvem matrizes. Os exemplos típicos são Mesh.vertices: ao acessar, você receberá uma nova cópia dos vértices da malha. E Input.touches cria uma cópia de todos os toques do usuário durante o frame atual.

Solução:

Muitas APIs Unity agora incluem versões não atribuíveis que você deve usar. Por exemplo, em vez de Input.touches, use Input.GetTouch e Input.touchCount. A partir da versão 5.3, foram introduzidas versões não atribuíveis para todas as APIs de consulta de Physics. Para aplicações 2D, também existem versões não atribuídas de todas as APIs de consulta de Physics2D. Para obter mais informações sobre as versões não atribuídas das APIs de Physics de Unity, consulte aqui.

Finalmente, se você usa GetComponents ou GetComponentsInChildren , agora existem versões que aceitam uma lista genérica de templates. Essas APIs preenchem a lista com os resultados da chamada GetComponents . Este processo não é exclusivamente não atributivo: se os resultados da chamada GetComponents excederem a capacidade da lista, esta será redimensionada. No entanto, reutilizar ou resumir a lista irá pelo menos reduzir a frequência das atribuições ao longo da vida útil do seu aplicativo.

Uso ótimo de estruturas de dados

Problema: Escolha de estruturas de dados incorretas

Evite selecionar estruturas de dados apenas porque são fáceis de usar. Em vez disso, opte por uma cujas características de desempenho correspondem melhor ao algoritmo ou sistema de jogo que você está escrevendo.

Solução:

A indexação em uma array ou lista é fácil, necessita apenas algumas adições e o processo é constante no tempo. Portanto, a indexação aleatória ou a iteração ocorre por meio de uma array ou lista com muito pouca sobrecarga. Então, se você iterar cada Frame em uma lista de elementos, você deve preferir arrays ou listas.

Se você precisa ter uma adição ou remoção de tempo constante, você provavelmente quer usar um Dicionário ou Hashset (veja detalhes abaixo).

Se você estiver relacionando dados de uma maneira orientada aos valores chave, em que um dado é relacionado a outro de maneira unidirecional, use um Dicionário.

Mais sobre Hashsets e Dicionários: Ambas as estruturas de dados são suportadas por uma tabela hash. Lembre-se de que uma tabela hash tem uma série de buckets; cada bucket é basicamente uma lista que contém todos os valores que possuem um código hash específico. Em C#, este código hash é determinado usando o método GetHashCode . Uma vez que o número de valores em um bucket é geralmente muito menor do que o tamanho total do Hashset ou Dicionário, adicionar ou remover elementos de um bucket está mais próximo de uma quantidade de tempo constante do que adicionar ou remover elementos aleatoriamente de uma Lista ou arrey. A diferença exata depende da capacidade da sua tabela hash e do número de elemtos que você está armazenando nela.

Verificar a presença (ou ausência) de um determinado valor em um Hashset ou Dicionário é muito fácil pelo mesmo motivo: é rápido verificar o número (relativamente pequeno) de valores no bucket que representa o código hash do valor.

Problema: Sobrecarga por uso inadequado de dicionários

Se você quiser iterar sobre pares de elementos de dados de cada frame, muitas vezes as pessoas usam um dicionário porque é conveniente. O problema é que você está iterando em uma tabela hash. Isso significa que cada bucket individual naquela tabela hash deve ser iterado, quer este contenha ou não valores. Isso acrescenta uma sobrecarga considerável, especialmente quando itera em tabelas hash com poucos valores armazenados.

Solução:

Em vez disso, crie uma estrutura ou uma tupla e, em seguida, armazene uma Lista ou array dessas estruturas ou tuplas que contenham suas relações de dados. Itere sobre esta lista/array em vez de iterar sobre o dicionário.

Cerca de 14:25 minutos na palestrade Ian. Ele tem uma dica rápida sobre como e quando usar os dicionários com senha InstanceID para reduzir a sobrecarga.

Problema: e se você tiver várias preocupações?

No mundo real, a maioria dos problemas apresenta, naturalmente, inúmeros requisitos de sobreposição e não existe uma estrutura de dados que atenda a todas as suas necessidades.

Um exemplo comum poderia ser um Update Manager, Este é um padrão de programação arquitetônica, em que um objeto (geralmente um monoBehaviour) irá atualizar callbacks para diferentes sistemas em seu jogo. Quando os sistemas querem receber atualizações, eles se inscrevem através do objeto Update Manager. Esse sistema requer iteração de carga baixa, inserção constante de tempo e verificações duplicadas de tempo constantes.

Solução: use duas estruturas de dados

  • Mantenha uma lista ou array para iteração.
  • Antes de alterar a lista, use um Hashset (ou outro tipo de conjunto de indexação) para se certificar de que o elemento que você está adicionando ou removendo está realmente presente.
  • Se a remoção é um problema, considere uma lista vinculada ou implementação de lista intrusivamente vinculada (tenha em conta que isso resulta em maior consumo de memória e sobrecarga de iteração ligeiramente superior).
Mais recursos

Queremos saber! Você gostou deste conteúdo?

Sim, continue. Bem. Poderia ser melhor