Os 10 erros mais comuns que os desenvolvedores de Unity cometem
Publicados: 2022-03-11O Unity é uma ferramenta ótima e simples de usar para desenvolvimento multiplataforma. Seus princípios são fáceis de entender e você pode começar intuitivamente a criar seus produtos. No entanto, se algumas coisas não forem levadas em consideração, elas retardarão seu progresso quando você prosseguir com seu trabalho para o próximo nível, conforme você estiver saindo da fase de protótipo inicial ou se aproximando de uma versão final. Este artigo fornecerá conselhos sobre como superar os problemas mais comuns e como evitar erros fundamentais em seus projetos novos ou existentes. Observe que a perspectiva deste artigo está mais focada no desenvolvimento de aplicativos 3D, mas tudo o que foi mencionado também é aplicável ao desenvolvimento 2D.
Erro comum da unidade nº 1: subestimar a fase de planejamento do projeto
Para cada projeto, é crucial determinar várias coisas antes mesmo de começar a parte de design e programação do aplicativo. Nos dias de hoje, quando o marketing do produto é uma parte importante de todo o processo, também é importante ter uma ideia clara de qual será o modelo de negócio da aplicação implementada. Você precisa ter certeza para quais plataformas você lançará o produto e quais plataformas estão em seu plano. Também é necessário definir as especificações mínimas de dispositivos suportados (você suportará dispositivos de baixo custo mais antigos ou apenas modelos mais recentes?) para ter uma ideia de qual desempenho e recursos visuais você pode pagar. Todos os tópicos deste artigo são influenciados por esse fato.
De um ponto de vista mais técnico, deve ser necessário definir antecipadamente todo o fluxo de trabalho de criação de ativos e modelos enquanto os fornece ao programador, com atenção especial ao processo de iteração quando os modelos precisarem de mais algumas alterações e refinamentos. Você deve ter uma ideia clara sobre a taxa de quadros desejada e o orçamento de vértices, para que o artista 3D possa saber em qual resolução máxima os modelos devem ter e quantas variações de LOD ele precisa fazer. Também deve ser especificado como unificar todas as medidas para ter uma escala consistente e processo de importação em toda a aplicação.
A forma como os níveis serão desenhados é crucial para trabalhos futuros, pois a divisão do nível influencia muito no desempenho. As preocupações com o desempenho devem estar sempre em sua mente ao projetar novos níveis. Não vá com visões irreais. É sempre importante fazer a si mesmo a pergunta “pode ser razoavelmente alcançado?” Caso contrário, você não deve desperdiçar seus preciosos recursos em algo dificilmente alcançável (caso não faça parte da sua estratégia de negócios tê-lo como sua principal vantagem competitiva, é claro).
Erro comum de unidade nº 2: trabalhando com modelos não otimizados
É fundamental ter todos os seus modelos bem preparados para poder usá-los em suas cenas sem modificações adicionais. Há várias coisas que o bom modelo deve cumprir.
É importante definir a escala corretamente. Às vezes, não é possível definir isso corretamente em seu software de modelagem 3D devido às diferentes unidades que esses aplicativos estão usando. Para fazer tudo certo, defina o fator de escala nas configurações de importação de modelos (deixe 0,01 para 3dsMax e Modo, defina 1,0 para Maya) e observe que às vezes você precisará reimportar objetos após alterar a configuração de escala. Essas configurações devem garantir que você possa usar apenas a escala básica 1,1,1 em suas cenas para obter um comportamento consistente e sem problemas de física. O lote dinâmico também provavelmente funcionará corretamente. Essa regra também deve ser aplicada em todos os subobjetos do modelo, não apenas no principal. Quando você precisar ajustar as dimensões do objeto, faça-o em relação a outros objetos no aplicativo de modelagem 3D em vez de no Unity. No entanto, você pode experimentar a escala no Unity para descobrir os valores apropriados, mas para o aplicativo final e o fluxo de trabalho consistente, é bom ter tudo bem preparado antes de importar para o Unity.
Em relação à funcionalidade do objeto e suas partes dinâmicas - tenha seus modelos bem divididos. Quanto menos subobjetos, melhor. Separe partes do objeto para o caso de precisar delas, por exemplo, para mover ou girar dinamicamente, para fins de animação ou outras interações. Todo objeto e seus subobjetos devem ter seu pivô devidamente alinhado e girado em relação à sua função principal. O objeto principal deve ter o eixo Z apontando para frente e o pivô deve estar na parte inferior do objeto para melhor posicionamento na cena. Use o mínimo possível de materiais em objetos (mais sobre isso abaixo).
Todos os ativos devem ter nomes próprios que descrevam facilmente seu tipo e funcionalidade. Mantenha essa consistência em todos os seus projetos.
Erro comum de unidade nº 3: Construindo uma arquitetura de código interdependente
A prototipagem e a implementação de funcionalidades no Unity são bastante fáceis. Você pode facilmente arrastar e soltar qualquer referência a outros objetos, endereçar cada objeto na cena e acessar todos os componentes que ele possui. No entanto, isso também pode ser potencialmente perigoso. Além dos problemas de desempenho perceptíveis (encontrar um objeto na hierarquia e acessar os componentes tem sua sobrecarga), também há um grande perigo em tornar partes do seu código totalmente dependentes umas das outras. Ou ser dependente de outros sistemas e scripts exclusivos da sua aplicação, ou até mesmo da cena atual, ou do cenário atual. Tente adotar uma abordagem mais modular e crie peças reutilizáveis que possam ser usadas em outras partes do seu aplicativo ou até mesmo compartilhadas em todo o seu portfólio de aplicativos. Construa sua estrutura e bibliotecas com base na API do Unity da mesma forma que você está construindo sua base de conhecimento.
Há muitas abordagens diferentes para garantir isso. Um bom ponto de partida é o próprio sistema de componentes do Unity. Complicações podem aparecer quando determinados componentes precisam se comunicar com outros sistemas do aplicativo. Para isso, você pode usar interfaces para tornar partes do seu sistema mais abstratas e reutilizáveis. Como alternativa, você pode usar uma abordagem orientada a eventos para reagir a eventos específicos de fora do escopo, criando um sistema de mensagens ou registrando-se diretamente em partes do outro sistema como ouvintes. A abordagem correta será tentar separar as propriedades do gameObject da lógica do programa (pelo menos algo como o princípio do controlador de modelo), porque é difícil identificar quais objetos estão modificando suas propriedades de transformação, como posição e rotação. Deve ser de responsabilidade exclusiva de seu controlador.
Tente fazer tudo bem documentado. Trate-o sempre como se você voltasse ao seu código depois de muito tempo e precisasse entender rapidamente o que exatamente essa parte do código está fazendo. Porque, na realidade, muitas vezes você chegará a algumas partes do seu aplicativo depois de algum tempo e é um obstáculo desnecessário para pular rapidamente para o problema. Mas não exagere. Às vezes, um nome de classe, método ou propriedade apropriado é suficiente.
Erro comum de unidade nº 4: desperdiçando seu desempenho
A mais recente linha de produtos de telefones celulares, consoles ou computadores de mesa nunca será tão avançada que não haja necessidade de se preocupar com o desempenho. Otimizações de desempenho são sempre necessárias e fornecem a base para fazer a diferença na aparência do seu jogo ou aplicativo em comparação com outros no mercado. Porque quando você economiza algum desempenho em uma parte, você pode usar isso para polir outras partes do seu aplicativo.
Existem muitas áreas para otimizações. O artigo inteiro seria necessário apenas para arranhar a superfície sobre este tópico. Pelo menos, tentarei dividir este domínio em algumas áreas centrais.
Atualizar loops
Não use coisas intensivas em desempenho em loops de atualização, use cache em vez disso. Um exemplo típico é um acesso a componentes ou outros objetos em uma cena ou cálculos intensivos em seus scripts. Se possível, armazene tudo em cache nos métodos Awake()
ou altere sua arquitetura para uma abordagem mais orientada a eventos para acionar as coisas apenas quando elas forem necessárias.
Instanciações
Para objetos que são instanciados com bastante frequência (por exemplo, balas em um jogo FPS), faça um pool pré-inicializado deles e apenas escolha um já inicializado quando precisar e ative-o. Então, em vez de destruí-lo quando não for mais necessário, desative-o e devolva-o ao pool.
Renderização
Use técnicas de seleção de oclusão ou LOD para limitar partes renderizadas da cena. Tente usar modelos otimizados para poder manter a contagem de vértices na cena sob controle. Esteja ciente de que a contagem de vértices não é apenas o número de vértices no modelo em si, mas é influenciada por outras coisas como normais (arestas duras), coordenadas UV (costuras UV) e cores de vértices. Além disso, várias luzes dinâmicas na cena influenciarão drasticamente o desempenho geral, portanto, tente preparar tudo com antecedência sempre que possível.
Desenhar chamadas
Tente reduzir a contagem de chamadas de sorteio. No Unity, você pode obter a redução de chamadas de desenho usando lotes estáticos para objetos parados e lotes dinâmicos para objetos em movimento. No entanto, você precisa preparar suas cenas e modelos primeiro (objetos em lote precisam compartilhar os mesmos materiais), e o agrupamento de objetos dinâmicos funciona apenas para modelos de baixa resolução. Alternativamente, você pode combinar malhas pelo script em uma ( Mesh.CombineMeshes
) em vez de usar lotes, mas você deve ter cuidado para não criar objetos muito grandes que não podem tirar vantagem da exibição de seleção de frustum em algumas plataformas. Em geral, a chave é usar o mínimo de materiais possível e compartilhá-los em toda a cena. Às vezes, você precisará criar atlas a partir de texturas para poder compartilhar um material entre objetos distintos. Uma boa dica também é usar uma resolução mais alta de texturas de mapas de luz de cena (não resolução gerada, mas resolução de saída de textura) para diminuir seu número quando você estiver iluminando ambientes maiores.
Problemas de saque excessivo
Não use texturas transparentes quando não for necessário, pois isso causará problemas de taxa de preenchimento. Não há problema em usá-lo para geometrias complicadas e mais distantes, como árvores ou arbustos. Quando você precisar usá-lo, prefira sombreadores misturados alfa em vez de sombreadores com teste alfa ou em vez de sombreadores recortados para plataformas móveis. Para identificar esses problemas em geral, tente diminuir a resolução do seu aplicativo. Se isso ajudar, é possível que você tenha esses problemas de taxa de preenchimento ou precise otimizar mais seus sombreadores. Caso contrário, pode ser mais um problema de memória.

Tonalizadores
Otimize seus shaders para um melhor desempenho. Reduza o número de passagens, use variáveis com menor precisão, substitua cálculos matemáticos complicados por texturas de pesquisa pré-geradas.
Sempre use um criador de perfil para determinar os gargalos. É uma ótima ferramenta. Para renderização, você também pode usar o incrível Frame Debugger, que o ajudará a aprender muito sobre como as coisas funcionam em geral ao decompor os processos de renderização com ele.
Erro comum de unidade nº 5: ignorando problemas de coleta de lixo
É necessário perceber que apesar do Garbage Collector (GC) em si nos ajudar a ser realmente eficientes e focados em coisas importantes na programação, existem algumas coisas que devemos estar explicitamente cientes. O uso do GC não é gratuito. Geralmente, devemos evitar alocações de memória desnecessárias para evitar que o GC seja acionado com muita frequência e, assim, prejudique o desempenho por picos de taxa de quadros. Idealmente, não deve haver nenhuma nova alocação de memória acontecendo regularmente a cada quadro. No entanto, como podemos atingir esse objetivo? É realmente determinado pela arquitetura do aplicativo, mas existem algumas regras que você pode seguir que ajudam:
- Evite alocações desnecessárias em loops de atualização.
- Use structs para contêineres de propriedade simples, pois eles não são alocados no heap.
- Tente pré-alocar arrays ou listas ou outras coleções de objetos, em vez de criá-los dentro de loops de atualização.
- Evite usar coisas mono problemáticas (como expressões LINQ ou loops foreach, por exemplo) porque o Unity está usando uma versão mais antiga, não idealmente otimizada do Mono (no momento em que escrevo, é a versão 2.6 modificada, com atualização no roteiro).
- Cache de strings em métodos
Awake()
ou em eventos. - Se a atualização da propriedade string no loop de atualização for necessária, use o objeto StringBuilder em vez de string.
- Use o criador de perfil para identificar possíveis problemas.
Erro comum de unidade nº 6: otimizar o uso de memória e espaço por último
É necessário manter a atenção no menor uso de memória e espaço do aplicativo desde o início do projeto, pois é mais complicado fazer isso quando você deixa a otimização para a fase de pré-lançamento. Em dispositivos móveis, isso é ainda mais importante, porque temos poucos recursos lá. Além disso, ao exceder o tamanho de 100 MB da instalação, podemos perder uma quantidade significativa de nossos clientes. Isso se deve ao limite de 100 MB para downloads da rede celular e também por motivos psicológicos. É sempre melhor quando seu aplicativo não desperdiça os preciosos recursos telefônicos do cliente, e eles estarão mais propensos a baixar ou comprar seu aplicativo quando seu tamanho for menor.
Para encontrar drenadores de recursos, você pode usar o log do editor onde pode ver (após cada nova compilação) o tamanho dos recursos divididos em categorias separadas, como áudio, texturas e DLLs. Para melhor orientação, existem extensões de editor no Unity Asset Store, que fornecerão um resumo detalhado com recursos e arquivos referenciados em seu sistema de arquivos. O consumo real de memória também pode ser visto no criador de perfil, mas é recomendável testá-lo quando conectado para compilar em sua plataforma de destino, pois há muitas inconsistências ao testar em um editor ou em qualquer outra coisa que não seja sua plataforma de destino.
Os maiores consumidores de memória geralmente são texturas. De preferência, use texturas compactadas, pois elas ocupam muito menos espaço e memória. Faça todas as texturas ao quadrado, idealmente, faça o comprimento de ambos os lados potência de dois (POT), mas lembre-se de que o Unity também pode dimensionar texturas NPOT para POT automaticamente. As texturas podem ser compactadas no formato POT. Texturas Atlas juntas para preencher toda a textura. Às vezes, você pode até usar o canal alfa de textura para obter algumas informações extras para seus sombreadores para economizar espaço e desempenho adicionais. E, claro, tente reutilizar texturas para suas cenas tanto quanto possível, e use texturas repetidas quando for possível manter uma boa aparência visual. Para dispositivos de baixo custo, você pode diminuir a resolução das texturas em Configurações de qualidade. Use o formato de áudio compactado para clipes de áudio mais longos, como a música de fundo.
Ao lidar com diferentes plataformas, resoluções ou localizações, você pode usar pacotes de ativos para usar diferentes conjuntos de texturas para diferentes dispositivos ou usuários. Esses pacotes de ativos podem ser carregados dinamicamente da Internet após a instalação do aplicativo. Dessa forma, você pode ultrapassar o limite de 100 MB baixando recursos durante o jogo.
Erro comum de unidade nº 7: erros comuns de física
Às vezes, ao mover objetos na cena, não percebemos que o objeto tem um colisor e que mudar sua posição forçará o motor a recalcular todo o mundo físico novamente. Nesse caso, você deve adicionar o componente Rigidbody
a ele (você pode configurá-lo para não cinemático se não quiser que forças externas estejam envolvidas).
Para modificar a posição do objeto com Rigidbody
nele, sempre setar Rigidbody.position
quando uma nova posição não seguir a anterior, ou Rigidbody.MovePosition
quando for um movimento contínuo, que também leva em consideração a interpolação. Ao modificá-lo, aplique as operações sempre em FixedUpdate
, não em funções Update
. Assegurará comportamentos físicos consistentes.
Se possível, use colisores primitivos em gameObjects, como esfera, caixa ou cilindro, e não colisores de malha. Você pode compor seu colisor final a partir de mais de um desses colisores. A física pode ser um gargalo de desempenho do seu aplicativo devido à sobrecarga da CPU e as colisões entre os colisores primitivos são muito mais rápidas de calcular. Você também pode ajustar a configuração Fixed Timestep no gerenciador de tempo para reduzir a frequência de atualizações fixas de física quando a precisão da interação física não for tão necessária.
Erro comum de unidade nº 8: testando manualmente todas as funcionalidades
Às vezes, pode haver uma tendência de testar a funcionalidade manualmente experimentando no modo de jogo porque é bastante divertido e você tem tudo sob seu controle direto. Mas esse fator legal pode diminuir muito rapidamente. Quanto mais complexo o aplicativo se torna, mais tarefas tediosas o programador precisa repetir e pensar para garantir que o aplicativo se comporte como foi originalmente planejado. Pode facilmente se tornar a pior parte de todo o processo de desenvolvimento, devido ao seu caráter repetitivo e passivo. Além disso, como a repetição manual de cenários de teste não é tão divertida, há uma chance maior de que alguns bugs passem por todo o processo de teste.
O Unity tem ótimas ferramentas de teste para automatizar isso. Com arquitetura e design de código apropriados, você pode usar testes de unidade para testar funcionalidades isoladas ou até mesmo testes de integração para testar cenários mais complexos. Você pode reduzir drasticamente a abordagem de tentativa e verificação em que está registrando dados reais e comparando-os com o estado desejado.
O teste manual é, sem dúvida, uma parte crítica do desenvolvimento. Mas sua quantidade pode ser reduzida, e todo o processo pode ser mais robusto e rápido. Quando não houver uma maneira possível de automatizá-lo, prepare suas cenas de teste para poder entrar no problema que você está tentando resolver o mais rápido possível. Idealmente, alguns quadros depois de apertar o botão play. Implemente atalhos ou truques para definir o estado desejado para teste. Além disso, isole a situação de teste para ter certeza do que está causando o problema. Cada segundo desnecessário no modo de reprodução quando o teste é acumulado, e quanto maior o viés inicial de testar o problema, maior a probabilidade de você não testar o problema e esperar que tudo funcione bem. Mas provavelmente não vai.
Erro comum do Unity nº 9: pensar em plug-ins da Unity Asset Store resolverá todos os seus problemas
Confie em mim; eles não vão. Ao trabalhar com alguns clientes, às vezes enfrentei a tendência ou relíquias do passado de usar plugins de armazenamento de ativos para cada coisinha. Não quero dizer que não existam extensões úteis do Unity na Unity Asset Store. Existem muitos deles, e às vezes é até difícil decidir qual escolher. Mas para cada projeto, é importante manter a consistência, que pode ser destruída pelo uso imprudente de peças diferentes que não se encaixam bem.
Por outro lado, para funcionalidades que levariam muito tempo para serem implementadas, é sempre útil usar produtos bem testados da Unity Asset Store, o que pode economizar muito tempo de desenvolvimento. No entanto, escolha com cuidado, use os comprovados que não trarão muitos bugs incontroláveis e estranhos ao seu produto final. Avaliações de cinco estrelas são uma boa medida para começar.
Se a funcionalidade desejada não for difícil de implementar, basta adicioná-la às bibliotecas pessoais (ou da empresa) em constante crescimento, que podem ser usadas em todos os seus projetos mais tarde. Dessa forma, você está aprimorando seu conhecimento e seu conjunto de ferramentas ao mesmo tempo.
Erro comum do Unity nº 10: não ter necessidade de estender a funcionalidade básica do Unity
Às vezes, pode parecer que o ambiente do Unity Editor é suficiente para testes básicos de jogos e design de níveis, e estendê-lo é uma perda de tempo. Mas acredite, não é. O grande potencial de extensão do Unity vem de ser capaz de adaptá-lo a problemas específicos que precisam ser resolvidos em vários projetos. Isso pode melhorar a experiência do usuário ao trabalhar no Unity ou acelerar drasticamente todo o fluxo de trabalho de desenvolvimento e design de nível. Seria lamentável não usar recursos internos, como gavetas de propriedades internas ou personalizadas, gavetas de decoração, configurações personalizadas do inspetor de componentes ou até mesmo não construir plugins inteiros com suas próprias janelas de editor.
Conclusão
Espero que esses tópicos sejam úteis para você à medida que avança em seus projetos do Unity. Existem muitas coisas que são específicas do projeto, então elas não podem ser aplicadas, mas é sempre útil ter algumas regras básicas em mente ao tentar resolver problemas mais difíceis e específicos. Você pode ter opiniões ou procedimentos diferentes sobre como resolver esses problemas em seus projetos. O mais importante é manter seus idiomas consistentes em todo o projeto para que qualquer pessoa em sua equipe possa entender claramente como o domínio específico deveria ter sido resolvido corretamente.
Leitura adicional no Blog da Toptal Engineering:
- Desenvolvimento de IA do Unity: um tutorial de máquina de estado finito