Laden und Speichern einer Tilemap

    • VB.NET

    Es gibt 10 Antworten in diesem Thema. Der letzte Beitrag () ist von Artentus.

      Laden und Speichern einer Tilemap

      Nach häufiger Nachfrage gibts jetzt auch mal ein ein Tilemap-Tutorial von mir. Gleich vorneweg, dieses Tutorial behandelt wirklich ausschließlich das Laden und Speichern von Tilemaps, grundlegendes Wissen für die Spieleentwicklung wird hier nicht vermittelt.


      Dann wollen wir mal anfangen. Bevor wir uns ums Laden und Speichern Gedanken machen können, müssen wir erst einmal festlegen, wie wir die Daten innerhalb des Programmes darstellen wollen. Ich habe hier mal ein extrem vereinfachtes Modell genommen, damit wir uns wirklich auf das eigentliche Problem fixieren können. Da eine Tilemap aus Tiles zusammengesetzt ist, ist es logisch, auch eine Klasse Tile zu erstellen. Diese Klasse besitzt bei mir die Eigenschaften X, Y und ID (ich denke es sollte klar sein, für was diese stehen).
      VB

      VB.NET-Quellcode

      1. Public Class Tile
      2. Private _id As Integer, _x As Integer, _y As Integer
      3. Public Property ID() As Integer
      4. Get
      5. Return _id
      6. End Get
      7. Set
      8. _id = value
      9. End Set
      10. End Property
      11. Public ReadOnly Property X() As Integer
      12. Get
      13. Return _x
      14. End Get
      15. End Property
      16. Public ReadOnly Property Y() As Integer
      17. Get
      18. Return _y
      19. End Get
      20. End Property
      21. Public Sub New(id As Integer, x As Integer, y As Integer)
      22. _id = id
      23. _x = x
      24. _y = y
      25. End Sub
      26. End Class
      C#

      C-Quellcode

      1. public class Tile
      2. {
      3. int _id, _x, _y;
      4. public int ID
      5. {
      6. get
      7. {
      8. return _id;
      9. }
      10. set
      11. {
      12. _id = value;
      13. }
      14. }
      15. public int X
      16. {
      17. get
      18. {
      19. return _x;
      20. }
      21. }
      22. public int Y
      23. {
      24. get
      25. {
      26. return _y;
      27. }
      28. }
      29. public Tile(int id, int x, int y)
      30. {
      31. _id = id;
      32. _x = x;
      33. _y = y;
      34. }
      35. }

      Dann brauchen wir natürlich auch unsere Spielfigur. Diese ist bei mir ebenfalls eine Klasse.
      VB

      VB.NET-Quellcode

      1. Public Class Player
      2. Private _x As Integer, _y As Integer
      3. Public ReadOnly Property X() As Integer
      4. Get
      5. Return _x
      6. End Get
      7. End Property
      8. Public ReadOnly Property Y() As Integer
      9. Get
      10. Return _y
      11. End Get
      12. End Property
      13. Public Sub New(x As Integer, y As Integer)
      14. _x = x
      15. _y = y
      16. End Sub
      17. End Class
      C#

      C-Quellcode

      1. public class Player
      2. {
      3. int _x, _y;
      4. public int X
      5. {
      6. get
      7. {
      8. return _x;
      9. }
      10. }
      11. public int Y
      12. {
      13. get
      14. {
      15. return _y;
      16. }
      17. }
      18. public Player(int x, int y)
      19. {
      20. _x = x;
      21. _y = y;
      22. }
      23. }

      Und zu guter Letzt wäre da natürlich auch noch die Map selber.
      Bei mir sieht es so aus, dass die Map-Klasse einen Spieler, ein zweidimensionales Array von Tiles und auch ihre Breite und Höhe speichert. Zusätzlich soll man eine Map aus einer Datei erstellen und in eine Datei speichern können.
      VB

      VB.NET-Quellcode

      1. Public Class Map
      2. Private _player As Player
      3. Private _width As Integer, _height As Integer
      4. Private tiles As Tile(,)
      5. Public ReadOnly Property Player() As Player
      6. Get
      7. Return _player
      8. End Get
      9. End Property
      10. Public ReadOnly Property Width() As Integer
      11. Get
      12. Return _width
      13. End Get
      14. End Property
      15. Public ReadOnly Property Height() As Integer
      16. Get
      17. Return _height
      18. End Get
      19. End Property
      20. Public Default ReadOnly Property Item(x As Integer, y As Integer) As Tile
      21. Get
      22. Return tiles(x, y)
      23. End Get
      24. End Property
      25. Public Sub New(path As String)
      26. End Sub
      27. Public Sub Save(path As String)
      28. End Sub
      29. End Class
      C#

      C-Quellcode

      1. public class Map
      2. {
      3. Player _player;
      4. int _width, _height;
      5. Tile[,] tiles;
      6. public Player Player
      7. {
      8. get
      9. {
      10. return _player;
      11. }
      12. }
      13. public int Width
      14. {
      15. get
      16. {
      17. return _width;
      18. }
      19. }
      20. public int Height
      21. {
      22. get
      23. {
      24. return _height;
      25. }
      26. }
      27. public Tile this[int x, int y]
      28. {
      29. get
      30. {
      31. return tiles[x, y];
      32. }
      33. }
      34. public Map(string path)
      35. {
      36. }
      37. public void Save(string path)
      38. {
      39. }
      40. }
      Nun gilt es, den Konstruktor und die Save-Methode zu implementieren.


      Jetzt können wir uns dem Format widmen, mit dem wir unsere Daten speichern wollen. Ich habe es hier wieder sehr einfach gehalten.
      Ich habe mich für ein binäres Format entschieden. Dieses kann zwar nicht per Hand bearbeitet werden (theoretisch schon, mit einem HexEditor), sondern braucht einen Map-Editor (einen solchen sollte man aber sowieso immer haben, da es die Dinge ungemein vereinfacht), aber ist dafür leichter für das Programm zu lesen und spart außerdem Speicherplatz.
      Die Datei kann man in zwei Teile zerlegen, den ersten Teil, der immer die selbe Länge hat, und den zweiten mit variabler Länge. Hier muss ich dazu sagen, dass dies wirklich die Variante für Arme ist. Man könnte die Datei auch in Chunks unterteilen, mit Headern und allem drum und dran, wodurch man um einiges flexibler wird, das wäre aber nicht mer anfängerfreundlich.
      Im ersten Teil werden die Mapgröße und die Startposition der Spielfigur gespeichert. Dies sind immer 4 Werte und in unserem Fall Integer (also 4 Byte). Ich werde übrigens überall in der Datei Integer verwenden. Wenn ihr sehr große Dateien bei eurem Spiel erwartet, dann könnt ihr das auch je nach Bedarf zu 2 oder 1 Byte ändern. Geht dabei aber mit Bedacht vor, hab ihr euch einmal entschieden, gibt es kein Zurück, ändert ihr es so müsst ihr alle Dateien aufwändig konvertieren, oder sie werden unbrauchbar. Das genaue Pattern ist:

      Quellcode

      1. MapBreite | MapHöhe | StartPositionX | StartPositionY

      Im zweiten Teil werden die Tiles gespeichert. Um Speicherplatz zu sparen speichere ich nur die Tiles, die auch tatsächlich belegt sind. Tiles mit der ID 0 (also Luft) werden nicht gespeichert sondern nachher vom Programm automatisch generiert. Für einen einzelnen Tile sieht das Pattern so aus:

      Quellcode

      1. PositionX | PositionY | ID
      Da wir wissen, wie lang ein Tile ist (12 Byte bzw 4 Integer), können wir die Tiles ohne Probleme hintereinander in die Datei schreiben. Der Integer nach der ID von Tile 1 ist also die PositionX von Tile 2:

      Quellcode

      1. PositionX1 | PositionY1 | ID1 | PositionX2 | PositionY2 | ID2 | ... | PositionXn | PositionYn | IDn

      Das komplette Pattern für die Datei sieht damit so aus:

      Quellcode

      1. MapBreite | MapHöhe | StartPositionX | StartPositionY | PositionX1 | PositionY1 | ID1 | PositionX2 | PositionY2 | ID2 | ... | PositionXn | PositionYn | IDn



      Das wars auch schon, jetzt können wir mit dem eigentlichen Programmieren anfangen.
      Um die Datei auszulesen verwenden wir am besten einen BinaryReader, denn wir speichern unsere Daten ja auch binär. Nun sieht es schon mal so aus:
      VB

      VB.NET-Quellcode

      1. Public Sub New(path As String)
      2. Dim fi = New FileInfo(path)
      3. Using fs = fi.OpenRead()
      4. Using br = New BinaryReader(fs)
      5. End Using
      6. End Using
      7. End Sub
      C#

      C-Quellcode

      1. public Map(string path)
      2. {
      3. var fi = new FileInfo(path);
      4. using (var fs = fi.OpenRead())
      5. {
      6. using (var br = new BinaryReader(fs))
      7. {
      8. }
      9. }
      10. }

      Den ersten Teil der Datei können wir sehr einfach auslesen. Einfach 4 mal ReadInt32() aufgerufen und wir haben die ersten 4 Integer aus der Datei. Diese können wir dann auch gleich auswerten.
      VB

      VB.NET-Quellcode

      1. Public Sub New(path As String)
      2. Dim fi = New FileInfo(path)
      3. Using fs = fi.OpenRead()
      4. Using br = New BinaryReader(fs)
      5. _width = br.ReadInt32()
      6. _height = br.ReadInt32()
      7. _player = New Player(br.ReadInt32(), br.ReadInt32())
      8. tiles = New Tile(_width - 1, _height - 1) {}
      9. End Using
      10. End Using
      11. End Sub
      C#

      C-Quellcode

      1. public Map(string path)
      2. {
      3. var fi = new FileInfo(path);
      4. using (var fs = fi.OpenRead())
      5. {
      6. using (var br = new BinaryReader(fs))
      7. {
      8. _width = br.ReadInt32();
      9. _height = br.ReadInt32();
      10. _player = new Player(br.ReadInt32(), br.ReadInt32());
      11. tiles = new Tile[_width, _height];
      12. }
      13. }
      14. }

      Jetzt bleiben nur noch die Tiles. Dafür müssen wir solange 3 Integer lesen, bis nicht mehr genug Bytes für 3 Integer in der Datei übrig sind. Man könnte auch einfach lesen, bis die Datei zu Ende ist, aber ich gehe hier lieber auf Nummer sicher. Das lässt sich ganz einfach mit einer Schleife lösen. Die 12 kommt daher, dass die Angaben in Bytes sind und ein Integer 4 Bytes besitzt, also 3 * 4 = 12.
      VB

      VB.NET-Quellcode

      1. Public Sub New(path As String)
      2. Dim fi = New FileInfo(path)
      3. Using fs = fi.OpenRead()
      4. Using br = New BinaryReader(fs)
      5. _width = br.ReadInt32()
      6. _height = br.ReadInt32()
      7. _player = New Player(br.ReadInt32(), br.ReadInt32())
      8. tiles = New Tile(_width - 1, _height - 1) {}
      9. Do While fs.Length - fs.Position >= 12
      10. Dim posX As Integer = br.ReadInt32()
      11. Dim posY As Integer = br.ReadInt32()
      12. tiles(posX, posY) = New Tile(br.ReadInt32(), posX, posY)
      13. Loop
      14. End Using
      15. End Using
      16. End Sub
      C#

      C-Quellcode

      1. public Map(string path)
      2. {
      3. var fi = new FileInfo(path);
      4. using (var fs = fi.OpenRead())
      5. {
      6. using (var br = new BinaryReader(fs))
      7. {
      8. _width = br.ReadInt32();
      9. _height = br.ReadInt32();
      10. _player = new Player(br.ReadInt32(), br.ReadInt32());
      11. tiles = new Tile[_width, _height];
      12. while (fs.Length - fs.Position >= 12)
      13. {
      14. int posX = br.ReadInt32();
      15. int posY = br.ReadInt32();
      16. tiles[posX, posY] = new Tile(br.ReadInt32(), posX, posY);
      17. }
      18. }
      19. }
      20. }

      Jetzt dürfen wir aber nicht vergessen, dass wir noch alle restlichen Tiles, die nicht in der Datei gespeichert waren, mit Luft (also Tiles der ID 0) gefüllt werden müssen. Dafür gehen wir alle Arrayelemente durch und prüfen, ob diese Nothing/Null sind, und erstellen dann gegebenenfalls die Tiles:
      VB

      VB.NET-Quellcode

      1. Public Sub New(path As String)
      2. Dim fi = New FileInfo(path)
      3. Using fs = fi.OpenRead()
      4. Using br = New BinaryReader(fs)
      5. _width = br.ReadInt32()
      6. _height = br.ReadInt32()
      7. _player = New Player(br.ReadInt32(), br.ReadInt32())
      8. tiles = New Tile(_width - 1, _height - 1) {}
      9. While fs.Length - fs.Position >= 12
      10. Dim posX As Integer = br.ReadInt32()
      11. Dim posY As Integer = br.ReadInt32()
      12. tiles(posX, posY) = New Tile(br.ReadInt32(), posX, posY)
      13. End While
      14. For x As Integer = 0 To _width - 1
      15. For y As Integer = 0 To _height - 1
      16. If tiles(x, y) Is Nothing Then
      17. tiles(x, y) = New Tile(0, x, y)
      18. End If
      19. Next
      20. Next
      21. End Using
      22. End Using
      23. End Sub
      C#

      C-Quellcode

      1. public Map(string path)
      2. {
      3. var fi = new FileInfo(path);
      4. using (var fs = fi.OpenRead())
      5. {
      6. using (var br = new BinaryReader(fs))
      7. {
      8. _width = br.ReadInt32();
      9. _height = br.ReadInt32();
      10. _player = new Player(br.ReadInt32(), br.ReadInt32());
      11. tiles = new Tile[_width, _height];
      12. while (fs.Length - fs.Position >= 12)
      13. {
      14. int posX = br.ReadInt32();
      15. int posY = br.ReadInt32();
      16. tiles[posX, posY] = new Tile(br.ReadInt32(), posX, posY);
      17. }
      18. for (int x = 0; x < _width; x++)
      19. {
      20. for (int y = 0; y < _height; y++)
      21. {
      22. if (tiles[x, y] == null)
      23. tiles[x, y] = new Tile(0, x, y);
      24. }
      25. }
      26. }
      27. }
      28. }

      Damit wäre das Einlesen auch schon geschafft, ihr seht also, gar nicht so viel Code.

      Jetzt haben wir die Daten im Programm, wir wollen sie von dort aber auch wieder zurück in eine Datei bekommen.
      Dafür gehen wir jetzt den umgekehrten Weg. Statt einem StreamReader nehmen wir diesmal einen StreamWriter, denn wir wollen ja was in die Datei schreiben. Dann können wir den ersten Teil wieder direkt erstellen, indem wir 4 mal Write() aufrufen. Die Tiles müssen wir wieder mit einer Schleife durchgehen und alle Tiles, die eine ID <> 0 besitzen, speichern wir. Die Save-Methode sieht demnach dann so aus:
      VB

      VB.NET-Quellcode

      1. Public Sub Save(path As String)
      2. Dim fi = New FileInfo(path)
      3. Using fs = fi.Open(FileMode.Create, FileAccess.Write)
      4. Using bw = New BinaryWriter(fs)
      5. bw.Write(_width)
      6. bw.Write(_height)
      7. bw.Write(_player.X)
      8. bw.Write(_player.Y)
      9. For x As Integer = 0 To _width - 1
      10. For y As Integer = 0 To _height - 1
      11. If tiles(x, y).ID <> 0 Then
      12. bw.Write(tiles(x, y).X)
      13. bw.Write(tiles(x, y).Y)
      14. bw.Write(tiles(x, y).ID)
      15. End If
      16. Next
      17. Next
      18. End Using
      19. End Using
      20. End Sub
      C#

      C-Quellcode

      1. public void Save(string path)
      2. {
      3. var fi = new FileInfo(path);
      4. using (var fs = fi.Open(FileMode.Create, FileAccess.Write))
      5. {
      6. using (var bw = new BinaryWriter(fs))
      7. {
      8. bw.Write(_width);
      9. bw.Write(_height);
      10. bw.Write(_player.X);
      11. bw.Write(_player.Y);
      12. for (int x = 0; x < _width; x++)
      13. {
      14. for (int y = 0; y < _height; y++)
      15. {
      16. if (tiles[x, y].ID != 0)
      17. {
      18. bw.Write(tiles[x, y].X);
      19. bw.Write(tiles[x, y].Y);
      20. bw.Write(tiles[x, y].ID);
      21. }
      22. }
      23. }
      24. }
      25. }
      26. }



      Damit geht auch dieses Tutorial zu Ende. Ich hoffe ich konnte euch alles gut vermitteln, bei Fragen stehe ich gerne zur Verfügung.
      Anbei findet ihr auch noch die originale C#-Projektmappe.
      Dateien
      • SimpleTileMap.rar

        (22,09 kB, 300 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Artentus“ ()

      Ist schon sehr gut erklärt vielen Dank aber irgendwie bin ich ja noch Anfänger :thumbsup:

      Edit:// Ich verstehe den Code nicht, er macht ja für mich kein Sinn..

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „GirlOnFire“ ()

      Ich fand/finde das Tutorial hervorragend und auch für Anfänger geeignet.
      Vorfallendem weil du eine einfache Variante zur Speicherung von der (Tile-)Map genommen hast, man hätte auch die Huffman-Kodierung nehmen können, das wäre aber in bisschen Overkill bei den kleinen Datenmengen(16 Bytes + 12 Bytes pro Tile bei diesem Beispiel) und den heutigen Festplattengrößen.
      "Zwei Dinge sind unendlich, das Universum und die menschliche Dummheit, aber bei dem Universum bin ich mir noch nicht ganz sicher." Albert Einstein
      @ErfinderDesRades
      Ich war der Meinung, der Sinn einer Tilemap sei klar.
      Falls das nicht der Fall ist: bei vielen 2D-Spielen besteht die Welt aus ganz vielen Quadraten, die nur jeweils eine andere Textur haben. Ein solches Quadrat nennt man "Tile".
      Ein Beispiel wäre [Release] Lumium | Jump 'n Run.
      Sehr schön gemacht :thumbsup:.
      Allerdings gibt es für mich ein paar Kritikpunkte:
      -Man kann keine "neue" Map erstellen (-> neue/zusätzliche Sub New)
      -Jedes Tile hat die Eigenschaften X und Y - wofür?
      -Die jetzige Sub New bei der Map solte eher eine Shared Funktion sein (-> Map.FromFile...)
      Außerdem hast du in der Map.Save-Methode, Zeile 13 X statt Y stehen.

      Thorstian schrieb:

      kann aber leider das projekt bei mir nicht öffnen
      Das Testprojekt ist in C#, ich hab den Code nur extra für hier nach VB übersetzt. Du brauchst also C# Express oder eine Vollversion von Visual Studio, um es zu öffnen. Allerdings steht da eh nicht mehr drin, als hier, also kannst du dir so ein Projekt auch ganz einfach selbst durch C&P des Codes erstellen.

      nafets3646 schrieb:

      Man kann keine "neue" Map erstellen (-> neue/zusätzliche Sub New)
      Es ging mir hier auch ausschließlich ums Laden/Speichern, wie man die Klassen jetzt noch erweitert steht ja jedem frei.

      nafets3646 schrieb:

      Jedes Tile hat die Eigenschaften X und Y - wofür?
      Das kann schon ganz hilfreich sein, wenn man z.B. die umgebenden Tiles eines Tile prüfen möchte, aber auch das steht natürlich jedem frei zu tun oder nicht.

      nafets3646 schrieb:

      Die jetzige Sub New bei der Map solte eher eine Shared Funktion sein (-> Map.FromFile...)
      Das kann man auch machen wie man will, ich finde einen Konstruktor aber schöner.

      nafets3646 schrieb:

      Außerdem hast du in der Map.Save-Methode, Zeile 13 X statt Y stehen.
      Danke, hab ich ausgebessert.