
É uma história de como escrever um plugin para a Unity Asset Store , resolver um problema isométrico bem conhecido nos jogos e ganhar um pouco de dinheiro com isso, além de entender como o editor do Unity é expansível. Imagens, código, gráficos e pensamentos dentro.
Prólogo
Então, foi uma noite em que descobri que não havia praticamente nada a fazer. O próximo ano não foi realmente promissor na minha vida profissional (embora diferente da pessoal, mas essa é outra história). De qualquer forma, tive a ideia de escrever algo divertido para os velhos tempos, que seria bastante pessoal, algo por conta própria, mas ainda teria uma pequena vantagem comercial (eu gosto dessa sensação calorosa quando seu projeto é interessante para outra pessoa, exceto para o seu empregador). E tudo isso andou de mãos dadas com o fato de eu ter esperado muito tempo para verificar as possibilidades da extensão do editor do Unity e para ver se há alguma coisa boa em sua plataforma para vender as extensões do próprio mecanismo.
Dediquei um dia a estudar a Asset Store: modelos, scripts, integrações com vários serviços. E, primeiro, parecia que tudo já havia sido escrito e integrado, tendo até várias opções de diferentes níveis de qualidade e detalhe, tanto quanto preços e suporte. Então, imediatamente reduzi-o a:
- apenas código (afinal, sou programador)
- Apenas 2D (já que eu adoro 2D e eles fizeram um suporte decente e pronto para isso no Unity)
E então me lembrei de quantos cactos eu comi e quantos ratos morreram quando estávamos fazendo um jogo isométrico antes. Você não vai acreditar quanto tempo perdemos na busca de soluções viáveis e quantas cópias quebramos na tentativa de resolver essa isometria e desenhá-la. Então, lutando para manter minhas mãos paradas, procurei por palavras-chave diferentes e não muito-chave e não consegui encontrar nada além de uma enorme pilha de arte isométrica, até que finalmente decidi fazer um plugin isométrico do zero.
Estabelecendo as metas
A primeira que eu preciso foi descrever resumidamente quais problemas esse plugin deveria resolver e que uso o desenvolvedor de jogos isométricos faria. Portanto, os problemas de isometria são os seguintes:
- classificação de objetos por distância, a fim de desenhá-los adequadamente
- extensão para criação, posicionamento e deslocamento de objetos isométricos no editor
Assim, com os principais objetivos da primeira versão formulada, estabeleci um prazo de 2-3 dias para a primeira versão preliminar. Portanto, não pode ser adiado, como o entusiasmo é uma coisa frágil e, se você não tem algo pronto nos primeiros dias, há uma grande chance de estragá-lo. E as férias de Ano Novo não são tão longas quanto parecem, mesmo na Rússia, e eu queria lançar a primeira versão em dez dias.
Classificação
Para resumir, a isometria é uma tentativa feita por sprites 2D de se parecer com modelos 3D. Isso, é claro, resulta em dezenas de problemas. O principal é que os sprites devem ser classificados na ordem em que foram desenhados para evitar problemas com sobreposição mútua.

Na captura de tela, você pode ver como é o sprite verde que é desenhado primeiro (2,1) e depois o azul (1,1)

A captura de tela mostra a classificação incorreta quando o sprite azul é desenhado primeiro
Nesse caso simples, a classificação não será um problema e haverá opções, por exemplo:
- ordenando pela posição de Y na tela, que é * (isoX + isoY) 0,5 + isoZ **
- desenho da célula de grade isométrica mais remota da esquerda para a direita, de cima para baixo [(3,3), (2,3), (3,2), (1,3), (2,2), (3, 1), ...]
- e um monte de outras maneiras interessantes e não muito interessantes
Todos eles são muito bons, rápidos e funcionais, mas apenas no caso de objetos ou colunas unicelulares estendidos na direção isoZ :) Afinal, eu estava interessado em uma solução mais comum que funcionaria para os objetos estendidos na direção de uma coordenada, ou mesmo as "cercas" que não têm absolutamente nenhuma largura, mas são estendidas na mesma direção que a altura necessária.

A captura de tela mostra a maneira correta de classificar objetos estendidos 3x1 e 1x3 com "cercas" medindo 3x0 e 0x3
E é aí que nossos problemas começam e nos colocam no lugar onde precisamos decidir o caminho a seguir:
divida os objetos "multicelulares" em objetos "unicelulares", ou seja, para cortá-lo verticalmente e, em seguida, classificar as listras surgidas
pense no novo método de classificação, mais complicado e interessante
Eu escolhi a segunda opção, não tendo nenhum desejo particular de entrar no processamento complicado de cada objeto, no corte (mesmo automático) e na abordagem especial da lógica. Para o registro, eles usaram o primeiro caminho em alguns jogos famosos como Fallout 1 e Fallout 2 . Você pode realmente ver essas tiras se entrar nos dados dos jogos.
Portanto, a segunda opção não implica nenhum critério de classificação. Isso significa que não existe um valor pré-calculado pelo qual você possa classificar objetos. Se você não acredita em mim (e acho que muitas pessoas que nunca trabalharam com isometria não), pegue um pedaço de papel e desenhe pequenos objetos medindo 2x8 e, por exemplo, 2x2 . Se você, de alguma forma, conseguir descobrir um valor para o cálculo de sua profundidade e classificação - basta adicionar um objeto 8x2 e tentar classificá-los em posições diferentes entre si.
Portanto, não existe esse valor, mas ainda podemos usar dependências entre eles (grosso modo, qual deles se sobrepõe) para classificação topológica . Podemos calcular as dependências dos objetos usando projeções de coordenadas isométricas no eixo isométrico.

A captura de tela mostra o cubo azul que depende do vermelho

A captura de tela mostra o cubo verde com dependência do azul
Um pseudocódigo para determinação de dependência para dois eixos (o mesmo funciona com o eixo Z):
bool IsIsoObjectsDepends(IsoObject obj_a, IsoObject obj_b) { var obj_a_max_size = obj_a.position + obj_a.size; return obj_b.position.x < obj_a_max_size.x && obj_b.position.y < obj_a_max_size.y; }
Com essa abordagem, construímos dependências entre todos os objetos, passando entre eles de forma recursiva e marcando a coordenada Z da exibição. O método é bastante universal e, mais importante, funciona. Você pode ler a descrição detalhada desse algoritmo, por exemplo, aqui ou aqui . Eles também usam esse tipo de abordagem na popular biblioteca isométrica flash ( as3isolib ).
E tudo foi ótimo, exceto que a complexidade do tempo dessa abordagem é O (N ^ 2), pois precisamos comparar todos os objetos com os outros para criar as dependências. Eu deixei a otimização para versões posteriores, depois de adicionar apenas uma prorrogação lenta, para que nada fosse classificado até que algo se movesse. Então, falaremos sobre otimização um pouco mais tarde.
Extensão do editor
A partir de agora, eu tinha os seguintes objetivos:
- a classificação dos objetos teve que funcionar no editor (não apenas em um jogo)
- tinha que haver outro tipo de Gizmos-Arrow (setas para mover objetos)
- opcionalmente, haveria um alinhamento com blocos quando o objeto é movido
- os tamanhos dos ladrilhos seriam aplicados e definidos automaticamente no inspetor isométrico do mundo
- Objetos AABB são desenhados de acordo com seus tamanhos isométricos
- saída de coordenadas isométricas no inspetor de objetos, alterando a posição em que mudaríamos a posição do objeto no mundo do jogo
E todos esses objetivos foram alcançados. O Unity realmente permite expandir seu editor significativamente. Você pode adicionar novas guias, janelas, botões, novos campos no inspetor de objetos. Se desejar, você pode até criar um inspetor personalizado para um componente do tipo exato de que precisa. Você também pode enviar informações adicionais na janela do editor (no meu caso, nos objetos AABB) e substituir os aparelhos de movimentação padrão de objetos também. O problema de classificar dentro do editor foi resolvido por meio dessa tag mágica ExecuteInEditMode , que permite executar componentes do objeto no modo editor, ou seja, da mesma maneira que em um jogo.
Tudo isso foi feito, é claro, não sem dificuldades e truques de todos os tipos, mas não havia nenhum problema em que eu passasse mais de duas horas (o Google, fóruns e comunidades certamente me ajudaram a resolver todos os problemas surgidos que não foram mencionados na documentação).

A captura de tela mostra meus aparelhos para objetos de movimento no mundo isométrico
Lançamento
Então, eu preparei a primeira versão, tirei a captura de tela. Eu até desenhei um ícone e escrevi uma descrição. Está na hora. Então, defino um preço nominal de US $ 5, carrego o plug-in na loja e espero que seja aprovado pelo Unity. Eu não pensei muito sobre o preço, já que realmente não queria ganhar muito dinheiro ainda. Meu objetivo era descobrir se existe uma demanda geral e, se houver, gostaria de estimar. Também queria ajudar os desenvolvedores de jogos isométricos que de alguma forma acabavam absolutamente privados de oportunidades e acréscimos.
Em cinco dias bastante dolorosos (passei quase o mesmo tempo escrevendo a primeira versão, mas sabia o que estava fazendo, sem mais pensar e pensar demais, o que me proporcionou maior velocidade em comparação com as pessoas que começaram a trabalhar com isometria) Eu recebi uma resposta da Unity dizendo que o plug-in foi aprovado e eu já podia vê-lo na loja, assim como suas zero (até agora) vendas. Ele fez check-in no fórum local, incorporou o Google Analytics na página do plug-in na loja e me preparou para esperar a grama crescer.
Não demorou muito tempo antes das primeiras vendas, assim como surgiram feedbacks no fórum e na loja. Nos dias restantes de 12 de janeiro, cópias do meu plugin foram vendidas, o que eu considerei como um sinal do interesse do público e decidi continuar.
Otimização
Então, eu estava descontente com duas coisas:
- Complexidade temporal da classificação - O (N ^ 2)
- Problemas com coleta de lixo e desempenho geral
Algoritmo
Tendo 100 objetos e O (N ^ 2), eu tinha 10.000 iterações para fazer apenas para encontrar dependências, e também teria que passar por todos eles e marcar o display Z para classificar. Deveria haver alguma solução para isso. Então, eu tentei um grande número de opções, não conseguia dormir pensando sobre este problema. De qualquer forma, não vou falar sobre todos os métodos que tentei, mas descreverei o que achei melhor até agora.
Primeiro, é claro, classificamos apenas objetos visíveis. O que isso significa é que precisamos constantemente saber o que está em nosso caminho. Se houver algum objeto novo, precisamos adicioná-lo no processo de classificação e, se um dos antigos se for - ignorá-lo. Agora, o Unity não permite determinar a Caixa delimitadora do objeto junto com seus filhos na árvore da cena. Passar por cima das crianças (sempre, a propósito, já que elas podem ser adicionadas e removidas) não funcionaria - muito devagar. Também não podemos usar o OnBecameVisible e outros eventos, porque eles funcionam apenas para objetos pai. Mas podemos obter todos os componentes do Renderer a partir do objeto necessário e de seus filhos. Claro, isso não parece ser a nossa melhor opção, mas não consegui encontrar outra maneira, a mesma universal e aceitável pelo desempenho.
List<Renderer> _tmpRenderers = new List<Renderer>(); bool IsIsoObjectVisible(IsoObject iso_object) { iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); for ( var i = 0; i < _tmpRenderers.Count; ++i ) { if ( _tmpRenderers[i].isVisible ) { return true; } } return false; }
Há um pequeno truque ao usar a função GetComponentsInChildren que permite obter componentes sem alocações no buffer necessário, diferente de outro que retorna uma nova matriz de componentes
Em segundo lugar, eu ainda tinha que fazer algo sobre O (N ^ 2) . Eu tentei várias técnicas de divisão de espaço antes de parar em uma grade bidimensional simples no espaço de exibição onde projeto meus objetos isométricos. Cada setor contém uma lista de objetos isométricos que o atravessam. Portanto, a ideia é simples: se as projeções dos objetos não são cruzadas, não faz sentido construir dependências entre os objetos. Em seguida, repassamos todos os objetos visíveis e construímos dependências apenas nos setores em que é necessário, diminuindo a complexidade de tempo do algoritmo e aumentando o desempenho. Calculamos o tamanho de cada setor como uma média entre os tamanhos de todos os objetos. Achei o resultado mais do que satisfatório.
Claro, eu poderia escrever um artigo separado sobre isso ... Ok, vamos tentar resumir isso. Primeiro, estamos descontando os componentes (usamos o GetComponent para encontrá-los, o que não é rápido). Eu recomendo a todos que se cuidem ao trabalhar com qualquer coisa relacionada ao Update . Você sempre deve ter em mente que isso acontece em todos os quadros, portanto, você deve ter muito cuidado. Há muitas coisas a serem lembradas, mas, no final, você conhece todas elas no criador de perfil incorporado. Torna muito mais fácil memorizar e usá-los :)
Além disso, você realmente entende a dor do coletor de lixo. Precisa de desempenho superior? Em seguida, esqueça qualquer coisa que possa alocar memória, que em C # (especialmente no antigo compilador Mono ) pode ser feita por qualquer coisa, variando de foreach (!) A lambdas emergentes, sem falar no LINQ que agora é proibido para você, mesmo nos casos mais simples. No final, em vez de C # com seu açúcar sintático, você obtém uma aparência de C com capacidades ridículas.
Aqui vou dar alguns links sobre o tópico que você pode achar útil:
Parte1 , Parte2 , Parte3 .
Resultados
Eu nunca conheci alguém usando essa técnica de otimização antes, então fiquei particularmente feliz em ver os resultados. E se nas primeiras versões foram necessários literalmente 50 objetos em movimento para o jogo transformá-lo em uma apresentação de slides, agora funciona muito bem, mesmo quando há 800 objetos em um quadro: tudo está girando na velocidade máxima e reorganizando por apenas 3-6 ms, o que é muito bom para esse número de objetos em isometria. Além disso, após a inicialização, quase não alocamos memória para um quadro :)
Outras oportunidades
Depois de ler feedbacks e sugestões, alguns recursos foram adicionados nas versões anteriores.
Mistura 2D / 3D
A mistura de 2D e 3D em jogos isométricos é uma oportunidade interessante, que permite minimizar o desenho de diferentes opções de movimento e rotações (por exemplo, modelos 3D de personagens animados). Não é algo realmente difícil de fazer, mas requer integração dentro do sistema de classificação. Tudo o que você precisa é obter uma caixa delimitadora do modelo com todos os seus filhos e, em seguida, mover o modelo ao longo da exibição Z pela largura da caixa.
Bounds IsoObject3DBounds(IsoObject iso_object) { var bounds = new Bounds(); iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); if ( _tmpRenderers.Count > 0 ) { bounds = _tmpRenderers[0].bounds; for ( var i = 1; i < _tmpRenderers.Count; ++i ) { bounds.Encapsulate(_tmpRenderers[i].bounds); } } return bounds; }
esse é um exemplo de como você pode obter o Bounding Box do modelo com todos os seus filhos

e é assim que parece quando é feito
Configurações isométricas personalizadas
Isso é relativamente simples. Pediram-me para possibilitar a definição do ângulo isométrico, proporção, altura do ladrilho. Depois de sofrer alguma dor envolvida em matemática, você obtém algo assim:

Física
E aqui fica mais interessante. Como a isometria simula o mundo 3D, a física também deve ser tridimensional, com altura e tudo mais. Eu vim com esse truque fascinante. Eu replico todos os componentes da física, como Rigidbody , Collider e assim por diante, para o mundo isométrico. De acordo com essas descrições e configurações, eu faço a cópia do mundo tridimensional físico invisível usando o próprio mecanismo e o PhysX embutido. Depois disso, pego os dados de simulação calculados e consigo esses dados duplicados para o mundo isométrico. Então faço o mesmo para simular eventos de colisão e acionamento.

O GIF de demonstração física do conjunto de ferramentas
Epílogo e conclusões
Depois de implementar todas as sugestões do fórum, decidi aumentar o preço para 40 dólares, para que não parecesse mais um plugin barato com cinco linhas de código :) Ficarei muito satisfeito em responder a perguntas e ouça seus conselhos. Como é a primeira vez que escrevo algo sobre Habr, agradeço todos os tipos de críticas, obrigado! E agora, algo que eu estava guardando por último, as estatísticas de vendas do mês:
Mês | 5 $ | 40 $ |
---|
Janeiro | 12 | 0 0 |
Fevereiro | 22 | 0 0 |
Março | 17 | 0 0 |
Abril | 9 | 0 0 |
Maio | 9 | 0 0 |
Junho | 9 | 0 0 |
Julho | 7 | 4 |
Agosto | 0 0 | 4 |
Setembro | 0 0 | 5 |
Link da página da Unity Asset Store: Isometric 2.5D Toolset