Mundos de jogo isométricos de desenho

Qual é a maneira correta de desenhar telhas isométricas em um jogo 2D?

Eu li referências (como essa ) que sugerem que as peças sejam renderizadas de uma maneira que ziguezagueie cada coluna na representação da matriz 2D do mapa. Eu imagino que eles deveriam ser desenhados mais em forma de diamante, onde o que é atraído para a canvas se relaciona mais de perto com o que a matriz 2D seria, apenas girou um pouco.

Existem vantagens ou desvantagens em qualquer um dos methods?

Atualização: Corrigido algoritmo de renderização de mapas, adicionado mais ilustrações, formatação alterada.

Talvez a vantagem da técnica de “zig-zag” para mapear as peças na canvas possa ser dito que as coordenadas x e y peça estão nos eixos vertical e horizontal.

Abordagem “desenhando em um diamante”:

Desenhando um mapa isométrico usando “desenho em um losango”, que, acredito, se refere a apenas renderizar o mapa usando um aninhamento for -loop sobre o array bidimensional, como este exemplo:

 tile_map[][] = [[...],...] for (cellY = 0; cellY < tile_map.size; cellY++): for (cellX = 0; cellX < tile_map[cellY].size cellX++): draw( tile_map[cellX][cellY], screenX = (cellX * tile_width / 2) + (cellY * tile_width / 2) screenY = (cellY * tile_height / 2) - (cellX * tile_height / 2) ) 

Vantagem:

A vantagem da abordagem é que ela é um simples aninhamento for loop com lógica bastante direta que funciona de maneira consistente em todos os blocos.

Desvantagem:

Uma desvantagem dessa abordagem é que as coordenadas x e y das peças no mapa aumentam em linhas diagonais, o que pode dificultar o mapeamento visual da localização na canvas para o mapa representado como uma matriz:

Imagem do mapa de azulejos

No entanto, haverá uma armadilha para implementar o código de exemplo acima - a ordem de renderização fará com que os blocos que deveriam estar atrás de certos blocos sejam desenhados no topo dos blocos na frente:

Imagem resultante da ordem de renderização incorreta

Para corrigir este problema, a ordem interna for forro deve ser invertida - a partir do valor mais alto e renderizando para o valor mais baixo:

 tile_map[][] = [[...],...] for (i = 0; i < tile_map.size; i++): for (j = tile_map[i].size; j >= 0; j--): // Changed loop condition here. draw( tile_map[i][j], x = (j * tile_width / 2) + (i * tile_width / 2) y = (i * tile_height / 2) - (j * tile_height / 2) ) 

Com a correção acima, a renderização do mapa deve ser corrigida:

Imagem resultante da ordem de renderização correta

Abordagem "zig-zag":

Vantagem:

Talvez a vantagem da abordagem "zig-zag" é que o mapa renderizado pode parecer um pouco mais verticalmente compacto do que a abordagem "diamond":

Abordagem em zig-zag para renderização parece compacta

Desvantagem:

Ao tentar implementar a técnica de zig-zag, a desvantagem pode ser que seja um pouco mais difícil escrever o código de renderização porque ele não pode ser escrito tão simples quanto um nested for -loop sobre cada elemento em uma matriz:

 tile_map[][] = [[...],...] for (i = 0; i < tile_map.size; i++): if i is odd: offset_x = tile_width / 2 else: offset_x = 0 for (j = 0; j < tile_map[i].size; j++): draw( tile_map[i][j], x = (j * tile_width) + offset_x, y = i * tile_height / 2 ) 

Além disso, pode ser um pouco difícil tentar descobrir a coordenada de um bloco devido à natureza escalonada da ordem de renderização:

Coordenadas em uma renderização de ordem em zig-zag

Nota: As ilustrações incluídas nesta resposta foram criadas com uma implementação Java do código de renderização de mosaico apresentado, com a seguinte matriz int como o mapa:

 tileMap = new int[][] { {0, 1, 2, 3}, {3, 2, 1, 0}, {0, 0, 1, 1}, {2, 2, 3, 3} }; 

As imagens dos blocos são:

  • tileImage[0] -> Uma checkbox com uma checkbox dentro.
  • tileImage[1] -> Uma checkbox preta.
  • tileImage[2] -> Uma checkbox branca.
  • tileImage[3] -> Uma checkbox com um object cinza alto.

Uma nota sobre larguras e alturas de ladrilho

As variables tile_width e tile_height que são usadas nos exemplos de código acima referem-se à largura e altura do bloco à terra na imagem que representa o bloco:

Imagem mostrando a largura e altura do ladrilho

O uso das dimensões da imagem funcionará, desde que as dimensões da imagem e as dimensões do ladrilho sejam correspondentes. Caso contrário, o mapa de blocos poderia ser renderizado com intervalos entre as peças.

De qualquer maneira, o trabalho é feito. Eu suponho que por ziguezague você quer dizer algo assim: (números são ordem de renderização)

 .. .. 01 .. .. .. 06 02 .. .. 11 07 03 .. 16 12 08 04 21 17 13 09 05 22 18 14 10 .. 23 19 15 .. .. 24 20 .. .. .. 25 .. .. 

E por diamante você quer dizer:

 .. .. .. .. .. 01 02 03 04 .. 05 06 07 .. 08 09 10 11 .. 12 13 14 .. 15 16 17 18 .. 19 20 21 .. 22 23 24 25 .. .. .. .. .. 

O primeiro método precisa de mais peças renderizadas para que a canvas inteira seja desenhada, mas você pode facilmente fazer uma verificação de limite e pular todas as peças totalmente fora da canvas. Ambos os methods exigirão alguns cálculos numéricos para descobrir qual é a localização do bloco 01. No final, ambos os methods são aproximadamente iguais em termos de matemática requerida para um certo nível de eficiência.

Se você tiver algumas peças que excedam os limites do seu diamante, recomendo desenhar em ordem de profundidade:

 ...1... ..234.. .56789. ..abc.. ...d... 

A resposta de Coobird é a correta e completa. No entanto, eu combinei suas dicas com as de outro site para criar um código que funciona no meu aplicativo (iOS / Objective-C), que eu queria compartilhar com qualquer um que venha aqui procurando por algo assim. Por favor, se você gosta / vota para cima esta resposta, faça o mesmo para os originais; tudo que fiz foi “ficar de pé sobre os ombros dos gigantes”.

Quanto à ordem de sorting, minha técnica é um algoritmo de pintor modificado: cada object tem (a) uma altitude da base (eu chamo de “nível”) e (b) um X / Y para a “base” ou “pé” de a imagem (exemplos: a base do avatar está a seus pés; a base da tree está em suas raízes; a base do avião é a imagem central, etc.) Então eu apenas classifico mais baixo para o mais alto, depois mais baixo (mais alto na canvas) para o mais alto Y, então mais baixo (mais à esquerda) para a maior base-X. Isso processa os blocos da maneira que se esperaria.

Código para converter canvas (ponto) em bloco (célula) e verso:

 typedef struct ASIntCell { // like CGPoint, but with int-s vice float-s int x; int y; } ASIntCell; // Cell-math helper here: // http://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511 // Although we had to rotate the coordinates because... // X increases NE (not SE) // Y increases SE (not SW) + (ASIntCell) cellForPoint: (CGPoint) point { const float halfHeight = rfcRowHeight / 2.; ASIntCell cell; cell.x = ((point.x / rfcColWidth) - ((point.y - halfHeight) / rfcRowHeight)); cell.y = ((point.x / rfcColWidth) + ((point.y + halfHeight) / rfcRowHeight)); return cell; } // Cell-math helper here: // http://stackoverflow.com/questions/892811/drawing-isometric-game-worlds/893063 // X increases NE, // Y increases SE + (CGPoint) centerForCell: (ASIntCell) cell { CGPoint result; result.x = (cell.x * rfcColWidth / 2) + (cell.y * rfcColWidth / 2); result.y = (cell.y * rfcRowHeight / 2) - (cell.x * rfcRowHeight / 2); return result; } 

O problema real é quando você precisa desenhar alguns azulejos / sprites interceptando / abrangendo duas ou mais outras peças.

Após 2 meses de análise pessoal do problema, finalmente encontrei e implementei um “desenho de renderização correto” para o meu novo jogo cocos2d-js. A solução consiste em mapear, para cada bloco (suscetível), quais sprites são “front, back, top e behind”. Uma vez feito isso, você pode desenhá-los seguindo uma “lógica recursiva”.