Analyse et rendu de cartes au format TMX en mosaïque dans votre propre moteur de jeu

Dans mon article précédent, nous avions examiné Tiled Map Editor comme un outil permettant de créer des niveaux pour vos jeux. Dans ce tutoriel, je vais vous guider dans l'étape suivante: analyser et rendre ces cartes dans votre moteur..

Remarque: Bien que ce tutoriel soit écrit en utilisant Flash et AS3, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Exigences

  • Version en mosaïque 0.8.1: http://www.mapeditor.org/
  • Carte TMX et tileset d'ici. Si vous avez suivi mon tutoriel d’introduction à Tiled, vous devriez déjà avoir ces.

Enregistrement au format XML

En utilisant la spécification TMX, nous pouvons stocker les données de différentes façons. Pour ce tutoriel, nous enregistrerons notre carte au format XML. Si vous envisagez d’utiliser le fichier TMX inclus dans la section Exigences, vous pouvez passer à la section suivante..

Si vous avez créé votre propre carte, vous devrez dire à Tiled de l'enregistrer au format XML. Pour ce faire, ouvrez votre carte avec mosaïque, puis sélectionnez Edition> Préférences….

Pour la liste déroulante "Stocker les données de la couche de carreaux en tant que:", sélectionnez XML, comme indiqué dans l'image ci-dessous:

Maintenant, lorsque vous enregistrez la carte, elle sera stockée au format XML. N'hésitez pas à ouvrir le fichier TMX avec un éditeur de texte pour jeter un coup d'oeil à l'intérieur. Voici un extrait de ce que vous pouvez vous attendre à trouver:

                      

Comme vous pouvez le constater, il stocke simplement toutes les informations de la carte dans ce format XML très pratique. La plupart des propriétés doivent être simples, à l’exception de gid - J'expliquerai cela plus en détail plus tard dans le tutoriel..

Avant de poursuivre, je voudrais attirer votre attention sur la groupe d'objets "Collision"élément. Comme vous vous en souviendrez peut-être dans le tutoriel sur la création de cartes, nous avons spécifié la zone de collision autour de l’arbre;.

Vous pouvez spécifier les power-ups ou les points d'apparition de joueur de la même manière, de sorte que vous puissiez imaginer le nombre de possibilités offertes pour Tiled en tant qu'éditeur de carte.!


Contour de base

Maintenant, voici un bref aperçu de la manière dont nous allons intégrer notre carte au jeu:

  1. Lire dans le fichier TMX.
  2. Analyser le fichier TMX en tant que fichier XML.
  3. Charger toutes les images du tileset.
  4. Organisez les images du jeu de mosaïques dans notre structure de carte, couche par couche..
  5. Lire l'objet de la carte.

Lecture dans le fichier TMX

En ce qui concerne votre programme, il s’agit d’un fichier XML. La première chose à faire est de le lire. La plupart des langues disposent d’une bibliothèque XML pour cela; dans le cas d'AS3, j'utiliserai la classe XML pour stocker les informations XML et un URLLoader pour lire le fichier TMX.

 xmlLoader = new URLLoader (); xmlLoader.addEventListener (Event.COMPLETE, xmlLoadComplete); xmlLoader.load (new URLRequest ("… /assets/example.tmx"));

Ceci est un lecteur de fichier simple pour "… /Assets/example.tmx". Cela suppose que le fichier TMX se trouve dans le répertoire de votre projet, dans le dossier "assets". Nous avons juste besoin d’une fonction à gérer lorsque la lecture du fichier est terminée:

 fonction privée xmlLoadComplete (e: Event): void xml = new XML (e.target.data); mapWidth = xml.attribute ("width"); mapHeight = xml.attribute ("height"); tileWidth = xml.attribute ("tilewidth"); tileHeight = xml.attribute ("tileheight"); var xmlCounter: uint = 0; pour chaque (var tileset: XML dans xml.tileset) var imageWidth: uint = xml.tileset.image.attribute ("width") [xmlCounter]; var imageHeight: uint = xml.tileset.image.attribute ("height") [xmlCounter]; var firstGid: uint = xml.tileset.attribute ("firstgid") [xmlCounter]; var tilesetName: String = xml.tileset.attribute ("name") [xmlCounter]; var tilesetTileWidth: uint = xml.tileset.attribute ("tilewidth") [xmlCounter]; var tilesetTileHeight: uint = xml.tileset.attribute ("tileheight") [xmlCounter]; var tilesetImagePath: String = xml.tileset.image.attribute ("source") [xmlCounter]; tileSets.push (nouveau TileSet (firstGid, tilesetName, tilesetTileWidth, tilesetTileHeight, tilesetImagePath, imageWidth, imageHeight)); xmlCounter ++;  totalTileSets = xmlCounter; 

C'est ici que se déroule l'analyse initiale. (Il y a quelques variables que nous garderons en dehors de cette fonction puisque nous les utiliserons plus tard.)

Une fois que les données de la carte sont stockées, nous passons à l'analyse de chaque jeu de mosaïques. J'ai créé une classe pour stocker les informations de chaque tileset. Nous allons placer chacune de ces instances de classe dans un tableau, car nous les utiliserons plus tard:

 classe publique TileSet public var firstgid: uint; public var lastgid: uint; nom de la variable publique: String; public var tileWidth: uint; source publique var: String; public var tileHeight: uint; public var imageWidth: uint; public var imageHeight: uint; public var bitmapData: BitmapData; public var tileAmountWidth: uint; fonction publique TileSet (firstgid, name, tileWidth, tileHeight, source, imageWidth, imageHeight) this.firstgid = firstgid; this.name = name; this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.source = source; this.imageWidth = imageWidth; this.imageHeight = imageHeight; tileAmountWidth = Math.floor (imageWidth / tileWidth); lastgid = tileAmountWidth * Math.floor (imageHeight / tileHeight) + firstgid - 1; 

Encore une fois, vous pouvez voir que gid apparaît à nouveau, dans le Firstid et Lastgid variables. Voyons maintenant à quoi cela sert.


Compréhension "gid"

Pour chaque mosaïque, nous devons l’associer d’une manière ou d’une autre à un groupe de mosaïques et à un emplacement particulier de ce groupe de mosaïques. C'est le but de la gid.

Regarde le grass-tiles-2-small.png tileset. Il contient 72 tuiles distinctes:

Nous donnons à chacune de ces tuiles une pièce unique gid de 1-72, de sorte que nous pouvons nous référer à une personne avec un seul numéro. Cependant, le format TMX ne spécifie que le premier gid du tileset, puisque tous les autres gids peut être dérivé de la connaissance de la taille du jeu de carreaux et de la taille de chaque carreau.

Voici une image pratique pour vous aider à visualiser et à expliquer le processus..

Donc, si nous plaçons la tuile en bas à droite de cet ensemble de tuiles sur une carte quelque part, nous stockons le gid 72 à cet endroit sur la carte.

Maintenant, dans l’exemple de fichier TMX ci-dessus, vous remarquerez que tree2-final.png a un Firstid de 73. C'est parce que nous continuons à compter sur la gids, et nous ne le réinitialisons pas à 1 pour chaque jeu de tuiles.

En résumé, un gid est un identifiant unique attribué à chaque mosaïque de chaque mosaïque dans un fichier TMX, en fonction de la position de la mosaïque dans le mosaïque et du nombre de mosaïques mentionnés dans le fichier TMX..


Chargement des tuiles

Nous voulons maintenant charger en mémoire toutes les images sources du jeu de mosaïques afin de pouvoir assembler notre carte. Si vous n'écrivez pas ceci dans AS3, la seule chose que vous devez savoir, c'est que nous chargeons les images pour chaque ensemble de pavés ici:

 // charge les images pour tileset pour (var i = 0; i < totalTileSets; i++)  var loader = new TileCodeEventLoader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, tilesLoadComplete); loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, progressHandler); loader.tileSet = tileSets[i]; loader.load(new URLRequest("… /assets/" + tileSets[i].source)); eventLoaders.push(loader); 

Il se passe ici quelques choses spécifiques à AS3, telles que l’utilisation de la classe Loader pour importer les images du jeu de mosaïques. (Plus précisément, il s’agit d’une extension Chargeur, simplement pour que nous puissions stocker le TileSet instances à l'intérieur de chaque Chargeur. C’est pour que lorsque le chargeur est terminé, nous pouvons facilement corréler le chargeur avec le jeu de mosaïques.)

Cela peut paraître compliqué, mais le code est vraiment très simple:

 Classe publique TileCodeEventLoader. Loader public var tileSet: TileSet; 

Maintenant, avant de commencer à prendre ces ensembles de tuiles et à créer la carte avec eux, nous devons créer une image de base pour les mettre:

 screenBitmap = new Bitmap (new BitmapData (mapWidth * tileWidth, mapHeight * tileHeight, false, 0x22ffff)); screenBitmapTopLayer = new Bitmap (new BitmapData (mapWidth * tileWidth, mapHeight * tileHeight, true, 0));

Nous allons copier les données de mosaïque sur ces images bitmap afin de pouvoir les utiliser comme arrière-plan. La raison pour laquelle j'ai configuré deux images est que nous pouvons avoir un calque supérieur et un calque inférieur, et que le lecteur se déplace entre eux afin de fournir une perspective. Nous spécifions également que la couche supérieure doit avoir un canal alpha.

Pour les écouteurs d'événements réels pour les chargeurs, nous pouvons utiliser ce code:

 fonction privée progressHandler (event: ProgressEvent): void trace ("progressHandler: bytesLoaded =" + event.bytesLoaded + "bytesTotal =" + event.bytesTotal); 

C’est une fonction amusante, car vous pouvez suivre l’état de chargement de l’image et ainsi donner à l’utilisateur un retour d’information sur la rapidité des choses, comme une barre de progression..

 fonction privée tilesLoadComplete (e: Event): void var currentTileset = e.target.loader.tileSet; currentTileset.bitmapData = Bitmap (e.target.content) .bitmapData; tileSetsLoaded ++; // attend jusqu'à ce que toutes les images du tileset soient chargées avant de les combiner couche par couche en un seul bitmap si (tileSetsLoaded == totalTileSets) addTileBitmapData (); 

Ici, nous stockons les données bitmap avec le tileset associé. Nous comptons également le nombre de jeux de carreaux complètement chargés et, une fois terminé, nous pouvons appeler une fonction (je l’ai nommée addTileBitmapData dans ce cas) pour commencer à assembler les pièces.


Combiner les tuiles

Pour combiner les mosaïques en une seule image, nous souhaitons la construire couche par couche afin qu'elle soit affichée de la même manière que la fenêtre d'aperçu dans Mosaïque..

Voici à quoi ressemblera la fonction finale; Les commentaires que j'ai inclus dans le code source devraient expliquer de manière adéquate ce qui se passe sans trop de confusion dans les détails. Je dois noter que cela peut être mis en œuvre de différentes manières et que votre mise en œuvre peut être complètement différente de la mienne..

 fonction privée addTileBitmapData (): void // charge chaque couche pour chaque couche (var layer: XML dans xml.layer) var tiles: Array = new Array (); var tileLength: uint = 0; // assigne le gid à chaque emplacement de la couche pour chaque (var tile: XML dans layer.data.tile) var gid: Number = tile.attribute ("gid"); // si gid> 0 si (gid> 0) tiles [tileLength] = gid;  tileLength ++;  // la boucle externe for continue dans les prochains extraits

Qu'est-ce qui se passe ici est que nous analysons uniquement les carreaux avec gids sont supérieurs à 0, car 0 indique une mosaïque vide et les stocke dans un tableau. Comme il y a tellement de "0 tuiles" dans notre couche supérieure, il serait inefficace de toutes les stocker en mémoire. Il est important de noter que nous stockons l'emplacement du gid avec un compteur parce que nous utiliserons son index dans le tableau plus tard.

 var useBitmap: BitmapData; var layerName: String = layer.attribute ("name") [0]; // décide où nous allons placer la couche var layerMap: int = 0; switch (layerName) case "Top": layerMap = 1; Pause; défaut: trace ("using layer layer"); 

Dans cette section, nous analysons le nom de la couche et vérifions s'il est égal à "Top". Si c'est le cas, nous définissons un indicateur pour que nous sachions le copier sur la couche bitmap supérieure. Nous pouvons être vraiment flexibles avec de telles fonctions et utiliser encore plus de couches disposées dans n'importe quel ordre..

 // stocke le gid dans un tableau 2d var tileCoordinates: Array = new Array (); pour (var tileX: int = 0; tileX < mapWidth; tileX++)  tileCoordinates[tileX] = new Array(); for (var tileY:int = 0; tileY < mapHeight; tileY++)  tileCoordinates[tileX][tileY] = tiles[(tileX+(tileY*mapWidth))];  

Maintenant, nous entreposons le gid, que nous avons analysé au début, dans un tableau 2D. Vous remarquerez les initialisations de double tableau; c'est simplement une façon de gérer les tableaux 2D en AS3.

Il y a un peu de maths en cours aussi. Rappelez-vous quand nous avons initialisé le carrelage tableau d'en haut, et comment avons-nous gardé l'index avec? Nous allons maintenant utiliser l’index pour calculer la coordonnée que gid appartient à. Cette image montre ce qui se passe:

Donc, pour cet exemple, nous obtenons le gid à l'index 27 dans le carrelage tableau, et le stocker à tileCoordinates [7] [1]. Parfait!

 pour (var spriteForX: int = 0; spriteForX < mapWidth; spriteForX++)  for (var spriteForY:int = 0; spriteForY < mapHeight; spriteForY++)  var tileGid:int = int(tileCoordinates[spriteForX][spriteForY]); var currentTileset:TileSet; // only use tiles from this tileset (we get the source image from here) for each( var tileset1:TileSet in tileSets)  if (tileGid >= tileset1.firstgid-1 && tileGid // nous avons trouvé le bon jeu de carreaux pour ce gid! currentTileset = tileset1; Pause;  var destY: int = spriteForY * tileWidth; var destX: int = spriteForX * tileWidth; // mathématiques de base pour savoir d'où vient la mosaïque sur l'image source tileGid - = currentTileset.firstgid -1; var sourceY: int = Math.ceil (tileGid / currentTileset.tileAmountWidth) -1; var sourceX: int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1; // copie la mosaïque du pavé de tuiles sur notre bitmap if (layerMap == 0) screenBitmap.bitmapData.copyPixels (currentTileset.bitmapData, new Rectangle (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset. tileHeight), nouveau Point (destX, destY), null, null, true);  else if (layerMap == 1) screenBitmapTopLayer.bitmapData.copyPixels (currentTileset.bitmapData, new Rectangle (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileHeight), nouveau Point ), null, null, true); 

C’est là que nous avons enfin commencé à copier le jeu de mosaïques sur notre carte..

Au début, nous commençons par parcourir chaque coordonnée de la mosaïque sur la carte. gid et recherchez le jeu de mosaïque stocké qui lui correspond, en vérifiant s'il se trouve entre le Firstid et notre calculé Lastgid.

Si vous avez compris le Compréhension "gid" section d'en haut, ce calcul devrait avoir un sens. Dans les termes les plus élémentaires, cela prend la coordonnée de mosaïque sur le jeu de mosaïques (sourceX et source) et en le copiant sur notre carte à l'emplacement de la tuile auquel nous avons bouclé (destX et desty).

Enfin, à la fin, nous appelons le copyPixel fonction pour copier l'image en mosaïque sur la couche supérieure ou la couche de base.


Ajout d'objets

Maintenant que la copie des couches sur la carte est terminée, examinons le chargement des objets de collision. Ceci est très puissant car, en plus de l'utiliser pour les objets de collision, nous pouvons également l'utiliser pour tout autre objet, tel qu'un power-up ou un emplacement de lancement du joueur, tant que nous l'avons spécifié avec Tiled.

Donc au bas de la addTileBitmapData fonction, mettons dans le code suivant:

 pour chaque (var objectgroup: XML dans xml.objectgroup) var objectGroup: String = objectgroup.attribute ("name"); switch (objectGroup) case "Collision": pour chaque (objet var: XML dans objectgroup.object) var rectangle: Shape = new Shape (); rectangle.graphics.beginFill (0x0099CC, 1); rectangle.graphics.drawRect (0, 0, object.attribute ("width"), object.attribute ("height")); rectangle.graphics.endFill (); rectangle.x = object.attribute ("x"); rectangle.y = object.attribute ("y"); collisionTiles.push (rectangle); addChild (rectangle);  Pause; default: trace ("type d'objet non reconnu:", objectgroup.attribute ("name")); 

Cela va parcourir les couches d'objets et rechercher la couche avec le nom "Collision". Quand il le trouve, il prend chaque objet de ce calque, crée un rectangle à cette position et le stocke dans le collisionTiles tableau. De cette façon, nous avons toujours une référence à ce sujet et nous pouvons effectuer une lecture en boucle pour le vérifier en cas de collision si nous avions un joueur.

(En fonction de la manière dont votre système gère les collisions, vous souhaiterez peut-être faire quelque chose de différent.)


Affichage de la carte

Enfin, pour afficher la carte, nous voulons tout d’abord rendre l’arrière-plan, puis le premier plan, afin que la superposition soit correcte. Dans d’autres langues, il s’agit simplement de rendre l’image.

 // charge la couche de fond addChild (screenBitmap); // rectangle juste pour montrer à quoi ressemblerait quelque chose entre les couches var playerExemple: Shape = new Shape (); playerExample.graphics.beginFill (0x0099CC, 1); playerExample.graphics.lineStyle (2); // contour rectangle playerExample.graphics.drawRect (0, 0, 100, 100); playerExample.graphics.endFill (); playerExample.x = 420; playerExample.y = 260; collisionTiles.push (playerExample); addChild (playerExample); // charge la couche supérieure addChild (screenBitmapTopLayer);

J'ai ajouté un peu de code entre les couches ici juste pour démontrer avec un rectangle que la superposition fonctionne réellement. Voici le résultat final:

Merci d'avoir pris le temps de terminer le tutoriel. J'ai inclus un zip contenant un projet FlashDevelop complet avec tout le code source et tous les actifs..


Lecture supplémentaire

Si tu es intéressé à faire plus de choses avec Tiled, une chose que je n'ai pas couverte est Propriétés. L'utilisation des propriétés est un petit saut par rapport à l'analyse des noms de couche et vous permet de définir un grand nombre d'options. Par exemple, si vous voulez un point d'apparition ennemi, vous pouvez spécifier le type d'ennemi, la taille, la couleur et le reste, à partir de l'éditeur de carte en mosaïque.!

Enfin, comme vous l'avez peut-être remarqué, XML n'est pas le format le plus efficace pour stocker les données TMX. Le format CSV est un bon moyen d’analyser facilement un meilleur stockage, mais il existe aussi base64 (non compressé, zlib compressé et gzip compressé). Si vous souhaitez utiliser ces formats au lieu de XML, consultez la page du wiki Tiled sur le format TMX..