O que você está procurando?
Engine & platform

Hacks de script do Advanced Editor para economizar seu tempo, parte 2

JORDI CABALLOL / UNITYSenior Software Engineer
Nov 8, 2022|15 Min
Hacks de script do Advanced Editor para economizar seu tempo, parte 2
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

Estou de volta para a segunda parte! Se você perdeu a primeira parte dos meus hacks avançados de script do Editor, confira aqui. Este artigo em duas partes foi elaborado para orientá-lo nas dicas avançadas do Editor para melhorar os fluxos de trabalho, de modo que seu próximo projeto seja mais tranquilo do que o anterior.

Cada hack é baseado em um protótipo demonstrativo que criei, semelhante a um RTS, em que as unidades de uma equipe atacam automaticamente os edifícios inimigos e outras unidades. Para relembrar, aqui está o protótipo de construção inicial:

No artigo anterior, compartilhei as práticas recomendadas sobre como importar e configurar os ativos de arte no projeto. Agora vamos começar a usar esses ativos no jogo, economizando o máximo de tempo possível.

Vamos começar desvendando os elementos do jogo. Ao configurar os elementos de um jogo, frequentemente nos deparamos com o seguinte cenário:

Por um lado, temos os Prefabs que vêm da equipe de arte - seja um Prefab gerado pelo FBX Importer ou um Prefab que foi cuidadosamente configurado com todos os materiais e animações apropriados, adicionando adereços à hierarquia, etc. Para usar esse Prefab no jogo, faz sentido criar uma Prefab Variant a partir dele e adicionar todos os componentes relacionados à jogabilidade. Dessa forma, a equipe de arte pode modificar e atualizar o Prefab, e todas as alterações são refletidas imediatamente no jogo. Embora essa abordagem funcione se o item exigir apenas alguns componentes com configurações simples, ela pode dar muito trabalho se você precisar configurar algo complexo do zero todas as vezes.

Por outro lado, muitos dos itens terão os mesmos componentes com valores semelhantes, como todos os Prefabs de carros ou Prefabs de inimigos semelhantes. Faz sentido que sejam todas variantes do mesmo Prefab básico. Dito isso, essa abordagem é ideal se a configuração da arte do Prefab for simples (ou seja, definir a malha e seus materiais).

A seguir, vamos ver como simplificar a configuração dos componentes de jogabilidade, para que possamos adicioná-los rapidamente aos nossos Prefabs de arte e usá-los diretamente no jogo.

Hack 7: Configure os componentes desde o início

A configuração mais comum que já vi para elementos complexos em um jogo é ter um componente "principal" (como "inimigo", "pickup" ou "porta") que se comporta como uma interface para se comunicar com o objeto e uma série de componentes pequenos e reutilizáveis que implementam a funcionalidade em si; coisas como "selectable", "CharacterMovement" ou "UnitHealth" e componentes incorporados do Unity, como renderizadores e colliders.

Alguns dos componentes dependem de outros componentes para funcionar. Por exemplo, o movimento do personagem pode precisar de um agente NavMesh. É por isso que o Unity tem o atributo RequireComponent pronto para definir todas essas dependências. Portanto, se houver um componente "principal" para um determinado tipo de objeto, você poderá usar o atributo RequireComponent para adicionar todos os componentes que esse tipo de objeto precisa ter.

Por exemplo, as unidades em meu protótipo têm os seguintes atributos:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Além de definir um local fácil de encontrar no AddComponentMenu, inclua todos os componentes extras de que ele precisa. Nesse caso, adicionei o Locomotion para me movimentar e o AttackComponent para atacar outras unidades.

Além disso, a classe base unit (que é compartilhada com os edifícios) tem outros atributos RequireComponent que são herdados por essa classe, como o componente Health. Com isso, só preciso adicionar o componente Soldier a um GameObject para que todos os outros componentes sejam adicionados automaticamente. Se eu adicionar um novo atributo RequireComponent a um componente, o Unity atualizará todos os GameObjects existentes com o novo componente, o que facilita a extensão dos objetos existentes.

O RequireComponent também tem um benefício mais sutil: Se tivermos o "componente A" que requer o "componente B", adicionar A a um GameObject não apenas garante que B também seja adicionado, mas também que B seja adicionado antes de A. Isso significa que, quando o método Reset for chamado para o componente A, o componente B já existirá e teremos acesso a ele imediatamente. Isso nos permite definir referências aos componentes, registrar UnityEvents persistentes e qualquer outra coisa que precisarmos fazer para configurar o objeto. Ao combinar o atributo RequireComponent e o método Reset, podemos configurar totalmente o objeto adicionando um único componente.

Hack 8: Compartilhar dados em Prefabs não relacionados

A principal desvantagem do método mostrado acima é que, se decidirmos alterar um valor, precisaremos alterá-lo manualmente para cada objeto. E se toda a configuração for feita por meio de código, será difícil para os designers modificá-la.

No artigo anterior, vimos como usar o AssetPostprocessor para adicionar dependências e modificar objetos no momento da importação. Agora vamos usar isso para impor alguns valores em nossos Prefabs.

Para facilitar a modificação desses valores pelos projetistas, leremos os valores de um Prefab. Isso permite que os projetistas modifiquem facilmente esse Prefab para alterar os valores de todo o projeto.

Se estiver escrevendo código do Editor, poderá copiar os valores de um componente em um objeto para outro, aproveitando a classe Preset.

Crie uma predefinição a partir do componente original e aplique-a ao(s) outro(s) componente(s) desta forma:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Da forma como está, ele substituirá todos os valores no Prefab, mas isso provavelmente não é o que queremos que ele faça. Em vez disso, copie apenas alguns valores, mantendo o restante intacto. Para fazer isso, use outra substituição de Preset.ApplyTo que receba uma lista das propriedades que devem ser aplicadas. É claro que poderíamos facilmente criar uma lista codificada das propriedades que queremos substituir, o que funcionaria bem para a maioria dos projetos, mas vamos ver como tornar isso completamente genérico.

Basicamente, criei um Prefab básico com todos os componentes e, em seguida, criei uma Variant para usar como modelo. Em seguida, decidi quais valores aplicar da lista de substituições na Variant.

Para obter as substituições, use PrefabUtility.GetPropertyModifications. Isso fornece a você todas as substituições em todo o Prefab, portanto, filtre apenas as necessárias para direcionar esse componente. É preciso ter em mente que o alvo da modificação é o componente do Prefab básico - não o componente da Variant -, portanto, precisamos obter a referência a ele usando GetCorrespondingObjectFromSource:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Agora, isso aplicará todas as substituições do modelo aos nossos Prefabs. O único detalhe que resta é que o modelo pode ser uma variante de uma variante, e queremos aplicar as substituições dessa variante também.

Para isso, precisamos apenas torná-lo recursivo:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Em seguida, vamos encontrar o modelo para nossos Prefabs. Idealmente, queremos usar modelos diferentes para tipos diferentes de objetos. Uma maneira eficiente de fazer isso é colocar os modelos na mesma pasta dos objetos aos quais queremos aplicá-los.

Procure um objeto chamado Template.prefab na mesma pasta do nosso Prefab. Se não conseguirmos encontrá-la, procuraremos na pasta principal de forma recursiva:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Nesse ponto, podemos modificar o modelo Prefab, e todas as alterações serão refletidas nos Prefabs dessa pasta, mesmo que eles não sejam variantes do modelo. Neste exemplo, alterei a cor padrão do jogador (a cor usada quando a unidade não está ligada a nenhum jogador). Observe como ele atualiza todos os objetos:

Hack 9: Equilibre os dados do jogo com ScriptableObjects e planilhas

Ao equilibrar os jogos, todas as estatísticas que você precisa ajustar são distribuídas em vários componentes, armazenados em um Prefab ou ScriptableObject para cada personagem. Isso torna o processo de ajuste de detalhes bastante lento.

Uma maneira comum de facilitar o balanceamento é usar planilhas eletrônicas. Elas podem ser muito úteis, pois reúnem todos os dados, e você pode usar fórmulas para calcular automaticamente alguns dos dados adicionais. Mas inserir esses dados manualmente no Unity pode ser muito demorado.

É aí que entram as planilhas. Eles podem ser exportados para formatos simples, como CSV(.csv) ou TSV(.tsv), que é exatamente a finalidade do ScriptedImporters. Abaixo está uma captura de tela das estatísticas das unidades no protótipo:

Exemplo de uma planilha | Tech from the Trenches

O código para isso é bastante simples: Crie um ScriptableObject com todas as estatísticas de uma unidade e, em seguida, você poderá ler o arquivo. Para cada linha da tabela, crie uma instância do ScriptableObject e preencha-o com os dados dessa linha.

Por fim, adicione todos os ScriptableObjects ao ativo importado usando o contexto. Também precisamos adicionar um ativo principal, que acabei de definir como um TextAsset vazio (já que não usamos o ativo principal para nada aqui).

Isso funciona tanto para edifícios quanto para unidades, mas você deve verificar qual deles está importando, pois as unidades terão muito mais estatísticas.

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Com isso concluído, agora há alguns ScriptableObjects que contêm todos os dados da planilha.

Dados importados da planilha

Os ScriptableObjects gerados estão prontos para serem usados no jogo conforme necessário. Você também pode usar o PrefabPostprocessor que foi configurado anteriormente.

No método OnPostprocessPrefab, temos a capacidade de carregar esse ativo e usar seus dados para preencher automaticamente os parâmetros dos componentes. Além disso, se você definir uma dependência para esse ativo de dados, os Prefabs serão reimportados sempre que você modificar os dados, mantendo tudo atualizado automaticamente.

Hack 10: Acelerar a iteração no Editor

Ao tentar criar níveis incríveis, é fundamental poder mudar e testar as coisas rapidamente, fazendo pequenos ajustes e tentando novamente. É por isso que os tempos de iteração rápidos e a redução das etapas necessárias para iniciar os testes são tão importantes.

Uma das primeiras coisas em que pensamos quando se trata de tempos de iteração no Unity é o Domain Reload. O Domain Reload é relevante em duas situações importantes: após a compilação do código para carregar as novas bibliotecas vinculadas dinamicamente (DLLs) e ao entrar e sair do modo Play. O recarregamento de domínio que vem com a compilação não pode ser evitado, mas você tem a opção de desativar os recarregamentos relacionados ao modo Play em Project Settings > Editor > Enter Play Mode Settings.

Desativar o Domain Reload ao entrar no modo Play pode causar alguns problemas se o seu código não estiver preparado para isso, sendo que o problema mais comum é que as variáveis estáticas não são redefinidas após a reprodução. Se o seu código puder funcionar com essa desativação, vá em frente. Para esse protótipo, o Domain Reload está desativado, portanto, você pode entrar no modo de jogo quase instantaneamente.

Hack 11: Geração automática de dados

Um outro problema com os tempos de iteração tem a ver com o recálculo dos dados necessários para jogar. Isso geralmente envolve a seleção de alguns componentes e o clique em botões para acionar os recálculos. Por exemplo, neste protótipo, há um TeamController para cada equipe na cena. Esse controlador tem uma lista de todos os edifícios inimigos para que possa enviar as unidades para atacá-los. Para preencher esses dados automaticamente, use a função IProcessSceneWithReport interface. Essa interface é chamada para as cenas em duas ocasiões diferentes: durante as compilações e ao carregar uma cena no modo Play. Com ele, você tem a oportunidade de criar, destruir e modificar qualquer objeto que desejar. Observe, no entanto, que essas alterações afetarão apenas o Builds e o Play Mode.

É nessa chamada de retorno que os controladores são criados e a lista de edifícios é definida. Graças a isso, não há necessidade de fazer nada manualmente. Os controladores com uma lista atualizada de edifícios estarão lá quando o jogo começar, e a lista será atualizada com as alterações que fizemos.

Para o protótipo, foi configurado um método utilitário que permite obter todas as instâncias de um componente em uma cena. Você pode usar isso para obter todos os edifícios:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

O restante do processo é um tanto trivial: Obtenha todos os edifícios, obtenha todas as equipes às quais os edifícios pertencem e crie um controlador para cada equipe com uma lista de edifícios inimigos.

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Hack 12: Trabalhar em várias cenas

Além da cena que está sendo editada, você também precisa carregar outras cenas para jogar (ou seja, uma cena com os gerentes, com a interface do usuário, etc.). Isso pode tomar um tempo valioso. No caso do protótipo, a tela com as barras de saúde está em uma cena diferente chamada InGameUI.

Uma maneira eficaz de trabalhar com isso é adicionar um componente à cena com uma lista das cenas que precisam ser carregadas junto com ela. Se você carregar essas cenas de forma síncrona no método Awake, a cena será carregada e todos os seus métodos Awake serão invocados nesse ponto. Portanto, no momento em que o método Start for chamado, você poderá ter certeza de que todas as cenas estão carregadas e inicializadas, o que lhe dá acesso aos dados nelas contidos, como os singletons do gerenciador.

Lembre-se de que algumas das cenas podem estar abertas quando você entra no modo Play, portanto, é importante verificar se a cena já está carregada antes de carregá-la:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Concluindo

Ao longo das partes um e dois deste artigo, mostrei a você como aproveitar alguns dos recursos menos conhecidos que o Unity tem a oferecer. Tudo o que foi descrito é apenas uma fração do que pode ser feito, mas espero que você ache esses hacks úteis para seu próximo projeto ou, no mínimo, interessantes.

Os ativos usados para criar o protótipo podem ser encontrados gratuitamente na Asset Store:

Se quiser discutir essa história de duas partes ou compartilhar suas ideias depois de lê-la, vá para o nosso fórum sobre scripts. Vou me despedir por enquanto, mas você ainda pode se conectar comigo no Twitter em @CaballolD. Fique atento aos futuros blogs técnicos de outros desenvolvedores do Unity como parte dasérieTech from the Trenches.