Desenhando mundos de jogo isométricos

178

Qual é a maneira correta de desenhar peças isométricas em um jogo 2D?

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

Existem vantagens ou desvantagens em qualquer um dos métodos?

Benny Hallett
fonte

Respostas:

505

Atualização: O algoritmo de renderização de mapa foi corrigido, adicionou mais ilustrações, mudou a formatação.

Talvez a vantagem da técnica "zig-zag" para mapear os ladrilhos para a tela possa ser dita de que os ladrilhos xe as ycoordenadas estão nos eixos vertical e horizontal.

Abordagem "Desenho em diamante":

Ao desenhar um mapa isométrico usando "desenho em um diamante", acredito que se refere apenas à renderização do mapa usando um forloop aninhado sobre a matriz 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 é um simples aninhado for loop com lógica bastante direta que funciona de forma consistente em todos os blocos.

Desvantagem:

Uma desvantagem dessa abordagem é que as coordenadas xe ydos blocos no mapa aumentarão em linhas diagonais, o que pode dificultar o mapeamento visual da localização na tela para o mapa representado como uma matriz:

Imagem do mapa de blocos

No entanto, haverá uma armadilha na implementação do 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 de ordem de renderização incorreta

Para corrigir esse problema, a forordem do loop interno deve ser revertida - iniciando no valor mais alto e renderizando no 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" seja que o mapa renderizado possa parecer um pouco mais verticalmente compacto do que a abordagem "diamante":

A abordagem em zigue-zague da renderização parece compacta

Desvantagem:

Ao tentar implementar a técnica de zig-zag, a desvantagem pode ser que é um pouco mais difícil escrever o código de renderização, porque não pode ser escrito tão simples quanto um forloop aninhado 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 pedido 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 bloco apresentado, com a seguinte intmatriz como o mapa:

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

As imagens de bloco são:

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

Uma observação sobre larguras e alturas de lado a lado

As variáveis tile_widthe tile_heightque são usadas nos exemplos de código acima se referem à largura e altura do bloco do solo na imagem que representa o bloco:

Imagem mostrando a largura e a altura do ladrilho

O uso das dimensões da imagem funcionará, desde que as dimensões da imagem e as dimensões do bloco correspondam. Caso contrário, o mapa de blocos poderá ser renderizado com intervalos entre os blocos.

coobird
fonte
136
você até desenhou fotos. Isso é esforço.
Zaratustra
Felicidades coobird. Estou usando a abordagem de diamante para o jogo atual que estou desenvolvendo e está dando certo. Obrigado novamente.
Benny Hallett
3
E quanto a várias camadas de altura? Posso desenhá-los smiliary, começando com o nível mais baixo e continuar até atingir o nível mais alto?
precisa saber é o seguinte
2
@DomenicDatti: Obrigado por suas amáveis palavras :)
coobird
2
Isso é incrível. Eu apenas usei sua abordagem diamante e também, em que alguém precisa de caso para obter coords grade de posição da tela, eu fiz isso: j = (2 * x - 4 * y) / tilewidth * 0.5; i = (p.x * 2 / tilewidth) - j;.
Kyr Dunenkoff
10

De qualquer maneira, o trabalho é feito. Suponho que em zigue-zague você queira dizer algo assim: (os números estão na 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 blocos renderizados para que a tela inteira seja desenhada, mas você pode facilmente fazer uma verificação de limite e pular todos os blocos totalmente fora da tela. Ambos os métodos exigirão uma trituração de número para descobrir qual é a localização do bloco 01. No final, ambos os métodos são aproximadamente iguais em termos de matemática necessária para um certo nível de eficiência.

zaratustra
fonte
14
Na verdade, eu quis dizer o contrário. A forma de diamante (que deixa as bordas do mapa liso) e o método de zig-zag, que saem das bordas spikey
Benny Hallett
1

Se você tiver alguns ladrilhos que excedem os limites do seu diamante, recomendo desenhar em ordem de profundidade:

...1...
..234..
.56789.
..abc..
...d...
Wouter Lievens
fonte
1

A resposta de Coobird é a correta e completa. No entanto, combinei suas dicas com as de outro site para criar um código que funcione no meu aplicativo (iOS / Objective-C), que eu gostaria de compartilhar com qualquer pessoa que vier aqui procurar algo assim. Por favor, se você gostar / faça o voto positivo desta resposta, faça o mesmo com os originais; tudo o que fiz foi "ficar nos ombros dos gigantes".

Quanto à ordem de classificação, minha técnica é o algoritmo de um pintor modificado: cada objeto tem (a) uma altitude da base (eu chamo de "nível") e (b) um X / Y para a "base" ou o "pé" de a imagem (exemplos: a base do avatar está a seus pés; a base da árvore está em suas raízes; a base do avião é a imagem central etc.) Depois, apenas classifico o nível mais baixo ao mais alto, depois o mais baixo (mais alto na tela) Y, então mais baixo (mais à esquerda) até mais alto base-X. Isso renderiza as peças da maneira que se esperaria.

Código para converter a tela (ponto) em bloco (célula) e vice-versa:

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;
}
Olie
fonte
1

Você pode usar a distância euclidiana do ponto mais alto e mais próximo do visualizador, exceto que isso não está certo. Isso resulta em uma ordem de classificação esférica. Você pode esclarecer isso olhando para mais longe. Mais longe, a curvatura fica achatada. Então, basta adicionar 1000 para cada um dos componentes x, ye z para fornecer x ', y' e z '. A classificação em x '* x' + y '* y' + z '* z'.

Siobhan O'Connor
fonte
0

O verdadeiro problema é quando você precisa desenhar alguns ladrilhos / sprites que cruzam / abrangem dois ou mais ladrilhos.

Após 2 meses (difíceis) 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 peça (suscetível), quais sprites estão "na frente, atrás, acima e atrás". Depois de fazer isso, você pode desenhá-los seguindo uma "lógica recursiva".

Carlos Lopez
fonte