Rysowanie izometrycznych światów gry

178

Jak prawidłowo rysować płytki izometryczne w grze 2D?

Przeczytałem odniesienia (takie jak ten ), które sugerują, że kafelki są renderowane w sposób, który zygzakuje każdą kolumnę w reprezentacji tablicy w 2D. Wyobrażam sobie, że powinny być narysowane bardziej w sposób diamentowy, a to, co zostanie narysowane na ekranie, jest bliżej związane z tym, jak wyglądałaby matryca 2D, tylko nieco obrócona.

Czy są jakieś zalety lub wady obu metod?

Benny Hallett
źródło

Odpowiedzi:

505

Aktualizacja: poprawiony algorytm renderowania mapy, dodano więcej ilustracji, zmieniono formatowanie.

Być może zaletą techniki „zygzakowatości” mapowania kafelków na ekran można powiedzieć, że kafelki xi ywspółrzędne są na osi pionowej i poziomej.

Podejście „ciągnąc za diament”:

Rysując mapę izometryczną za pomocą „rysowania w diamentie”, co moim zdaniem odnosi się do renderowania mapy za pomocą zagnieżdżonej forpętli nad dwuwymiarową tablicą, na przykład w tym przykładzie:

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

Korzyść:

Zaletą tego podejścia jest to, że jest to prosta zagnieżdżona forpętla z dość prostą logiką, która działa konsekwentnie na wszystkich płytkach.

Niekorzyść:

Jednym minusem tej metody jest to, że xi ywspółrzędne płytki na mapie wzrośnie w ukośne linie, które mogłyby utrudnić wizualnie map lokalizację na ekranie mapie reprezentowane jako tablica:

Obraz mapy kafelków

Jednak zaimplementowanie powyższego przykładowego kodu będzie pułapką - kolejność renderowania spowoduje, że płytki, które powinny znajdować się za niektórymi płytkami, zostaną narysowane na wierzchu płytek z przodu:

Obraz wynikający z nieprawidłowej kolejności renderowania

Aby rozwiązać ten problem, forkolejność pętli wewnętrznej musi zostać odwrócona - zaczynając od najwyższej wartości i renderując w kierunku niższej wartości:

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

Przy powyższej poprawce renderowanie mapy powinno zostać poprawione:

Wynikowy obraz z prawidłowej kolejności renderowania

Podejście „zygzakowate”:

Korzyść:

Być może zaletą podejścia „zygzakowatego” jest to, że renderowana mapa może wydawać się nieco bardziej zwarta w pionie niż podejście „diamentowe”:

Zygzakowate podejście do renderowania wydaje się zwarte

Niekorzyść:

Próbując wdrożyć technikę zygzakowatą, wadą może być to, że nieco trudniej jest napisać kod renderujący, ponieważ nie można go napisać tak prosto, jak zagnieżdżona forpętla nad każdym elementem w tablicy:

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
        )

Ponadto może być trochę trudna próba ustalenia współrzędnych płytki ze względu na rozłożony charakter kolejności renderowania:

Współrzędne na renderowaniu zygzakowatym

Uwaga: ilustracje zawarte w tej odpowiedzi zostały utworzone przy użyciu implementacji Java przedstawionego kodu renderowania kafelków, z następującą inttablicą jako mapą:

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

Obrazy kafelków to:

  • tileImage[0] -> Pudełko z pudełkiem w środku.
  • tileImage[1] -> Czarna skrzynka.
  • tileImage[2] -> Białe pudełko.
  • tileImage[3] -> Pudełko z wysokim szarym przedmiotem.

Uwaga na temat szerokości i wysokości płytek

Zmienne tile_widthi tile_heightktóre są stosowane w powyższych przykładach kodu odnoszą się do szerokości i wysokości płyty naziemnej w obrazie reprezentującym płytki:

Obraz przedstawiający szerokość i wysokość kafelka

Korzystanie z wymiarów obrazu będzie działać, o ile wymiary obrazu i wymiary kafelków będą takie same. W przeciwnym razie mapę kafelków można renderować z przerwami między kafelkami.

coobird
źródło
136
nawet narysowałeś zdjęcia. To jest wysiłek.
zaratustra
Pozdrawiam coobird. Używam diamentowego podejścia do obecnej gry, którą opracowuję, i działa to na ucztę. Dzięki jeszcze raz.
Benny Hallett
3
Co z wieloma warstwami wysokości? Czy mogę narysować je pobocznie, zaczynając od najniższego poziomu i kontynuować aż do osiągnięcia najwyższego poziomu?
NagyI
2
@DomenicDatti: Dziękuję za miłe słowa :)
coobird
2
To jest niesamowite. I właśnie użył Twojego podejścia brylantem, a także, w przypadku gdy ktoś musi dostać coords siatki z pozycji ekranu, zrobiłem tak: j = (2 * x - 4 * y) / tilewidth * 0.5; i = (p.x * 2 / tilewidth) - j;.
Kyr Dunenkoff,
10

Tak czy inaczej, praca zostanie wykonana. Zakładam, że zygzakiem masz na myśli coś takiego: (liczby są kolejnością renderowania)

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

A przez diament rozumiesz:

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

Pierwsza metoda wymaga renderowania większej liczby kafelków, aby narysować pełny ekran, ale można łatwo sprawdzić granicę i pominąć wszystkie kafelki całkowicie poza ekranem. Obie metody będą wymagały pewnej liczby zgryzów, aby dowiedzieć się, jaka jest lokalizacja płytki 01. W końcu obie metody są w przybliżeniu równe pod względem matematycznym wymaganym dla pewnego poziomu wydajności.

zaratustra
źródło
14
Miałem na myśli odwrotnie. Kształt rombu (który pozostawia gładkie krawędzie mapy) i metoda zygzakowata, pozostawiając ostre krawędzie
Benny Hallett
1

Jeśli masz jakieś płytki, które przekraczają granice twojego diamentu, zalecamy rysowanie w kolejności:

...1...
..234..
.56789.
..abc..
...d...
Wouter Lievens
źródło
1

Odpowiedź Coobirda jest poprawna, kompletna. Jednak połączyłem jego podpowiedzi z poradami z innej witryny, aby stworzyć kod, który działa w mojej aplikacji (iOS / Objective-C), który chciałem udostępnić każdemu, kto tu przyjeżdża i szuka czegoś takiego. Jeśli podoba Ci się / głosuj w górę na tę odpowiedź, zrób to samo dla oryginałów; wszystko, co zrobiłem, to „stanąć na ramionach gigantów”.

Jeśli chodzi o porządek sortowania, moja technika jest zmodyfikowanym algorytmem malarza: każdy obiekt ma (a) wysokość podstawy (nazywam „poziomem”) i (b) X / Y dla „podstawy” lub „stopy” obraz (przykłady: podstawa awatara jest u jego stóp; podstawa drzewa u podstawy; podstawa samolotu to środkowy obraz itp.) Następnie sortuję od najniższego do najwyższego poziomu, a następnie od najniższego (najwyższego na ekranie) do najwyższej bazy Y, a następnie najniższa (najbardziej na lewo) do najwyższej podstawy-X. To renderuje kafelki tak, jak można by się spodziewać.

Kod do konwersji ekranu (punktu) na kafelek (komórkę) i odwrotnie:

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
źródło
1

Możesz użyć odległości euklidesowej od punktu najwyższego i najbliższego dla widza, z tą różnicą, że nie jest to w porządku. Daje to sferyczny porządek sortowania. Możesz to naprawić, patrząc z dalszej odległości. Dalej krzywizna ulega spłaszczeniu. Więc po prostu dodaj powiedz 1000 do każdego ze składników x, y i z, aby otrzymać x ', y' i z '. Sortuj według x '* x' + y '* y' + z '* z'.

Siobhan O'Connor
źródło
0

Prawdziwy problem polega na tym, że musisz narysować kilka kafelków / duszek przecinających się / obejmujących dwa lub więcej innych kafelków.

Po 2 (ciężkich) miesiącach osobistej analizy problemu w końcu znalazłem i wdrożyłem „poprawny rysunek renderowania” dla mojej nowej gry cocos2d-js. Rozwiązanie polega na mapowaniu dla każdego kafelka (podatnego), którego duszki to „przód, tył, góra i tył”. Po wykonaniu tej czynności możesz je narysować zgodnie z „logiką rekurencyjną”.

Carlos Lopez
źródło