Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

25.07.2008, 23:10

Container für Tilemap

Hallo zusammen,
ich hab das Problem bereits auf c++.de gepostet, ich wollte nun hier noch einige Ideen sammeln :)

Ich möchte ein Jump'n'Run programmieren. Ich hab bereits einmal eines zu einer Zeit, wo ich noch nicht so viel Ahnung von C++ hatte, geschrieben. Damals habe ich für die Tiles ein zweidimensionales statisches Array mit dem Typ meiner Tileklasse verwendet, was natürlich nicht gerade eine optimale Lösung darstellt.

Nun möchte ich das Ganze neu machen, und zwar dynamisch. Ich habe bereits daran gedacht, verschachtelte STL-Container (für 2D) zu verwenden. In Frage kämen ausschliesslich std::vector und std::deque wegen des Random Access. Letzterer wäre hierbei vielleicht etwas besser geeignet, weil die Tilemap leichter vergrössert werden kann (durch Funktion push_front() und günstigere Speicherverwaltung), jedoch ist der Random Access leicht langsamer als bei std::vector.

Bei beiden STL-Containern besteht aber das Problem, dass Einfügungen relativ langsam wird und einiges an Speicherplatz verschwendet wird, der sich auf 2 Dimensionen noch quadriert - im ungünstigsten Fall wird gerade einmal ein Viertel des tatsächlich benötigten Speicherplatzes genutzt. Wenn meine Tile-Klasse dann grösser wird, könnte sich das ungünstig auswirken, vor allem bei grossen Tilemaps.

Aus diesem Grund habe ich bereits über eine eigene Containerklasse Table und deren Implementierung als Template nachgedacht. Ich würde intern ein dynamisches, zweidimensionales Array aus Zeigern auf den Templatetypen T allokieren. Dadurch, dass Zeiger und nicht die Objekte selber verwaltet werden, sind interne Operationen wie Löschungen, Einfügungen und Reallokationen weniger schwerfällig. Zudem könnte ich die Funktionen InsertRow(), InsertCol(), RemoveRow(), RemoveCol() bereitstellen, die einiges vereinfachen würden.

Andererseits wäre für den RandomAccess neben einer Funktion Table::At(unsigned int x, unsigned int y) auch ein operator [] nützlich. Allerdings ist es relativ mühsam, den Indexoperator dafür zu überladen, das ja folgendes erstes Konstrukt leider nicht möglich ist:

C-/C++-Quelltext

1
2
/* 1. */  Table[x,y]
/* 2. */  Table[x][y]
Die zweite Möglichkeit ist dagegen schon realisierbar, allerdings nur über eine Dummy-Struktur. Da gibt es auch keine bessere Möglichkeit (operator () und At() gefallen mir eben nicht so ;)), oder?

Gibt es triftige Gründe, die gegen eine solche Umsetzung sprechen? Sind die STL-Container hierbei vorteilhafter (Performance ist nicht extrem entscheidend, Random Access sollte einfach schnell sein)? Falls jemand bereits ein Jump'n'Run oder ein anderes Spiel mit ähnlichem 2D-Aufbau realisiert hat, wäre ich froh, wenn dieser seine Implementierung präsentieren könnte.

drakon

Supermoderator

Beiträge: 6 513

Wohnort: Schweiz

Beruf: Entrepreneur

  • Private Nachricht senden

2

25.07.2008, 23:25

Hi Nexus auch hier. :)

Also das was du vorhast bringt dir überhaupt nix.
Im schlimmsten Fall hast du sogar Nachteile.
Ich benutze für meine Tilemap einen std::vectro<std::vector<Tile>>
Und glaub mir. Das Problem von der Performance liegt ganz wo anders. ;)

Und da kannst du mir auch glaube. Ich habe schon seeeehr viele Tiles da mal reingestopft. Einer hat beim testen, weil er meinte, dass es Pixel sind mal 2000x2000 gemacht. Naja.. Es ist gelaufen. Verständlicheweise langsam, aber vector war nicht das Problem. ;)

EDIT:
Ich habe dein Thread bei cpp auch kurz angeschaut und wie schon gesagt irgendwelche binäre Bäume sind da einfach Overkill.
Und wenn du lieb bist,gebe ich dir einen Tipp, wo der Zugriff auf ein Tile extreeem billig ist. ;)
Das kostet dich dann ein paar Multiplikationen und ein paar Additionen. Fertig. 8)

3

25.07.2008, 23:41

Hi drakon, du scheinst ja hier wie auch bei c++.de sehr aktiv zu sein :)

Zitat von »"drakon"«

Also das was du vorhast bringt dir überhaupt nix.
Im schlimmsten Fall hast du sogar Nachteile.

Inwiefern? Ich finds einfach noch praktisch, wenn man gerade in der Klasse Funktionen zur Einfügung/Löschung von Spalten oder Zeilen hat...

Zitat von »"drakon"«

Ich benutze für meine Tilemap einen std::vectro<std::vector<Tile>>
Und glaub mir. Das Problem von der Performance liegt ganz wo anders. ;)

Performance ist bei mir eigentlich auch nicht das Problem, ich glaube gern, dass STL-Container schneller als meine Implementationen sind, aber können diese nicht relativ schnell sehr viel Speicher fressen (und nur wenig davon nutzen)? Klar wird es für Jump'n'Run-Welten schon nicht gerade den RAM sprengen, aber ich will ja nicht extra verschwenderisch programmieren...

Zitat von »"drakon"«


Und wenn du lieb bist,gebe ich dir einen Tipp, wo der Zugriff auf ein Tile extreeem billig ist. ;)

Wie lieb muss ich denn dafür sein? :D

Ach ja, wie stehts eigentlich mit std::deque?

drakon

Supermoderator

Beiträge: 6 513

Wohnort: Schweiz

Beruf: Entrepreneur

  • Private Nachricht senden

4

25.07.2008, 23:53

Also wenn du unbednigt einen besseren Zugriff haben willst, dann schreib dir lieber einen kleinen Wrapper für einen 2 Dimensionalen vector. Da hast du dann alles drin. Performance und Handhabung.
Aber mal so am Rande wofür musst du deine Tilemap so extrem bearbeiten?
Also vergrössern/verkleiner musst du eigl. nie dynamisch. Spalten/Zeilen hinzufügen/entfernen?! :roll:
Mach ein 2 dimensionalen vector und dann kannst du da drin einen Zeiger auf ein Tile speichern. Wenn dort nix sein soll, machst du hal einen Null Zeiger rein und prüfst dementsprechend. Bearbeiten musst du da nix, ausser das Tile, dass da ist, aber auf das kannst du leicht zugreifen.

Also ich habe das so gemacht, dass ich sage, dass jedes Tile gleich gross ist auf einer Map. Dann, kannst du ganz einfach, wenn du z.B das Tile an Position 35.43:77.66 haben willst ganz einfach duch allfällige Multiplikation der Grösse direkt auf das Tile 35:78 kommen, wenn du dann noch abrundest. (und je nach Darstellung der Position musst du hald noch ein 50% dazurechnen bevor du abrundest, aber das kennst du ja wahrscheinlich schon).
Das funktioniert bei mir so recht gut.

5

26.07.2008, 00:10

Zitat von »"drakon"«

Aber mal so am Rande wofür musst du deine Tilemap so extrem bearbeiten?
Also vergrössern/verkleiner musst du eigl. nie dynamisch. Spalten/Zeilen hinzufügen/entfernen?! :roll:

Eben nicht extrem, aber es sollte trotzdem einigermassen konfortabel sein. ;) Ich werde einen Level-Editor haben, bei dem ich die Ränder verändern oder die Dimension explizit angeben kann. Aber der Vector-Wrapper ist eine Überlegung wert.

Zitat von »"drakon"«

Mach ein 2 dimensionalen vector und dann kannst du da drin einen Zeiger auf ein Tile speichern. Wenn dort nix sein soll, machst du hal einen Null Zeiger rein und prüfst dementsprechend. Bearbeiten musst du da nix, ausser das Tile, dass da ist, aber auf das kannst du leicht zugreifen.

Bringt es mehr, es über einen Zeiger zu lösen? Ich hätte jetzt eher an direkten Zugriff auf die Tiles gedacht, sonst bringt mir ja die automatische Speicherverwaltung vom Vektor nicht viel. Oder kommt man ernsthaft in Probleme wegen der Grösse, wenn man nicht Zeiger verwendet?

Zitat von »"drakon"«

Also ich habe das so gemacht, dass ich sage, dass jedes Tile gleich gross ist auf einer Map.

Ja, das ist bei mir auch so.

Zitat

Dann, kannst du ganz einfach, wenn du z.B das Tile an Position 35.43:77.66 haben willst ganz einfach duch allfällige Multiplikation der Grösse direkt auf das Tile 35:78 kommen, wenn du dann noch abrundest. (und je nach Darstellung der Position musst du hald noch ein 50% dazurechnen bevor du abrundest, aber das kennst du ja wahrscheinlich schon).

Sorry, hier kann ich dir nicht ganz folgen. Wozu brauchst du Multiplikationen, um das auf das nächste Tile runden?

Damit wir uns nicht missverstehen: Ich hab es immer so gehandhabt, dass die Tiles rasterförmig angeordnet und quadratisch sind. Die einzelnen Tiles haben dann ganzzahlige Indizes, was meinst du mit 35.43:77.66? Oder wäre das nur ein Spezialfall (z.B. in der Kollisionsabfrage)? Und beim Zeichnen würde ich schauen, welche im Bild sind, und diese dann in zwei For-Schleifen durchgehen.

xardias

Community-Fossil

Beiträge: 2 731

Wohnort: Santa Clara, CA

Beruf: Software Engineer

  • Private Nachricht senden

6

26.07.2008, 08:25

Also ich muss Drakon zustimmen. Es gibt kaum etwas statischeres in einem Spiel als die Tilemap (Wann man sich nicht gerade im Editor befindet). Da sollte man einen Container nehmen der möglichst schnelles Zeichnen und auffinden von Tiles ermöglicht.

Und ich denke für beides ist vector optimal. Aber wenn du es wirklich flexibel haben willst. Mach dir eine abstrakte Klasse, diese implementierst du einmal mit einem Vector einmal mit listen. So kannst du für den Editor die Listenversion nehmen um schnelleres einfügen zu erlauben und für Ingame die Vector version.

Wobei ich bezweifle, dass sich der Aufwand lohnt. Performancemäßig kommt vermutlich auf jede "Einschub" und "Vergrößerungs" Aktion der Tilemap tausende von Rendervorgängen und Lookups von Feldern ("Setzte Feld 23,34 auf xyz").
Und gerade was den Lookup angeht sind vectoren schneller, listen müssen von Anfang an komplett durchlaufen werden, vectoren nicht.

Also.. summa summarum.. ich würde sagen, dass der Vector eigentlich ziemlich ideal ist für Tilemaps.

drakon

Supermoderator

Beiträge: 6 513

Wohnort: Schweiz

Beruf: Entrepreneur

  • Private Nachricht senden

7

26.07.2008, 15:53

Zitat von »"Nexus"«

Zitat von »"drakon"«

Aber mal so am Rande wofür musst du deine Tilemap so extrem bearbeiten?
Also vergrössern/verkleiner musst du eigl. nie dynamisch. Spalten/Zeilen hinzufügen/entfernen?! :roll:

Eben nicht extrem, aber es sollte trotzdem einigermassen konfortabel sein. ;) Ich werde einen Level-Editor haben, bei dem ich die Ränder verändern oder die Dimension explizit angeben kann. Aber der Vector-Wrapper ist eine Überlegung wert.

Naja. Wenn für dich konfortablen bedeutet, dass du den einen eigenen vector oder ne map schreiben musst, gehts du lieber über vector und machst dir hald einen Wrapper das ist vieel konfortabler. ;)

Zitat von »"Nexus"«


Zitat von »"drakon"«

Mach ein 2 dimensionalen vector und dann kannst du da drin einen Zeiger auf ein Tile speichern. Wenn dort nix sein soll, machst du hal einen Null Zeiger rein und prüfst dementsprechend. Bearbeiten musst du da nix, ausser das Tile, dass da ist, aber auf das kannst du leicht zugreifen.

Bringt es mehr, es über einen Zeiger zu lösen? Ich hätte jetzt eher an direkten Zugriff auf die Tiles gedacht, sonst bringt mir ja die automatische Speicherverwaltung vom Vektor nicht viel. Oder kommt man ernsthaft in Probleme wegen der Grösse, wenn man nicht Zeiger verwendet?

Du kannst auch ganze Objekte (Tile) speichern. Spielt eigentlich nicht so eine grosse Rolle. Musst dann hald einfach intern speichern, ob das Tile übrehaupt gerendert werden soll, oder nicht. ;)
Naja. vector ist nicht unbedingt besser, wegen dem automatischen Speicherverwaltung. Das hast du mit normalen Arrays genau so, dass wenn das Array zerstört wird auch der Inhalt zerstört wird. (natürlich bei keinem von beidem wird delete aufgerufen). Das musst du dann hald schon noch selber machen beim zerstören, dass ist aber echt keine Sache.

Zitat von »"Nexus"«


Zitat von »"drakon"«

Also ich habe das so gemacht, dass ich sage, dass jedes Tile gleich gross ist auf einer Map.

Ja, das ist bei mir auch so.

Zitat

Dann, kannst du ganz einfach, wenn du z.B das Tile an Position 35.43:77.66 haben willst ganz einfach duch allfällige Multiplikation der Grösse direkt auf das Tile 35:78 kommen, wenn du dann noch abrundest. (und je nach Darstellung der Position musst du hald noch ein 50% dazurechnen bevor du abrundest, aber das kennst du ja wahrscheinlich schon).

Sorry, hier kann ich dir nicht ganz folgen. Wozu brauchst du Multiplikationen, um das auf das nächste Tile runden?

Damit wir uns nicht missverstehen: Ich hab es immer so gehandhabt, dass die Tiles rasterförmig angeordnet und quadratisch sind. Die einzelnen Tiles haben dann ganzzahlige Indizes, was meinst du mit 35.43:77.66? Oder wäre das nur ein Spezialfall (z.B. in der Kollisionsabfrage)? Und beim Zeichnen würde ich schauen, welche im Bild sind, und diese dann in zwei For-Schleifen durchgehen.


Naja, wenn du sagen wir sagst, dass ein Tile 1x1 gross ist (nicht Pixel, sondern Relativ), und dann die ganze map auf 0.5 skalierst, dann müsstest du hald die Abstände auch noch skalieren.
Das mit den Kommas meinte ich, wenn du einen Spieler auf Kollision testen willst genau. (Der kann sich ja auch ausserhalb der ganzzahligen Tiles befinden)

Ok. Ich gebe dir nochmal einen Tipp, wie du das ganze nochmal optimieren kannst. Wir haben ja gesagt, dass die Tilemap im Spiel recht statisch ist. Nun habe ich das bei mir so gemacht, dass ich hald beim laden die ganze Map (oder bei grösseren maps) zuerst einmal einfach z.B 30x30 Tiles in einen statischen VB schiebe. Wenn du jetzt eine Map hast, die 60x60 gross ist, hast du hald 4 VB's mit jeweils 30x30 Tiles drin. Jetzt kannst du ganz einfach diesen ganzen VB rendern, ohne jedesmal jedes Tile druchgehen zu müssen. (Das heisst, dass Quadtree o.ä. schon mal überflüssig ist und wahrscheinlich auch aufwendiger). Wenn du jetzt sagen wir 120x120 haben willst, hast du auch schon recht viele VB's. Nun kannst du aber einfach jede "Untermap" darauf kontrollieren, ob sie irgendwie sichtbar ist. (das ist dann lediglich ein sehr grosses Rect, dass du überprüfen musst). Und hast du so je nachdem zwischen 1-4 gerenderten Maps.
In meinem Editor rendere ich dann hald direkt aus einem vector<vector>. Ist dementsprechend langsamer, kann aber die Tilemap direkt verändern, wenn ich dann die Map speichere und wieder in meinem Spiel lade, ist die Veränderung da und die Geschwindigkeit auch.
Du musst dann lediglich den vector mit den Tiles gespeichert lassen, damit man im Spiel die Tiles abfragen kann, was aber, wie erwähnt eigentlich keinen Aufwand bringt.

Alles klar?

Wäre noch interessant zu Wissen, wie andere das gelöst haben? Jemand da, der ne Tilemap geschrieben hat? :)

8

26.07.2008, 16:14

Gut, vielen Dank für eure Vorschläge.

@ drakon:
Ich dachte beim Rendern eher, dass linear geprüft wird, welche Tiles alle zu rendern sind. Also nichts mit Untermaps. Dazu schaut man, wo sich die Ränder des Fensters relativ zur linken oberen Ecke der gesamten Karte befinden. So kann man mit Ganzzahldivisionen herausfinden, welches das erste zu rendernde Tile ist (links oben im Bildschirm) und geht dann in zwei For-Schleifen alle Tiles von links oben nach rechts unten durch. Ist das irgendwie zu wenig performant? Der ganze Rendervorgang läuft ja (abgesehen von nicht zu rendernden Tiles) in konstanter Zeit.
(Und was meinst du genau mit VB? :? )

Und was std::vector vs. Array betrifft: Abgesehen davon, dass Vector wahrscheinlich kaum langsamer ist, wird bei mir sowieso der grösste Teil der Zeit durch Grafikroutinen verbraten. Der Random Access sollte also kein Problem darstellen.

Zitat von »"drakon"«

Naja, wenn du sagen wir sagst, dass ein Tile 1x1 gross ist (nicht Pixel, sondern Relativ), und dann die ganze map auf 0.5 skalierst, dann müsstest du hald die Abstände auch noch skalieren.

Bei mir wird wahrscheinlich am Anfang von einer Datei gelesen, und die einzelnen Tiles werden im Konstruktor auf die richtige Grösse angepasst (durch statische Klassenvariable), da müsste man also lediglich diesen Wert ändern, und alles würde automatisch angepasst.
Und was meine eigene Template-Tilemap betrifft: Ich habe sie bereits geschrieben, um mich etwas ausführlicher mit dynamischer Speicherverwaltung zu beschäftigen, ich müsste also nicht noch zusätzlich Zeit verwenden für die Implementierung - beim Vector-Wrapper hingegen schon :p. Ich hatte übrigens gar nicht so lange, um sie zu implementieren. ;)

Ach ja, noch was: Warum sollte man std::vector und nicht std::deque verwenden? Was spricht gegen std::deque?

drakon

Supermoderator

Beiträge: 6 513

Wohnort: Schweiz

Beruf: Entrepreneur

  • Private Nachricht senden

9

26.07.2008, 16:30

Zitat

Ich dachte beim Rendern eher, dass linear geprüft wird, welche Tiles alle zu rendern sind. Also nichts mit Untermaps. Dazu schaut man, wo sich die Ränder des Fensters relativ zur linken oberen Ecke der gesamten Karte befinden. So kann man mit Ganzzahldivisionen herausfinden, welches das erste zu rendernde Tile ist (links oben im Bildschirm) und geht dann in zwei For-Schleifen alle Tiles von links oben nach rechts unten durch. Ist das irgendwie zu wenig performant? Der ganze Rendervorgang läuft ja (abgesehen von nicht zu rendernden Tiles) in konstanter Zeit.


Naja. So hatte ich das auch mal. Aber das rendern alleine war einfach zu langsam. Ein statischer VB (Vertexbuffer) ist einfach extrem performant zu rendern. Viele einzelne Tiles rendern ist (mit 99%) Wahrscheinlichkeit langsamer. Im übrigen ist Rendern nicht konstant. Das ist extrem exponentiell.

Gegen eine deque spricht eigentlich nix, ausser der Name. :)
Sollte keine Rolle spiele, was du nimmst.

Naja. Ich habe dir nur erlärt, wie ich es gelöst habe. Die genaue Implementierung kannst du natürlich anderst machen, aber ich habe da sehr viel ausprobiert, um eine Tilemap performant zu rendern und am Schluss ist dann eben das rausgekommen. ;)

Aber um ehrlich zu sein, weiss ich immer noch nicht, warum du überhaupt so einen Klasse haben willst. Zufügen/löschen tust du lediglich am Ende einer Map und das geht ganz einfach mit ::Resize() fertig. Dann auf jede Reihe anwenden und die Sache ist gegessen. Da es dir ja auch nicht auf die Geschwindigkeit ankommt, ist das doch keine Sache. (Vor allem, da es ja lediglich 1 - 2 mal vorkommt und das auch nur im Editor) :roll:

Das Gurke

Community-Fossil

Beiträge: 1 996

Wohnort: Pinneberg

Beruf: Schüler

  • Private Nachricht senden

10

26.07.2008, 17:02

Solltest du eine relativ kleine, statische Tilemap haben (z.B. ein einzelner Raum) kann es sich lohnen, diesen nur einmalig auf eine Textur zu rendern und dann mit der Textur weiterzuarbeiten. Es zwingt dich ja niemand etwas neu zu rendern, was sich nicht ändert ;)

Sowas kommt grad bei Tilemaps häufiger vor als man denkt.

Werbeanzeige