Projekt całej mapy vs. projekt tablicy kafelkowej

11

Pracuję nad 2D RPG, które będą zawierały zwykłe mapy lochów / miast (wstępnie wygenerowane).

Używam kafelków, które następnie połączę, aby tworzyć mapy. Mój pierwotny plan polegał na złożeniu kafelków za pomocą Photoshopa lub innego programu graficznego, aby uzyskać jeden większy obraz, który mógłbym następnie wykorzystać jako mapę.

Przeczytałem jednak o kilku miejscach, w których ludzie rozmawiają o tym, jak wykorzystali tablice do zbudowania swojej mapy w silniku (więc dajesz tablicę x kafelkom silnika, a ona składa je jako mapę). Rozumiem, jak to się robi, ale implementacja wydaje się o wiele bardziej skomplikowana i nie widzę oczywistych zalet.

Jaka jest najczęstsza metoda i jakie są zalety / wady każdej z nich?

Cristol.GdM
źródło
1
Głównym problemem jest pamięć. Tekstura, która raz wypełnia ekran 1080p, ma około 60 MB. Obraz używa więc int na piksel, a kafelek używa int na (piksel / (rozmiar płytki * rozmiar płytki)). Jeśli więc kafelki mają 32,32, możesz przedstawić mapę 1024 razy większą, używając kafelków kontra piksel. Również czasy zamiany tekstur będą boleć, przepychając tak wiele pamięci dookoła itp.
ClassicThunder

Odpowiedzi:

19

Po pierwsze, powiem, że 2D RPG są bliskie i drogie mojemu sercu i praca ze starymi silnikami DX7 VB6 MORPG (nie śmiejcie się, to było 8 lat temu, teraz :-)) jest tym, co najpierw zainteresowało mnie tworzeniem gier . Niedawno zacząłem konwertować grę, nad którą pracowałem w jednym z tych silników, na XNA.

To powiedziawszy, zalecam, abyś używał struktury opartej na kafelkach z nakładaniem warstw na mapę. Z każdym używanym interfejsem API grafiki, będziesz mieć ograniczenie rozmiaru tekstur, które możesz załadować. Nie wspominając o limitach pamięci tekstur kart graficznych. Biorąc to pod uwagę, jeśli chcesz zmaksymalizować rozmiar swoich map, nie tylko minimalizując ilość i rozmiar tekstur ładowanych do pamięci, ale także zmniejszając rozmiar swoich zasobów na dysku twardym użytkownika ORAZ czasy ładowania, jesteś na pewno będę chciał iść z kafelkami.

Jeśli chodzi o implementację, szczegółowo opisałem, jak sobie z tym poradziłem w kilku pytaniach tutaj na GameDev.SE i na moim blogu (oba linki poniżej), a nie o to dokładnie pytasz, więc po prostu przejdź do podstaw tutaj. Zwrócę też uwagę na cechy kafelków, które sprawiają, że są one korzystne przy ładowaniu kilku dużych wstępnie renderowanych obrazów. Jeśli coś nie jest jasne, daj mi znać.

  1. Pierwszą rzeczą, którą musisz zrobić, to stworzyć arkusz. To tylko duży obraz, który zawiera wszystkie kafelki wyrównane w siatce. To (i może dodatkowy w zależności od liczby płytek) będzie jedyną rzeczą, którą musisz załadować. Tylko 1 zdjęcie! Możesz załadować 1 na mapę lub jedną na każdy kafelek w grze; jakakolwiek organizacja dla ciebie działa.
  2. Następnie musisz zrozumieć, w jaki sposób możesz wziąć ten „arkusz” i przetłumaczyć każdy kafelek na liczbę. Jest to dość proste z pewną prostą matematyką. Zauważ, że tutaj podział jest dzieleniem całkowitym , więc miejsca dziesiętne są pomijane (lub zaokrąglane w dół, jeśli wolisz). Jak przekonwertować komórki na współrzędne iz powrotem.
  3. OK, teraz, kiedy podzieliłeś arkusz na szereg komórek (liczb), możesz wziąć te liczby i podłączyć je do dowolnego pojemnika. Dla uproszczenia możesz po prostu użyć tablicy 2D.

    int[,] mapTiles = new int[100,100]; //Map is 100x100 tiles without much overhead
  4. Następnie chcesz je narysować. Jednym ze sposobów, aby uczynić to DUŻO wydajniejszym (w zależności od wielkości mapy), jest obliczenie tylko komórek, które aktualnie przegląda kamera i ich przeglądanie. Możesz to zrobić, pobierając współrzędne tablicy kafelków mapy z lewego górnego rogu ( tl) i prawego dolnego rogu ( br). Następnie wykonaj pętlę zi tl.X to br.X, w zagnieżdżonej pętli, z, tl.Y to br.Yaby je narysować. Przykładowy kod poniżej:

    for (int x = tl.X; x <= br.X;x++) {
        for (int y = tl.Y; y <= br.Y;y++) {
            //Assuming tileset setup from image
            Vector2 tilesetCoordinate = new Vector2((mapTiles[x,y] % 8) * 32,(mapTiles[x,y] / 8) * 32);
            //Draw 32x32 tile using tilesetCoordinate as the source x,y
        }
    }
  5. Pula! To są podstawy silnika kafelkowego. Widać, że łatwo jest mieć nawet mapę 1000 x 1000 z niewielkim narzutem. Ponadto, jeśli masz mniej niż 255 kafelków, możesz użyć tablicy bajtów zmniejszającej pamięć o 3 bajty na komórkę. Jeśli bajt jest zbyt mały, prawdopodobnie wystarczyłabyś do twoich potrzeb.

Uwaga: Pominąłem pojęcie współrzędnych światowych (na czym będzie opierać się pozycja twojego aparatu), ponieważ myślę, że to nie wchodzi w zakres tej odpowiedzi. Możesz przeczytać o tym tutaj na GameDev.SE.

Moje zasoby Tile-Engine
Uwaga: Wszystkie są skierowane na XNA, ale w zasadzie dotyczy to wszystkiego - wystarczy zmienić wezwania do losowania.

  • Moja odpowiedź na to pytanie opisuje, jak radzę sobie z komórkami mapy i warstwami w mojej grze. (Zobacz trzeci link.)
  • Moja odpowiedź na to pytanie wyjaśnia, w jaki sposób przechowuję dane w formacie binarnym.
  • To pierwszy (no technicznie drugi, ale pierwszy „techniczny”) post na blogu o rozwoju gry, nad którą pracowałem. Cała seria zawiera informacje o takich elementach, jak shadery pikseli, oświetlenie, zachowanie kafelków, ruch i wszystkie te fajne rzeczy. Właściwie zaktualizowałem post, aby zawierał treść mojej odpowiedzi na pierwszy link, który zamieściłem powyżej, więc możesz chcieć przeczytać go zamiast tego (mogłem coś dodać). Jeśli masz jakieś pytania, możesz dodać komentarz tutaj lub tutaj, a chętnie pomogę.

Inne zasoby silnika kafelkowego

  • Samouczek silnika kafelków z tej witryny dał mi podstawę do tworzenia map.
  • Nie obejrzałem jeszcze tych samouczków wideo, ponieważ nie miałem czasu, ale prawdopodobnie są one pomocne. :) Mogą być nieaktualne, jeśli używasz XNA.
  • Ta strona zawiera jeszcze kilka samouczków, które (jak sądzę) są oparte na powyższych filmach. Może warto to sprawdzić.
Richard Marskell - Drackir
źródło
Łał, to dwa razy więcej informacji niż zadałem i prawie odpowiada na wszystkie pytania, które nie zadałem, świetnie, dziękuję :)
Cristol.GdM
@Mikalichov - Cieszę się, że mogłem pomóc! :)
Richard Marskell - Drackir
Zazwyczaj z macierzą 2D chcesz użyć pierwszego wymiaru jako Y, a następnego jako X. W pamięci macierz będzie miała kolejno 0,0-i, a następnie 1,0-i. Jeśli zagnieżdżasz pętle z pierwszą kurą X, w rzeczywistości przeskakujesz w przód i tył w pamięci zamiast podążać sekwencyjną ścieżką czytania. Zawsze dla (y), a następnie dla (x).
Brett W
@BrettW - prawidłowy punkt. Dzięki. Zastanawiałem się, jak tablice 2D są przechowywane w .Net (kolejność rzędów. Dzisiaj dowiedziałem się czegoś nowego :-)). Jednak po zaktualizowaniu mojego kodu zdałem sobie sprawę, że jest dokładnie taki sam jak opisujesz, tylko z przełączonymi zmiennymi. Różnica polega na kolejności rysowania płytek. Mój obecny przykładowy rysunek rysuje od góry do dołu, od lewej do prawej, podczas gdy to, co opisujesz, rysuje od lewej do prawej, od góry do dołu. Dlatego dla uproszczenia postanowiłem przywrócić go z powrotem do oryginału. :-)
Richard Marskell - Drackir
6

Zarówno systemy oparte na kafelkach, jak i statyczny model / system tekstur mogą być używane do reprezentowania świata, a każdy z nich ma inną siłę. To, czy jedno jest lepsze od drugiego, sprowadza się do tego, jak używasz tych elementów, i co najlepiej pasuje do twojego zestawu umiejętności i potrzeb.

Biorąc to pod uwagę, oba systemy można stosować osobno lub łącznie.

Standardowy system oparty na kafelkach ma następujące zalety:

  • Prosta tablica 2D płytek
  • Każda płytka ma jeden materiał
    • Ten materiał może być pojedynczą teksturą, wielokrotnością lub mieszany z otaczających płytek
    • Może ponownie używać tekstur dla wszystkich kafelków tego typu, zmniejszając tworzenie zasobów i przeróbkę
  • Każda płytka może być przejezdna lub nie (wykrywanie kolizji)
  • Łatwe do renderowania i identyfikowania części mapy
    • Pozycja postaci i pozycja mapy są współrzędnymi bezwzględnymi
    • Prosta pętla w odpowiednich kafelkach dla regionu ekranu

Wadą kafelków jest to, że musisz stworzyć system do budowania danych opartych na kafelkach. Możesz stworzyć obraz, który używa każdego piksela jako kafelka, tworząc w ten sposób tablicę 2D z tekstury. Możesz także utworzyć własny format. Za pomocą flag bitowych możesz przechowywać dużą liczbę danych na kafelek w dość małej przestrzeni.

Głównym powodem, dla którego większość ludzi robi kafelki, jest to, że pozwala im tworzyć małe zasoby i wykorzystywać je ponownie. Dzięki temu możesz stworzyć znacznie większy obraz i mniejsze kawałki. Zmniejsza to konieczność przeróbek, ponieważ nie zmieniasz całej mapy świata, aby wprowadzić niewielką zmianę. Na przykład, jeśli chcesz zmienić odcień całej trawy. Na dużym obrazku trzeba odmalować całą trawę. W systemie kafelkowym wystarczy zaktualizować kafelki trawy.

Podsumowując, prawdopodobnie wykonasz o wiele mniej przeróbek w systemie opartym na kafelkach niż na dużej mapie graficznej. Możesz skończyć z systemem kolizji opartym na kafelkach, nawet jeśli nie użyjesz go do mapy terenu. To, że używasz kafelka wewnętrznie, nie oznacza, że ​​nie możesz używać modeli do reprezentowania obiektów środowiska, które mogą użyć więcej niż 1 kafelka dla ich przestrzeni.

Musisz podać więcej szczegółów na temat swojego środowiska / silnika / języka, aby podać przykłady kodu.

Brett W.
źródło
Dzięki! Pracuję pod C # i wybieram podobną (ale mniej utalentowaną) wersję Final Fantasy 6 (lub w zasadzie większość Square SNES RPG), więc płytki podłogowe ORAZ płytki strukturalne (tj. Domy). Moim największym zmartwieniem jest to, że nie widzę, jak zbudować macierz 2D bez spędzania godzin na sprawdzaniu „tam jest trawnik, potem trzy proste w poziomie, potem narożnik domu, a potem ...”, z mnóstwem możliwych błędów wzdłuż droga.
Cristol.GdM
Wygląda na to, że Richard objął cię specyfiką kodu. Osobiście użyłbym wyliczenia typów tilypów i użył tagów bitowych, aby zidentyfikować wartość kafelka. Pozwala to na przechowywanie wartości 32+ w średniej liczbie całkowitej. Będziesz także mógł przechowywać wiele flag na kafelku. Możesz więc powiedzieć Grass = true, Wall = true, Collision = true itd. Bez oddzielnych zmiennych. Następnie po prostu przypisujesz „motywy”, które określają arkusz kafelków dla grafiki dla tego konkretnego regionu mapy.
Brett W
3

Rysowanie mapy: Płytki są łatwe w silniku, ponieważ wtedy mapę można przedstawić jako gigantyczną tablicę wskaźników płytek. Narysuj kod to tylko zagnieżdżona pętla:

for i from min_x to max_x:
    for j from min_y to max_y:
        Draw(Tiles[i][j], getPosition(i,j))

W przypadku dużych map jeden ogromny obraz bitmapowy dla całej mapy może zajmować dużo miejsca w pamięci i może być większy niż karta graficzna obsługuje dla jednego rozmiaru obrazu. Ale nie będziesz mieć żadnych problemów z kafelkami, ponieważ przydzielasz pamięć graficzną tylko raz dla każdego kafelka (lub optymalnie jednego razem, jeśli wszystkie są na jednym arkuszu)

Łatwo jest również ponownie wykorzystać informacje o kafelkach, np. W przypadku kolizji. na przykład, aby uzyskać kafelek terenu w lewym górnym rogu duszka postaci:

let x,y = character.position
let i,j = getTileIndex(x,y) // <- getTileIndex is the opposite function of getPosition
let tile = Tiles[i][j]

Załóżmy, że musisz sprawdzić, czy nie ma kolizji z kamieniami / drzewami itp. Możesz uzyskać kafelek w pozycji (pozycja postaci + rozmiar duszka postaci + aktualny kierunek) i sprawdzić, czy jest oznaczony jako dostępny do przejścia lub nie.

Jimmy
źródło