Jaki jest dobry sposób na przechowywanie danych mapy til?

13

Tworzę platformówkę 2D z kilkoma przyjaciółmi z Uni. Oparliśmy go na XNA Platformer Starter Kit, który używa plików .txt do przechowywania mapy kafelków. Chociaż jest to proste, nie zapewnia nam wystarczającej kontroli i elastyczności przy projektowaniu na poziomie. Kilka przykładów: w przypadku wielu warstw treści wymaganych jest wiele plików, każdy obiekt jest przymocowany do siatki, nie pozwala na obracanie obiektów, ograniczoną liczbę znaków itp. Robię więc badania, jak przechowywać dane poziomu i plik mapy.

Dotyczy to tylko przechowywania w systemie plików map kafelków, a nie struktury danych, która ma być używana przez grę podczas jej działania. Mapa kafelków jest ładowana do tablicy 2D, więc pytanie dotyczy tego, z którego źródła należy wypełnić tablicę.

Uzasadnienie dla DB: Z mojej perspektywy widzę mniej nadmiarowości danych przy użyciu bazy danych do przechowywania danych kafelków. Płytki w tej samej pozycji x, y o tej samej charakterystyce można ponownie wykorzystywać z poziomu na poziom. Wydaje się, że napisanie metody pobierania wszystkich kafelków używanych na danym poziomie z bazy danych byłoby dość proste.

Uzasadnienie dla JSON / XML: Pliki edytowalne wizualnie, zmiany można śledzić przez SVN o wiele łatwiej. Ale jest powtarzana treść.

Czy mają jakieś wady (czasy ładowania, czasy dostępu, pamięć itp.) W porównaniu do innych? A co jest powszechnie stosowane w branży?

Obecnie plik wygląda następująco:

....................
....................
....................
....................
....................
....................
....................
.........GGG........
.........###........
....................
....GGG.......GGG...
....###.......###...
....................
.1................X.
####################

1 - Punkt początkowy gracza, X - Wyjście z poziomu,. - Puste miejsce, # - Platforma, G - Klejnot

Stephen Tierney
źródło
2
Jakiego istniejącego „formatu” używasz? Powiedzenie „pliki tekstowe” oznacza po prostu, że nie zapisujesz danych binarnych. Kiedy mówisz „za mało kontroli i elastyczności”, jakie są konkretne problemy, na które napotykasz? Dlaczego tossup między XML a SQLite? To określi odpowiedź. blog.stackoverflow.com/2011/08/gorilla-vs-shark
tetradê
Wybrałbym JSON, ponieważ jest bardziej czytelny.
Den
3
Dlaczego ludzie myślą o używaniu SQLite do tych celów? To relacyjna baza danych ; dlaczego ludzie myślą, że relacyjna baza danych tworzy dobry format pliku?
Nicol Bolas,
1
@StephenTierney: Dlaczego to pytanie nagle zmieniło się z XML vs. SQLite na JSON vs. jakakolwiek baza danych? Moje pytanie dotyczy w szczególności tego, dlaczego nie jest to po prostu: „Jaki jest dobry sposób na przechowywanie danych mapy til?” To pytanie X w stosunku do Y jest arbitralne i bez znaczenia.
Nicol Bolas,
1
@Kylotan: Ale to nie działa zbyt dobrze. Bardzo trudno jest edytować, ponieważ możesz edytować bazę danych tylko za pomocą poleceń SQL. Trudno to odczytać, ponieważ tabele nie są tak naprawdę skutecznym sposobem patrzenia na mapę tilem i zrozumienia, co się dzieje. I chociaż może wyszukiwać, procedura wyszukiwania jest niezwykle skomplikowana. Sprawienie, by coś działało, jest ważne, ale jeśli sprawi, że proces tworzenia gry będzie o wiele trudniejszy, to nie warto iść krótką ścieżką.
Nicol Bolas,

Odpowiedzi:

14

Więc czytając zaktualizowane pytanie, wydaje się, że najbardziej martwisz się o „nadmiarowe dane” na dysku, z pewnymi drugorzędnymi obawami dotyczącymi czasów ładowania i wykorzystania przez przemysł.

Po pierwsze, dlaczego martwisz się nadmiarowymi danymi? Nawet w przypadku rozdętego formatu, takiego jak XML, implementacja kompresji i obniżenie rozmiaru poziomów byłoby dość proste. Bardziej prawdopodobne jest, że zajmiesz miejsce teksturami lub dźwiękami niż dane poziomu.

Po drugie, format binarny najprawdopodobniej załaduje się szybciej niż format tekstowy, który trzeba przeanalizować. Podwójnie, jeśli możesz po prostu upuścić go w pamięci i mieć tam swoje struktury danych. Istnieją jednak pewne wady tego. Po pierwsze, pliki binarne są prawie niemożliwe do debugowania (szczególnie dla osób tworzących treści). Nie możesz ich różnicować ani wersji. Ale będą mniejsze i ładują się szybciej.

To, co robią niektóre silniki (i co uważam za idealną sytuację), to wdrażanie dwóch ścieżek ładowania. Do programowania używasz pewnego rodzaju formatu tekstowego. Tak naprawdę nie ma znaczenia, jakiego konkretnego formatu używasz, o ile używasz solidnej biblioteki. W celu wydania przełączasz się na ładowanie wersji binarnej (mniejsze, szybsze ładowanie, być może pozbawione jednostek debugowania). Twoje narzędzia do edycji poziomów wypluwają oba. To o wiele więcej pracy niż tylko jeden format pliku, ale możesz w pewnym sensie uzyskać to, co najlepsze z obu światów.

Biorąc to wszystko pod uwagę, myślę, że trochę podskakujesz.

Pierwszym krokiem do rozwiązania tych problemów jest zawsze dokładne opisanie problemu. Jeśli wiesz, jakich danych potrzebujesz, to wiesz, jakie dane musisz przechowywać. Stamtąd jest to sprawdzalne pytanie optymalizacyjne.

Jeśli masz przypadek użycia do przetestowania, możesz przetestować kilka różnych metod przechowywania / ładowania danych, aby zmierzyć to, co uważasz za ważne (czas ładowania, użycie pamięci, rozmiar dysku itp.). Nie wiedząc o tym, trudno być bardziej szczegółowym.

Tetrad
źródło
9
  • XML: Łatwa do edycji ręcznie, ale gdy zaczniesz ładować dużo danych, spowalnia.
  • SQLite: Może to być lepsze, jeśli chcesz odzyskać wiele danych naraz lub nawet tylko małe porcje. Jednak chyba, że ​​używasz tego gdzie indziej w swojej grze, myślę, że jest to przesada dla twoich map (i prawdopodobnie nadmiernie skomplikowana).

Polecam użyć niestandardowego formatu pliku binarnego. W mojej grze mam tylko SaveMapmetodę, która przechodzi i zapisuje każde pole za pomocą BinaryWriter. Pozwala to również wybrać kompresję, jeśli chcesz, i daje większą kontrolę nad rozmiarem pliku. tzn. Zapisz shortzamiast zamiast, intjeśli wiesz, że nie będzie większy niż 32767. W dużej pętli zapisanie czegoś jako shortzamiast intmoże oznaczać znacznie mniejszy rozmiar pliku.

Ponadto, jeśli pójdziesz tą drogą, polecam pierwszą zmienną w pliku, która będzie numerem wersji.

Rozważmy na przykład klasę mapy (bardzo uproszczoną):

class Map {
    private const short MapVersion = 1;
    public string Name { get; set; }

    public void SaveMap(string filename) {
        //set up binary writer
        bw.Write(MapVersion);
        bw.Write(Name);
        //close/dispose binary writer
    }
    public void LoadMap(string filename) {
        //set up binary reader
        short mapVersion = br.ReadInt16();
        Name = br.ReadString();
        //close/dispose binary reader
    }
}

Powiedzmy, że chcesz dodać nową właściwość do mapy, powiedzmy Listo Platformobiektach. Wybrałem to, ponieważ jest to trochę bardziej zaangażowane.

Po pierwsze, zwiększasz MapVersioni dodajesz List:

private const short MapVersion = 2;
public string Name { get; set; }
public List<Platform> Platforms { get; set; }

Następnie zaktualizuj metodę zapisu:

public void SaveMap(string filename) {
    //set up binary writer
    bw.Write(MapVersion);
    bw.Write(Name);
    //Save the count for loading later
    bw.Write(Platforms.Count);
    foreach(Platform plat in Platforms) {
        //For simplicities sake, I assume Platform has it's own
        // method to write itself to a file.
        plat.Write(bw);
    }
    //close/dispose binary writer
}

Następnie, i tam właśnie naprawdę widzisz korzyści, zaktualizuj metodę ładowania:

public void LoadMap(string filename) {
    //set up binary reader
    short mapVersion = br.ReadInt16();
    Name = br.ReadString();
    //Create our platforms list
    Platforms = new List<Platform>();
    if (mapVersion >= 2) {
        //Version is OK, let's load the Platforms
        int mapCount = br.ReadInt32();
        for (int i = 0; i < mapCount; i++) {
            //Again, I'm going to assume platform has a static Read
            //  method that returns a Platform object
            Platforms.Add(Platform.Read(br));
        }
    } else {
        //If it's an older version, give it a default value
        Platforms.Add(new Platform());
    }
    //close/dispose binary reader
}

Jak widać, LoadMapsmetoda może nie tylko załadować najnowszą wersję mapy, ale także starsze wersje! Kiedy ładuje starsze mapy, masz kontrolę nad wartościami domyślnymi, których używa.

Richard Marskell - Drackir
źródło
9

Krótka historia

Dobrą alternatywą dla tego problemu jest przechowywanie poziomów w bitmapie, po jednym pikselu na płytkę. Korzystanie z RGBA pozwala na łatwe przechowywanie czterech różnych wymiarów (warstw, identyfikatorów, obrotu, odcienia itp.) Na jednym obrazie.

Długa historia

To pytanie przypomniało mi, kiedy Notch transmitował na żywo swój wpis o Ludum Dare kilka miesięcy temu, i chciałbym się podzielić na wypadek, gdybyś nie wiedział, co zrobił. Myślałem, że to naprawdę interesujące.

Zasadniczo używał map bitowych do przechowywania swoich poziomów. Każdy piksel w mapie bitowej odpowiadał jednemu „kafelkowi” na świecie (w jego przypadku tak naprawdę nie był to kafelek, ponieważ była to gra z prażonymi rodami, ale wystarczająco blisko). Przykład jednego z jego poziomów:

wprowadź opis zdjęcia tutaj

Jeśli więc używasz RGBA (8 bitów na kanał), możesz użyć każdego kanału jako innej warstwy i maksymalnie 256 rodzajów kafelków dla każdego z nich. Czy to wystarczy? Lub na przykład jeden z kanałów może utrzymywać obrót kafelka, jak wspomniano. Poza tym tworzenie edytora poziomów do pracy z tym formatem również powinno być dość trywialne.

A co najważniejsze, nie potrzebujesz nawet żadnego niestandardowego procesora treści, ponieważ możesz po prostu załadować go do XNA jak każdy inny Texture2D i odczytać piksele z powrotem w pętli.

IIRC użył czystej czerwonej kropki na mapie, aby zasygnalizować wrogowi, i w zależności od wartości kanału alfa dla tego piksela określałby typ wroga (np. Wartość alfa 255 może być nietoperzem, a 254 to zombie lub coś innego).

Innym pomysłem byłoby podzielenie obiektów gry na te, które są ustalone w siatce, i te, które mogą „drobno” poruszać się między płytkami. Zachowaj ustalone kafelki w mapie bitowej, a obiekty dynamiczne na liście.

Może nawet istnieć sposób na zmultipleksowanie jeszcze większej ilości informacji w tych 4 kanałach. Jeśli ktoś ma na to jakiś pomysł, daj mi znać. :)

David Gouveia
źródło
3

Prawdopodobnie mógłbyś uciec z prawie wszystkim. Niestety współczesne komputery są tak szybkie, że ta przypuszczalnie stosunkowo niewielka ilość danych nie zrobiłaby zauważalnej różnicy czasu, nawet jeśli jest przechowywana w rozdętym i źle skonstruowanym formacie danych, który wymaga dużej ilości przetwarzania do odczytania.

Jeśli chcesz zrobić „właściwą” rzecz, tworzysz format binarny, jak sugerował Drackir, ale jeśli dokonujesz innego wyboru z jakiegoś głupiego powodu, prawdopodobnie nie wróci i nie ugryzie (przynajmniej nie pod względem wydajności).

aaaaaaaaaaaa
źródło
1
Tak, to zdecydowanie zależy od rozmiaru. Mam mapę zawierającą około 161 000 kafelków. Każda z nich ma 6 warstw. Pierwotnie miałem go w XML, ale miał 82 MB i ładowanie trwało wieczność . Teraz zapisuję go w formacie binarnym i ma około 5 MB przed kompresją i ładuje się w około 200 ms. :)
Richard Marskell - Drackir
2

Zobacz to pytanie: „Binarny XML” dla danych gry? Zdecydowałbym się również na JSON, YAML, a nawet XML, ale ma to dość duże obciążenie. Jeśli chodzi o SQLite, nigdy nie użyłbym tego do przechowywania poziomów. Relacyjna baza danych jest przydatna, jeśli spodziewasz się przechowywać dużo powiązanych danych (np. Zawiera linki relacyjne / klucze obce) i jeśli oczekujesz zadawania dużej liczby różnych zapytań, przechowywanie mapowania nie wydaje się robić tego rodzaju i dodałoby niepotrzebnej złożoności.

Roy T.
źródło
2

Podstawowym problemem tego pytania jest to, że łączy w sobie dwie koncepcje, które nie mają ze sobą nic wspólnego:

  1. Paradygmat przechowywania plików
  2. Reprezentacja danych w pamięci

Możesz przechowywać swoje pliki jako zwykły tekst w dowolnym dowolnym formacie, JSON, skrypcie Lua, XML, dowolnym formacie binarnym itp. I żaden z nich nie będzie wymagał , aby reprezentacja tych danych w pamięci miała jakąkolwiek konkretną formę.

Zadaniem twojego kodu ładującego poziom jest konwersja paradygmatu przechowywania plików na reprezentację w pamięci. Na przykład mówisz: „Widzę mniej nadmiarowości danych przy użyciu bazy danych do przechowywania danych kafelków”. Jeśli chcesz mniej nadmiarowości, powinien to sprawdzić Twój kod ładujący poziom. To jest coś, z czym powinna sobie poradzić Twoja reprezentacja w pamięci.

To nie jest coś, czym powinien zajmować się Twój format pliku. I oto dlaczego: te pliki muszą skądś pochodzić.

Albo będziesz pisać je ręcznie, albo będziesz używał jakiegoś narzędzia, które tworzy i edytuje dane na poziomie. Jeśli piszesz je ręcznie, najważniejszą rzeczą, której potrzebujesz, jest format, który jest łatwy do odczytania i modyfikacji. Nadmiarowość danych nie jest czymś Twój Format nawet musi rozważyć, ponieważ będzie spędzać większość swojej czasie edycji plików. Czy chcesz ręcznie korzystać z wszelkich wymyślonych mechanizmów, aby zapewnić nadmiarowość danych? Czy nie byłoby lepiej wykorzystać swój czas, aby twój program ładujący zajął się tym?

Jeśli masz narzędzie, które je tworzy, faktyczny format ma znaczenie (co najwyżej) ze względu na czytelność. Możesz umieścić wszystko w tych plikach. Jeśli chcesz pominąć zbędne dane w pliku, po prostu zaprojektuj format, który może to zrobić, i spraw, aby Twoje narzędzie prawidłowo korzystało z tej możliwości. Twój format tilemap może obejmować kompresję RLE (kodowanie w czasie wykonywania), kompresję duplikacji i dowolne techniki kompresji, ponieważ jest to twój format tilemap.

Problem rozwiązany.

Relacyjne bazy danych (RDB) istnieją, aby rozwiązać problem wykonywania złożonych wyszukiwań w dużych zestawach danych zawierających wiele pól danych. Jedynym rodzajem wyszukiwania, jakie przeprowadzasz na mapie kafelków, jest „zdobądź kafelek w pozycji X, Y”. Każda tablica może to obsłużyć. Używanie relacyjnej bazy danych do przechowywania i utrzymywania danych mapy warstw byłoby ekstremalnie przesadne i nie warte niczego.

Nawet jeśli wbudujesz kompresję w swoją reprezentację w pamięci, nadal będziesz w stanie łatwo pokonać RDB pod względem wydajności i zajmowanego miejsca w pamięci. Tak, będziesz musiał wdrożyć tę kompresję. Ale jeśli rozmiar danych w pamięci stanowi taki problem, że należy rozważyć RDB, prawdopodobnie prawdopodobnie chcesz wdrożyć określone rodzaje kompresji. Będziesz szybszy niż RDB i jednocześnie mniej pamięci.

Nicol Bolas
źródło
1
  • XML: bardzo prosty w użyciu, ale narzut staje się denerwujący, gdy struktura staje się głęboka.
  • SQLite: wygodniejszy dla większych struktur.

W przypadku naprawdę prostych danych prawdopodobnie wybrałbym trasę XML, jeśli znasz już ładowanie i parsowanie XML.

Inżynier
źródło