iOS-Bibliothek zur Darstellung von TMX-Dateien

Mit dem freien Mapeditor Tiled, gibt es ein sehr mächtiges Werkzeug für die Erstellung und Bearbeitung von Karten. Nachdem eine Karte erstellt wurde und diese im TMX-Format gesichert wurde, soll die Karte in den meisten Fällen genutzt werden. Für die unterschiedlichen Systeme existieren eine Reihe von Bibliotheken zur Nutzung und Darstellung von TMX-Karten.

Ein Demo zur Darstellung einer TMX-Karte im Simulator

Unter iOS und für die Nutzung mittels Swift eignet sich die Bibliothek SKTiled, welche auf GitHub zu finden ist. Mit SKTiled können die unterschiedlichen Varianten von TMX-Dateien, wie isometrische oder rechteckige Maps angezeigt werden. Neben den normalen Tiles, werden auch animierte Tiles in der Anzeige unterstützt. Die Bibliothek funktioniert unter iOS, tvOS und macOS. Lizenziert ist die Bibliothek unter der MIT-Lizenz und damit freie Software.

TMX-Support unter Unity 3D

Seit vielen Jahren gibt es unter mapeditor.org den freien Karteneditor Tiled zum Download. Mittlerweile hat sich der hauptsächlich von Thorbjørn Lindeijer entwickelte Editor zum besten kachelbasierten Editor auf dem Markt gemausert. Auf der Wiki zum Editor, gibt es eine Seite welche die Unterstützung des TMX-Dateiformates, welches Tiled für die Speicherung der Karten nutzt, in anderen Sprachen und Entwicklungsumgebungen aufzählt.

Eine einfache Karte unter Tiled

Eine einfache Karte unter Tiled

In diesem Artikel möchte ich auf die TMX-Unterstützung in Unity 3D eingehen und die einzelnen Frameworks behandeln. Unity 3D als solches bringt keinerlei Unterstützung für das TMX-Format mit. Mit Orthello Pro gibt es ein 2D-Framework für Unity welches das TMX-Format in Form seiner TileMap-Komponente unterstützt. Im Prinzip funktioniert diese Unterstützung ohne weitere Probleme – allerdings gibt es einige Kleinigkeiten, welche mich an dieser Variante stören. So sind die Kollisionen unsauber, das bedeutet das man Objekte z.B. von der Seite betreten kann, obwohl sie mit Kollisionen ausgestattet sind (an dieser Stelle kann es natürlich sein, das ich den Fehler zu vertreten habe, ich lasse mich gerne vom Gegenteil überzeugen). Daneben wird Orthello in letzter Zeit nicht mehr so aktiv gepflegt wie man sich dies von einem kommerziellen Produkt wünschen würde. Bei größeren Karten kommt außerdem das Problem dazu, das sobald man die Layer-Eigenschaften öffnet, Unity nicht mehr zu bedienen ist, da es durch die Darstellung jeder einzelnen Kacheleigenschaft überfordert ist.

Die TileMap Komponente von Orthello Pro

Die TileMap Komponente von Orthello Pro

Die nächste Lösung ist Tiled Tilemaps, welche unter dem Problem krankt, das Sie nicht mehr aktiv weiter entwickelt wird, und bedingt dadurch TMX-Dateien unter aktuellen Unity-Versionen nicht mehr geöffnet werden können. Bei der Tiled to Unity Komponente handelt es sich im engeren Sinne nicht um eine TMX-Unterstützung, sondern um ein Workflow-Tool um TMX-Karten in 3D-Welten umzuwandeln, weshalb es im Rahmen dieses Artikels nicht weiter behandelt wird.

Eine weitere Lösung ist Tiled2Unity, welche TMX-Dateien in Unity in ein Mesh exportiert und im Moment für Windows und Mac OS X funktioniert und aktiv weiterentwickelt wird.

Die letzte Lösung (welche hier besprochen werden soll) für TMX-Unterstützung unter Unity ist UniTMX bzw. da UniTMX seit zwei Jahren nicht mehr weiterentwickelt wird, dessen Fork X-UniTMX, bei welchem es sich um freie Software handelt. X-UniTMX wird mit einer Reihe von Beispielen geliefert und unterstützt unter anderen isometrische Karten. Damit wäre es im Moment meine erste Wahl für die TMX-Unterstützung unter Unity 3D.

melonJS ohne preload benutzen

Bei melonJS handelt es sich um eine Javascript Engine zur Spieleentwicklung im Browser mittels HTML5. Möchte man dort ein Level laden, so geschieht das in Form einer TMX Datei. In melonJS gibt es dabei zwei Methoden zum laden der entsprechenden Daten. Diese sind “me.loader.preload” und “me.loader.load”. Alle Beispiele welche man im Netz so findet nutzen dabei immer die “preload” Methode:

var g_resources= [
{ name: "desert1",          type: "image", src: "desert1.png" },
{ name: "desert",           type: "tmx",   src: "desert.tmx" },
{ name: "player_male_base", type: "image", src: "player_male_base.png" },
{ name: "fog",              type: "image", src: "fog.png" }
];

...

me.loader.preload(g_resources);

Das Problem ist das man bei größeren Spielen mit ein paar hundert MiB Spieldaten, das ganze schlecht komplett in den Speicher laden kann. Hierfür gibt es die “load” Methode. Allerdings enthält diese einige Fehler welche das ganze erschweren. Die “preload” Methode trägt jede TMX Datei in das Array “levels” im “me.levelDirector” ein. Bei der “load” Methode passiert genau dies nicht. Deshalb muss man hier anders vorgehen (bis der Fehler behoben ist). Ein Minimalbeispiel zum laden eines Levels sieht damit so aus:

<!DOCTYPE html>
<html>
  <head>
    <title>melonJS minimal sample</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <script type="text/javascript" src="melonJS.js"></script>

    <script type="text/javascript">
      var jsApp = {
        start: function() {
          if (!me.video.init('jsapp', 640, 480, false, '1', false))
          {
            alert("Sorry but your browser does not support html 5 canvas.");
            return;
          }

          me.loader.onload = this.loaded.bind(this);

          //lade manuell
          me.loader.load({name: "desert1",  type:"image",  src: "desert1.png"}, this.onload);
          me.loader.load({name: "desert",  type:"tmx",  src: "desert.tmx"}, this.onload);

          //zeige Ladebildschirm
          me.state.change(me.state.LOADING);
        },

        onload: function(data) {
          me.levelDirector.addTMXLevel("desert"); //Bugfix
          me.levelDirector.loadLevel("desert");
        },

        loaded: function() {
          me.state.set(me.state.PLAY, new PlayScreen());
          me.state.change(me.state.PLAY);
        }
      }

      var PlayScreen = me.ScreenObject.extend({
      });

      //starte wenn der Browser bereit ist
      window.onReady(function() {
        jsApp.start();
      });
    </script>
  </body>
</html>

Damit lädt man ein Level dynamisch, ohne auf die “preload” Methode angewiesen zu sein. Möglich wird dies durch den kleinen Bugfix in der “onload” Methode:

me.levelDirector.addTMXLevel("desert"); //Bugfix

Dieser sorgt dafür das der levelDirector über die entsprechende TMX Datei informiert wird und somit die Datei beim laden des Levels auch findet.

Das TMX Format

Auf der Website http://www.mapeditor.org gibt es einen freien Tilebasierten Mapeditor names Tiled. Der Editor wurde in Java geschrieben und wird unter anderem von Projekten wie The Mana World (http://www.themanaworld.org) benutzt.

Das Standardformat dieses Editores ist TMX. Dabei handelt es sich um eine XML Datei in der die Daten kodiert sind. Vor einiger Zeit war ich damit beschäftigt einen Reader für das Format zu schreiben. Leider ist die Dokumentation des Formates sehr lückenhaft, aber dank einiger tatkräftiger Hilfe auf der Mailingliste war der Reader nach einer Weile dann doch fertig. Doch nun zum TMX Format. Eine TMX Datei könnte z.B. so aussehen: ow-testmap.tmx

Als erstes kommt der map Tag in dem alle anderen tags untergeordnet sind. Der erste Tag von Interesse ist dann der tileset Tag. In diesem Tag stehen der Name des Tilesets, die FirstGID (dazu später mehr), die Breite und Höhe der Tiles sowie das Quellbild des Tilesets.

Nachdem die Tilesets definiert sind beginnen auch schon die Layer Definitionen. Eine Karte kann aus mehreren Layern bestehen z.B. einem Kollisionlayer. Im Layer Tag stehen der Name, die Breite und Höhe des Layers. Darunter folgt bei komprimierten Karten (die Standardeinstellung von Tiled) der Tag data mit dem Attribut encoding. In unserem Beispiel ist die Kodierung Base64 und die Kompression gzip. Um an die Daten des Layers zu kommen muss man den von den Data Tags eingeschlossen String nehmen und mittels Base64 decodieren. Anschließend erhält man ein Byte Array. Dieses Byte Array muss dann noch mittels des gzip Algorithmus dekomprimiert werden. Bei dieser Aktion kommt am Ende auch wieder ein Byte Array heraus.

In diesem Byte Array stehen dann die Tilenummern drin und zwar in Form von 4 Byte Integers (Little Endian). Hat man z.B. eine 2×2 Karte so würden die Tiles in folgender Reihenfolge kommen: 0,0 – 0,1 – 1,0 – 1,1.

Nun stellt sich nur noch die Frage wie man diese Tilenummern interpretiert. Und dabei kommt jetzt die FirstGID des Tileset zum tragen. Nehmen wir an das wir die Tilenummer 270 auslesen. Um nun zu ermitteln zu welchem Tileset die Nummer gehört prüfen wir in welches Tileset in diesem Bereich liegt und stellen fest das das Tileset desert2 genau in diesem Bereich liegt. Dann rechnet man noch Tilesetnummer – FirstGID und schon hat man die Tilesetnummer und das Tileset (in diesem Fall 11). Nun wissen wir das das Tile das elfte Tile im Tileset desert2 ist.