Disegnare mondi di gioco isometrici


178

Qual è il modo corretto di disegnare tessere isometriche in un gioco 2D?

Ho letto riferimenti (come questo ) che suggeriscono che le tessere vengano renderizzate in modo da zigzagare ogni colonna nella rappresentazione di array 2D della mappa. Immagino che dovrebbero essere disegnati più a forma di diamante, dove ciò che viene disegnato sullo schermo si collega più da vicino a come sarebbe l'array 2D, appena ruotato un po '.

Ci sono vantaggi o svantaggi per entrambi i metodi?

Risposte:


505

Aggiornamento: algoritmo di rendering della mappa corretto, aggiunta di più illustrazioni, modifica della formattazione.

Forse il vantaggio della tecnica "a zig-zag" per mappare le piastrelle sullo schermo si può dire che le piastrelle xe le ycoordinate sono sugli assi verticale e orizzontale.

Approccio "Disegnare in un diamante":

Disegnando una mappa isometrica usando "disegno in un diamante", che credo si riferisca al semplice rendering della mappa usando un forciclo nidificato sull'array bidimensionale, come questo esempio:

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)
        )

Vantaggio:

Il vantaggio dell'approccio è che è un semplice nidificato for loop con una logica piuttosto semplice che funziona in modo coerente in tutte sezioni.

Svantaggio:

Un aspetto negativo di tale approccio è che le coordinate xe ydelle tessere sulla mappa aumenteranno in linee diagonali, il che potrebbe rendere più difficile mappare visivamente la posizione sullo schermo sulla mappa rappresentata come un array:

Immagine della mappa delle piastrelle

Tuttavia, ci sarà una trappola nell'implementazione del codice di esempio sopra - l'ordine di rendering farà sì che le tessere che si suppone siano dietro determinate tessere vengano disegnate sopra le tessere davanti:

Immagine risultante da un ordine di rendering errato

Per correggere questo problema, l' forordine del ciclo interno deve essere invertito, partendo dal valore più alto e rendendolo al valore più basso:

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)
        )

Con la correzione precedente, il rendering della mappa dovrebbe essere corretto:

Immagine risultante dal corretto ordine di rendering

Approccio "Zig-zag":

Vantaggio:

Forse il vantaggio dell'approccio "zig-zag" è che la mappa renderizzata può sembrare un po 'più compatta verticalmente dell'approccio "diamante":

L'approccio a zig-zag al rendering sembra compatto

Svantaggio:

Dal tentativo di implementare la tecnica a zig-zag, lo svantaggio potrebbe essere che è un po 'più difficile scrivere il codice di rendering perché non può essere scritto come un forciclo nidificato su ogni elemento in un array:

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
        )

Inoltre, potrebbe essere un po 'difficile provare a capire le coordinate di un riquadro a causa della natura sfalsata dell'ordine di rendering:

Coordinate su un rendering a zig-zag

Nota: le illustrazioni incluse in questa risposta sono state create con un'implementazione Java del codice di rendering del riquadro presentato, con il seguente intarray come mappa:

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

Le immagini delle piastrelle sono:

  • tileImage[0] -> Una scatola con una scatola dentro.
  • tileImage[1] -> Una scatola nera
  • tileImage[2] -> Una scatola bianca
  • tileImage[3] -> Una scatola con un alto oggetto grigio al suo interno.

Una nota su larghezze e altezze delle piastrelle

Le variabili tile_widthe tile_heightche sono utilizzate negli esempi di codice sopra riportati si riferiscono alla larghezza e all'altezza della piastrella di terra nell'immagine che rappresenta la piastrella:

Immagine che mostra la larghezza e l'altezza della piastrella

L'uso delle dimensioni dell'immagine funzionerà, purché le dimensioni dell'immagine e quelle della piastrella corrispondano. Altrimenti, la mappa delle tessere potrebbe essere resa con degli spazi tra le tessere.


136
hai anche disegnato delle foto. Questo è uno sforzo.
Zaratustra,

Saluti coobird. Sto usando l'approccio del diamante per il gioco attuale che sto sviluppando e sta funzionando a meraviglia. Grazie ancora.
Benny Hallett,

3
Che dire di più livelli di altezza? Posso disegnarli in maniera smilatoria, a partire dal livello più basso e continuare fino al livello più alto?
NagyI

2
@DomenicDatti: Grazie per le tue gentili parole :)
coobird

2
Questo e spettacolare. Ho appena usato il tuo approccio diamante e anche, nel caso ha bisogno di qualcuno per ottenere Coordinate reticolo dalla posizione dello schermo, ho fatto questo: j = (2 * x - 4 * y) / tilewidth * 0.5; i = (p.x * 2 / tilewidth) - j;.
Kyr Dunenkoff il

10

In entrambi i casi il lavoro viene svolto. Presumo che per zigzag intendi qualcosa del genere: (i numeri sono in ordine di rendering)

..  ..  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 per diamante intendi:

..  ..  ..  ..  ..
  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
..  ..  ..  ..  ..

Il primo metodo richiede il rendering di più riquadri in modo che venga disegnato l'intero schermo, ma è possibile effettuare facilmente un controllo dei limiti e saltare tutte le tessere completamente fuori dallo schermo. Entrambi i metodi richiederanno un certo numero di scricchiolii per scoprire qual è la posizione della piastrella 01. Alla fine, entrambi i metodi sono all'incirca uguali in termini di matematica richiesta per un certo livello di efficienza.


14
In realtà intendevo il contrario. La forma a diamante (che lascia i bordi della mappa lisci) e il metodo a zig-zag, lasciando i bordi appuntiti
Benny Hallett,

1

Se hai delle tessere che superano i limiti del tuo diamante, ti consiglio di disegnare in profondità:

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

1

La risposta di Coobird è quella corretta, completa. Tuttavia, ho combinato i suoi suggerimenti con quelli di un altro sito per creare codice che funzioni nella mia app (iOS / Objective-C), che volevo condividere con chiunque venga qui alla ricerca di qualcosa del genere. Per favore, se ti piace / vota questa risposta, fai lo stesso per gli originali; tutto ciò che ho fatto è stato "stare sulle spalle dei giganti".

Per quanto riguarda l'ordinamento, la mia tecnica è un algoritmo di pittore modificato: ogni oggetto ha (a) un'altitudine della base (chiamo "livello") e (b) una X / Y per la "base" o "piede" di l'immagine (esempi: la base dell'avatar è ai suoi piedi; la base dell'albero è alle sue radici; la base dell'aeroplano è l'immagine al centro, ecc.) Quindi seleziono dal più basso al livello più alto, quindi dal più basso (il più alto sullo schermo) alla base più alta- Y, quindi dal più basso (il più a sinistra) al più alto base-X. Questo rende le tessere come ci si aspetterebbe.

Codice per convertire lo schermo (punto) in riquadro (cella) e viceversa:

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;
}

1

Puoi usare la distanza euclidea dal punto più alto e più vicino allo spettatore, tranne che non è del tutto corretto. Si traduce in un ordinamento sferico. Puoi raddrizzarlo guardando da più lontano. Più lontano la curvatura si appiattisce. Quindi basta aggiungere dire 1000 a ciascuno dei componenti x, y e z per dare x ', y' e z '. L'ordinamento su x '* x' + y '* y' + z '* z'.


0

Il vero problema è quando devi disegnare delle tessere / folletti che si intersecano / si estendono su due o più tessere.

Dopo 2 (difficili) mesi di analisi personale del problema, ho finalmente trovato e implementato un "disegno di rendering corretto" per il mio nuovo gioco cocos2d-js. La soluzione consiste nella mappatura, per ogni piastrella (sensibile), che gli sprite sono "davanti, dietro, sopra e dietro". Una volta fatto puoi disegnarli seguendo una "logica ricorsiva".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.