Ganzes Kartendesign vs. Kachel-Array-Design

10

Ich arbeite an einem 2D-Rollenspiel, das die üblichen Dungeon- / Stadtpläne (vorgeneriert) enthält.

Ich verwende Kacheln, die ich dann kombinieren werde, um die Karten zu erstellen. Mein ursprünglicher Plan war es, die Kacheln mit Photoshop oder einem anderen Grafikprogramm zusammenzusetzen, um ein größeres Bild zu erhalten, das ich dann als Karte verwenden konnte.

Ich habe jedoch an mehreren Stellen gelesen, dass Leute darüber gesprochen haben, wie sie Arrays verwendet haben, um ihre Karte in der Engine zu erstellen (also geben Sie Ihrer Engine ein Array von x Kacheln und sie werden als Karte zusammengesetzt). Ich kann verstehen, wie es gemacht wird, aber es scheint viel komplizierter zu implementieren, und ich kann keine offensichtlichen Vorteile erkennen.

Was ist die häufigste Methode und welche Vor- und Nachteile hat jede?

Cristol.GdM
quelle
1
Das Hauptproblem ist das Gedächtnis. Eine Textur, die einen 1080p-Bildschirm einmal ausfüllt, ist ungefähr 60 MB groß. Ein Bild verwendet also ein int pro Pixel, eine Kachel ein int pro (Pixel / (Kachelgröße * Kachelgröße)). Wenn die Kacheln also 32,32 sind, können Sie eine 1024-mal so große Karte darstellen, indem Sie Kacheln gegen ein Pixel verwenden. Auch Texturwechselzeiten werden weh tun, wenn man so viel Speicher
herumschiebt

Antworten:

18

Lassen Sie mich zunächst einmal sagen, dass mir 2D-RPGs sehr am Herzen liegen. Die Arbeit mit alten DX7 VB6 MORPG-Engines (nicht lachen, es war jetzt vor 8 Jahren :-)) hat mich zuerst für die Spieleentwicklung interessiert . In jüngerer Zeit habe ich begonnen, ein Spiel, an dem ich in einer dieser Engines gearbeitet habe, auf XNA umzustellen.

Meine Empfehlung lautet jedoch, dass Sie eine kachelbasierte Struktur mit Ebenen für Ihre Karte verwenden. Mit jeder Grafik-API, die Sie verwenden, ist die Größe der Texturen, die Sie laden können, begrenzt. Ganz zu schweigen von den Speicherbeschränkungen für die Grafikkartentextur. Wenn Sie also die Größe Ihrer Karten maximieren und gleichzeitig nicht nur die Menge und Größe der Texturen minimieren möchten, die Sie in den Speicher laden, sondern auch die Größe Ihrer Assets auf der Festplatte des Benutzers UND die Ladezeiten verringern möchten, sind Sie es Ich werde definitiv mit Fliesen gehen wollen.

Was die Implementierung angeht, habe ich bei einigen Fragen hier auf GameDev.SE und in meinem Blog (beide unten verlinkt) ausführlich darauf eingegangen, wie ich damit umgegangen bin, und das ist nicht genau das, was Sie fragen, also werde ich es einfach tun Gehen Sie hier auf die Grundlagen ein. Ich werde auch die Merkmale von Kacheln notieren, die sie gegenüber dem Laden mehrerer großer vorgerenderter Bilder vorteilhaft machen. Wenn etwas nicht klar ist, lass es mich wissen.

  1. Als erstes müssen Sie ein Kachelblatt erstellen. Dies ist nur ein großes Bild, das alle Ihre Kacheln enthält, die in einem Raster ausgerichtet sind. Dies (und möglicherweise eine zusätzliche, abhängig von der Anzahl der Kacheln) ist das einzige, was Sie laden müssen. Nur 1 Bild! Sie können 1 pro Karte oder eine mit jedem Plättchen im Spiel laden. Welche Organisation auch immer für Sie arbeitet.
  2. Als nächstes müssen Sie verstehen, wie Sie dieses "Blatt" nehmen und jede Kachel in eine Zahl übersetzen können. Dies ist mit einfachen Berechnungen ziemlich einfach. Beachten Sie, dass die Division hier eine ganzzahlige Division ist , sodass die Dezimalstellen entfernt (oder abgerundet, wenn Sie dies bevorzugen). So konvertieren Sie Zellen in Koordinaten und zurück.
  3. OK, jetzt, da Sie das Kachelblatt in eine Reihe von Zellen (Zahlen) aufgeteilt haben, können Sie diese Zahlen nehmen und in einen beliebigen Container stecken. Der Einfachheit halber können Sie einfach ein 2D-Array verwenden.

    int[,] mapTiles = new int[100,100]; //Map is 100x100 tiles without much overhead
  4. Als nächstes möchten Sie sie zeichnen. Eine der Möglichkeiten, wie Sie dies VIEL effizienter gestalten können (abhängig von der Kartengröße), besteht darin, nur die Zellen zu berechnen, die die Kamera gerade anzeigt, und diese zu durchlaufen. Sie können dies tun, indem Sie die Koordinaten des Kartenkachel-Arrays der oberen linken ( tl) und unteren rechten ( br) Ecken der Kamera abrufen. Dann Schleife von tl.X to br.Xund in einer verschachtelten Schleife von, tl.Y to br.Yum sie zu zeichnen. Beispielcode unten:

    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. Jackpot! Das sind die Grundlagen der Fliesenmaschine. Sie können sehen, dass es einfach ist, eine 1000x1000-Karte mit wenig Aufwand zu haben. Wenn Sie weniger als 255 Kacheln haben, können Sie auch ein Byte-Array verwenden, das den Speicher um 3 Bytes pro Zelle reduziert. Wenn ein Byte zu klein ist, würde wahrscheinlich ein Ushort für Ihre Anforderungen ausreichen.

Hinweis: Ich habe das Konzept der Weltkoordinaten (auf dem die Position Ihrer Kamera basiert) weggelassen, da dies meiner Meinung nach außerhalb des Rahmens dieser Antwort liegt. Sie können dies hier auf GameDev.SE nachlesen.

Meine Tile-Engine-Ressourcen
Hinweis: Alle diese Ressourcen richten sich an XNA, gelten jedoch für alles - Sie müssen nur die Draw-Aufrufe ändern.

  • Meine Antwort auf diese Frage beschreibt, wie ich mit den Kartenzellen und der Überlagerung in meinem Spiel umgehe. (Siehe dritter Link.)
  • Meine Antwort auf diese Frage erklärt, wie ich die Daten in einem Binärformat speichere.
  • Dies ist der erste (technisch gesehen zweite, aber erste "technische") Blog-Beitrag über die Entwicklung des Spiels, an dem ich gearbeitet habe. Die ganze Serie enthält Informationen über Dinge wie Pixel-Shader, Beleuchtung, Kachelverhalten, Bewegung und all das lustige Zeug. Ich habe den Beitrag tatsächlich aktualisiert, um den Inhalt meiner Antwort auf den ersten Link, den ich oben gepostet habe, aufzunehmen. Vielleicht möchten Sie ihn stattdessen lesen (ich habe möglicherweise Dinge hinzugefügt). Wenn Sie Fragen haben, können Sie dort oder hier einen Kommentar abgeben und ich helfe Ihnen gerne weiter.

Andere Tile-Engine-Ressourcen

  • Das Tutorial der Kachel-Engine auf dieser Website gab mir die Grundlage für die Erstellung meiner Karten.
  • Ich habe diese Video-Tutorials noch nicht gesehen, weil ich keine Zeit hatte, aber sie sind wahrscheinlich hilfreich. :) Sie können jedoch veraltet sein, wenn Sie XNA verwenden.
  • Diese Seite enthält einige weitere Tutorials, die (glaube ich) auf den obigen Videos basieren. Es kann sich lohnen, einen Blick darauf zu werfen.
Richard Marskell - Drackir
quelle
Waow, das ist zweimal mehr Info als ich gestellt habe und beantwortet so ziemlich alle Fragen, die ich nicht gestellt habe, großartig, danke :)
Cristol.GdM
@Mikalichov - Ich bin froh, dass ich helfen konnte! :)
Richard Marskell - Drackir
Normalerweise möchten Sie bei einem 2D-Array die erste Dimension als Ihr Y und die nächste als Ihr X verwenden. Im Speicher wird ein Array nacheinander 0,0-i und dann 1,0-i sein. Wenn Sie mit X zuerst verschachtelte Schleifen ausführen, springen Sie tatsächlich im Speicher hin und her, anstatt einem sequentiellen Lesepfad zu folgen. Immer für (y) dann für (x).
Brett W
@BrettW - Ein gültiger Punkt. Vielen Dank. Ich habe mich gefragt, wie 2D-Arrays in .Net gespeichert sind (Zeilen-Hauptreihenfolge. Habe heute etwas Neues gelernt! :-)). Nachdem ich meinen Code aktualisiert hatte, stellte ich jedoch fest, dass er genau dem entspricht, was Sie beschreiben, nur dass die Variablen vertauscht wurden. Der Unterschied besteht darin, in welcher Reihenfolge die Kacheln gezeichnet werden. Mein aktueller Beispielcode zeichnet von oben nach unten, von links nach rechts, während das, was Sie beschreiben, von links nach rechts, von oben nach unten zeichnen würde. Der Einfachheit halber habe ich mich entschlossen, das Original wiederherzustellen. :-)
Richard Marskell - Drackir
6

Sowohl kachelbasierte Systeme als auch ein statisches Modell / Textur-System können verwendet werden, um eine Welt darzustellen, und jedes hat unterschiedliche Stärken. Ob eines besser ist als das andere, hängt davon ab, wie Sie die Teile verwenden und was für Ihre Fähigkeiten und Bedürfnisse am besten geeignet ist.

Davon abgesehen können beide Systeme einzeln oder auch zusammen verwendet werden.

Ein Standardsystem auf Fliesenbasis hat die folgenden Stärken:

  • Einfaches 2D-Array von Kacheln
  • Jede Fliese hat ein einzelnes Material
    • Dieses Material kann eine einzelne Textur sein, mehrere oder aus umgebenden Fliesen gemischt werden
    • Kann Texturen für alle Kacheln dieses Typs wiederverwenden, wodurch die Erstellung und Nacharbeit von Assets reduziert wird
  • Jede Kachel kann passierbar sein oder nicht (Kollisionserkennung)
  • Einfach zu rendern und Teile der Karte zu identifizieren
    • Zeichenposition und Kartenposition sind absolute Koordinaten
    • Einfache Durchsicht relevanter Kacheln für den Bildschirmbereich

Der Nachteil von Kacheln besteht darin, dass Sie ein System zum Erstellen dieser kachelbasierten Daten erstellen müssen. Sie können ein Bild erstellen, das jedes Pixel als Kachel verwendet und so Ihr 2D-Array aus einer Textur erstellt. Sie können auch ein proprietäres Format erstellen. Mit Bitflags können Sie eine große Anzahl von Daten pro Kachel auf relativ kleinem Raum speichern.

Der Hauptgrund, warum die meisten Leute Kacheln machen, ist, dass sie kleine Assets erstellen und diese wiederverwenden können. Auf diese Weise können Sie ein viel größeres Bild erstellen, um kleinere Teile zu erhalten. Es reduziert Nacharbeiten, da Sie nicht die gesamte Weltkarte ändern, um eine kleine Änderung vorzunehmen. Zum Beispiel, wenn Sie den Schatten des gesamten Grases ändern möchten. In einem großen Bild müssten Sie das gesamte Gras neu streichen. In einem Fliesensystem aktualisieren Sie einfach die Grasplättchen.

Alles in allem werden Sie wahrscheinlich viel weniger Nacharbeiten mit einem kachelbasierten System durchführen als mit einer großen grafischen Karte. Möglicherweise verwenden Sie ein kachelbasiertes System für Kollisionen, auch wenn Sie es nicht für Ihre Bodenkarte verwenden. Nur weil Sie eine Kachel intern verwenden, heißt das nicht, dass Sie keine Modelle verwenden können, um die Objekte der Umgebung darzustellen, die möglicherweise mehr als eine Kachel für ihren Platz verwenden.

Sie müssten mehr Details zu Ihrer Umgebung / Engine / Sprache angeben, um Codebeispiele bereitzustellen.

Brett W.
quelle
Vielen Dank! Ich arbeite unter C # und strebe eine ähnliche (aber weniger talentierte) Version von Final Fantasy 6 (oder im Grunde das meiste Square SNES-Rollenspiel) an, also Bodenfliesen UND Strukturfliesen (dh Häuser). Meine größte Sorge ist, dass ich nicht sehe, wie man das 2D-Array erstellt, ohne stundenlang zu überprüfen, ob es dort eine Grasecke gibt, dann drei gerade horizontale, dann eine Hausecke, dann ..., mit Tonnen möglicher Fehler entlang der Weg.
Cristol.GdM
Es sieht so aus, als hätte Richard Sie in Bezug auf einen Code behandelt. Ich persönlich würde eine Aufzählung von Tiletypen verwenden und Bitflags verwenden, um den Wert der Kachel zu identifizieren. Auf diese Weise können Sie mehr als 32 Werte in einer durchschnittlichen Ganzzahl speichern. Sie können auch mehrere Flags pro Kachel speichern. Sie können also Grass = true, Wall = true, Collision = true usw. ohne separate Variablen sagen. Dann weisen Sie einfach "Themen" zu, die das Kachelblatt für die Grafiken für diesen bestimmten Kartenbereich bestimmen.
Brett W
3

Zeichnen der Karte: Kacheln schonen die Engine, da die Karte dann als riesige Anordnung von Kachelindizes dargestellt werden kann. Zeichnungscode ist nur eine verschachtelte Schleife:

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

Bei großen Karten kann ein großes Bitmap-Bild für die gesamte Karte viel Speicherplatz beanspruchen und größer sein als das, was eine Grafikkarte für eine einzelne Bildgröße unterstützt. Sie haben jedoch keine Probleme mit Kacheln, da Sie für jede Kachel nur einmal Grafikspeicher zuweisen (oder optimalerweise eine Summe, wenn sich alle auf einem Blatt befinden).

Es ist auch einfach, die Kachelinformationen für z. B. Kollisionen wiederzuverwenden. So erhalten Sie beispielsweise die Geländekachel in der oberen linken Ecke des Sprites des Charakters:

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

Angenommen, Sie müssen nach Kollisionen mit Steinen / Bäumen usw. suchen. Sie können die Kachel an der Position (Zeichenposition + Zeichen-Sprite-Größe + aktuelle Richtung) abrufen und prüfen, ob sie als begehbar oder nicht begehbar markiert ist.

Jimmy
quelle