Zeichnen isometrischer Spielwelten

178

Was ist der richtige Weg, um isometrische Kacheln in einem 2D-Spiel zu zeichnen?

Ich habe Referenzen (wie diese ) gelesen , die vorschlagen, dass die Kacheln so gerendert werden, dass jede Spalte in der 2D-Array-Darstellung der Karte im Zick-Zack angezeigt wird. Ich stelle mir vor, dass sie eher diamantiert gezeichnet werden sollten, wobei das, was auf den Bildschirm gezeichnet wird, enger mit dem Aussehen des 2D-Arrays zusammenhängt, nur ein wenig gedreht.

Gibt es Vor- oder Nachteile für beide Methoden?

Benny Hallett
quelle

Antworten:

505

Update: Karten-Rendering-Algorithmus korrigiert, weitere Abbildungen hinzugefügt, Formatierung geändert.

Vielleicht kann der Vorteil der "Zick-Zack" -Technik zum Zuordnen der Kacheln zum Bildschirm darin bestehen, dass sich die Kacheln xund yKoordinaten auf der vertikalen und horizontalen Achse befinden.

Ansatz "Zeichnen in einem Diamanten":

Durch Zeichnen einer isometrischen Karte mit "Zeichnen in einem Diamanten", was sich meines Erachtens darauf bezieht, die Karte nur mit einer verschachtelten forSchleife über dem zweidimensionalen Array zu rendern, wie in diesem Beispiel:

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

Vorteil:

Der Vorteil des Ansatzes besteht darin, dass es sich um eine einfache verschachtelte forSchleife mit ziemlich einfacher Logik handelt, die in allen Kacheln konsistent funktioniert.

Nachteil:

Ein Nachteil dieses Ansatzes besteht darin, dass die xund yKoordinaten der Kacheln auf der Karte in diagonalen Linien zunehmen, was es möglicherweise schwieriger macht, die Position auf dem Bildschirm visuell auf die als Array dargestellte Karte abzubilden:

Bild der Kachelkarte

Die Implementierung des obigen Beispielcodes wird jedoch eine Gefahr darstellen. Die Renderreihenfolge führt dazu, dass Kacheln, die sich hinter bestimmten Kacheln befinden sollen, über die Kacheln vor ihnen gezeichnet werden:

Resultierendes Bild aus falscher Renderreihenfolge

Um dieses Problem zu forbeheben, muss die Reihenfolge der inneren Schleife umgekehrt werden - beginnend mit dem höchsten Wert und gerendert in Richtung des niedrigeren Werts:

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

Mit dem obigen Fix sollte das Rendern der Karte korrigiert werden:

Resultierendes Bild aus korrekter Renderreihenfolge

"Zick-Zack" -Ansatz:

Vorteil:

Möglicherweise besteht der Vorteil des "Zick-Zack" -Ansatzes darin, dass die gerenderte Karte vertikal etwas kompakter erscheint als der "Diamant" -Ansatz:

Der Zick-Zack-Ansatz für das Rendern scheint kompakt zu sein

Nachteil:

Beim Versuch, die Zick-Zack-Technik zu implementieren, kann der Nachteil sein, dass es etwas schwieriger ist, den Rendering-Code zu schreiben, da er nicht so einfach wie eine verschachtelte forSchleife über jedes Element in einem Array geschrieben werden kann:

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
        )

Es kann auch etwas schwierig sein, die Koordinate einer Kachel zu ermitteln, da die Renderreihenfolge versetzt ist:

Koordinaten auf einem Zick-Zack-Rendering

Hinweis: Die in dieser Antwort enthaltenen Abbildungen wurden mit einer Java-Implementierung des dargestellten Kachel-Rendering-Codes mit dem folgenden intArray als Karte erstellt:

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

Die Kachelbilder sind:

  • tileImage[0] -> Eine Kiste mit einer Kiste darin.
  • tileImage[1] -> Eine Blackbox.
  • tileImage[2] -> Eine weiße Kiste.
  • tileImage[3] -> Eine Kiste mit einem hohen grauen Gegenstand.

Ein Hinweis zu Fliesenbreiten und -höhen

Die Variablen tile_widthund tile_heightdie in den obigen Code Beispielen verwendet werden , beziehen sich auf die Breite und die Höhe der Bodenfliese in dem Bild , die die Fliese:

Bild zeigt die Fliesenbreite und -höhe

Die Verwendung der Bildabmessungen funktioniert, solange die Bildabmessungen und die Kachelabmessungen übereinstimmen. Andernfalls könnte die Kachelkarte mit Lücken zwischen den Kacheln gerendert werden.

Coobird
quelle
136
Du hast sogar Bilder gezeichnet. Das ist Anstrengung.
Zaratustra
Prost Coobird. Ich verwende den Diamant-Ansatz für das aktuelle Spiel, das ich entwickle, und es ist ein Vergnügen. Danke noch einmal.
Benny Hallett
3
Was ist mit mehreren Höhenschichten? Kann ich sie smiliary zeichnen, beginnend mit der niedrigsten Stufe und weiter bis zur höchsten Stufe?
NagyI
2
@DomenicDatti: Vielen Dank für Ihre freundlichen Worte :)
Coobird
2
Das ist fantastisch. Ich habe gerade Ihren Diamant-Ansatz verwendet und für den Fall, dass jemand Gitterkoordinaten von der Bildschirmposition abrufen muss, habe ich Folgendes getan : j = (2 * x - 4 * y) / tilewidth * 0.5; i = (p.x * 2 / tilewidth) - j;.
Kyr Dunenkoff
10

In beiden Fällen wird die Arbeit erledigt. Ich gehe davon aus, dass Sie mit Zickzack so etwas meinen: (Zahlen sind in der Reihenfolge des Renderns)

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

Und mit Diamant meinst du:

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

Bei der ersten Methode müssen mehr Kacheln gerendert werden, damit der gesamte Bildschirm gezeichnet wird. Sie können jedoch problemlos eine Grenzüberprüfung durchführen und alle Kacheln vollständig außerhalb des Bildschirms überspringen. Beide Methoden erfordern eine gewisse Zahlenkalkulation, um herauszufinden, wo sich die Kachel 01 befindet. Letztendlich sind beide Methoden in Bezug auf die Mathematik, die für ein bestimmtes Effizienzniveau erforderlich ist, ungefähr gleich.

Zaratustra
quelle
14
Ich meinte eigentlich umgekehrt. Die Diamantform (die die Kanten der Karte glatt lässt) und die Zick-Zack-Methode, die die Kanten stachelig lässt
Benny Hallett
1

Wenn Sie einige Kacheln haben, die die Grenzen Ihres Diamanten überschreiten, empfehle ich, in Tiefenreihenfolge zu zeichnen:

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

Coobirds Antwort ist die richtige, vollständige. Ich habe seine Hinweise jedoch mit denen einer anderen Website kombiniert, um Code zu erstellen, der in meiner App (iOS / Objective-C) funktioniert und den ich mit allen teilen möchte, die hierher kommen und nach so etwas suchen. Bitte, wenn Sie diese Antwort mögen / abstimmen, machen Sie dasselbe für die Originale; Alles, was ich tat, war "auf den Schultern von Riesen zu stehen".

Was die Sortierreihenfolge betrifft, ist meine Technik ein modifizierter Malalgorithmus: Jedes Objekt hat (a) eine Höhe der Basis (ich nenne "Ebene") und (b) ein X / Y für die "Basis" oder "Fuß" von das Bild (Beispiele: Die Basis des Avatars liegt ihm zu Füßen; die Basis des Baumes befindet sich an den Wurzeln; die Basis des Flugzeugs befindet sich in der Mitte usw.) Dann sortiere ich einfach die niedrigste bis höchste Ebene, dann die niedrigste (höchste auf dem Bildschirm) bis zur höchsten Basis. Y, dann niedrigste (ganz links) bis höchste Basis-X. Dadurch werden die Kacheln so, wie man es erwarten würde.

Code zum Konvertieren von Bildschirm (Punkt) in Kachel (Zelle) und zurück:

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
quelle
1

Sie können den euklidischen Abstand vom höchsten und dem Betrachter am nächsten gelegenen Punkt verwenden, außer dass dies nicht ganz richtig ist. Dies führt zu einer sphärischen Sortierreihenfolge. Sie können dies korrigieren, indem Sie von weiter weg schauen. Weiter entfernt wird die Krümmung abgeflacht. Fügen Sie also einfach 1000 zu jeder der x-, y- und z-Komponenten hinzu, um x ', y' und z 'zu erhalten. Die Sortierung nach x '* x' + y '* y' + z '* z'.

Siobhan O'Connor
quelle
0

Ein echtes Problem ist, wenn Sie einige Kacheln / Sprites zeichnen müssen, die zwei oder mehr andere Kacheln schneiden / überspannen.

Nach 2 (harten) Monaten persönlicher Problemanalyse habe ich endlich eine "korrekte Renderzeichnung" für mein neues cocos2d-js-Spiel gefunden und implementiert. Die Lösung besteht darin, für jede Kachel (anfällig) die Sprites zuzuordnen, die "vorne, hinten, oben und hinten" sind. Sobald Sie dies getan haben, können Sie sie nach einer "rekursiven Logik" zeichnen.

Carlos Lopez
quelle