Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Annexe : Carte au format PNG

Par gnicos (Nicolas Gauville), le 24 août 2012
Navigation rapide : Sommaire

Rappel : Ceci est une annexe du cours sur les jeux isométriques en HTML5/JS.

Ce cours se base sur les chapitres “pas à pas”, et utilise les fichiers fournis à la fin du huitième chapitre :

Jeu entier (incluant les dossiers “www” et “dev”) : jeu.zip

Moteur isométrique (dossier “dev/src” uniquement) : moteur_iso.zip

A l'inverse, ce cours ne se base pas sur les autres annexes.

I. Une carte au format PNG

Dans cette annexe, nous allons voir comment charger une carte au format PNG. Ce format peut avoir plusieurs avantages : c'est un format, tout comme le XML, très utilisé pour les cartes, permettant notamment d'être utilisé facilement dans plusieurs langages différents, par exemple dans le cas d'un éditeur de cartes, ou dans le cas d'un jeu codé dans plusieurs langages pour plusieurs plateformes.

Pour commencer, nous allons voir le principe de ce format de carte, qui n'est pas forcément aussi évident que les autres. Les images sont composées d'un ensemble de pixels, c'est à dire un ensemble de points de différentes couleurs. (notez au passage, qu'un pixel ne correspond pas à une taille fixe, la taille qu'un pixel prends à l'écran dépend de la densité de cet écran).

Chaque pixel est caractérisé par trois composantes : rouge, vert et bleu. Dans le cas d'une image png, nous avons également une composante alpha, définissant la transparence. Nous appellerons, par la suite, ces composantes “r”, “g”, “b” et “a”.

Pour enregistrer notre carte sous forme d'image, nous allons ainsi faire correspondre chaque pixel à une tuile du jeu. Au lieu d'avoir des numéros, nous aurons ainsi un code couleur pour chaque type de tuile.

Ainsi, notre carte au format “tableau 2D en JavaScript” :

var map = [     [1, 1, 0, 0, 0],
                [0, 0, 0, 0, 0],
                [0, 0, 1, 0, 1],
                [0, 0, 1, 0, 0],
                [0, 0, 0, 0, 0]];

Pourra être transformée en image, assimilée à un tableau de pixels, donnant chacun un code couleur pour identifier un type de tuile.

Ici, pour des raisons de simplicité, nous utiliserons “une couleur = une tuile”. Cependant, dans de gros projets, le fait d'avoir quatre composantes séparées peut être très intéressant, pour définir plusieurs paramètres directement depuis la carte (par exemple, la propriété “walkable”).

Pour transcrire notre carte, il suffit de refaire un image de la taille de la carte (soit 5×5 pixels), et d'assigner une couleur à chaque numéro (0 et 1 pour notre ancienne carte). Ici, j'ai choisi du blanc (valeur 255 à toutes les composantes), et un gris très foncé ( rgba ( 15, 15, 15, 255 ) ).

Vous pouvez bien-entendu choisir les couleurs que vous voulez. Pour créer la carte, un logiciel tout simple d'édition d'images suffit, par exemple “Paint” sur Windows, “Aperçu” sur Mac. Dans mon cas, j'obtiens le résultat suivant :

Si vous souhaitez garder la même que moi, enregistrez cette image (oui, sans zoom, 5×5 px paraissent bien petits !). Nous pouvons la placer dans un dossier “maps”, par exemple : “maps/map.png”.

II. Javascript, getPixel ?

La grosse difficulté que nous allons rencontrer, c'est l'incapacité du JavaScript à lire la couleur d'un pixel d'une image. Malheureusement, il n'existe pas pour l'instant de méthode permettant de lire directement ces couleurs. C'est là une des grosses lacunes du JavaScript.

C'est également l'un des gros intérêts du tutoriel, beaucoup abandonnent alors ce format, lorsqu'il faut le traiter en JavaScript. Heureusement, il y a une solution : utiliser la balise “Canvas” !

En effet, si nous ne pouvons pas lire la couleur d'un pixel sur une image, nous pouvons, par contre, lire la couleur d'un pixel d'un Canvas. L'astuce sera donc simple : nous allons tracer notre image sur un Canvas, et récupérer les couleurs des pixels de ce Canvas !

III. Modification du jeu

Utiliser ce système va demander un certain nombre de modifications dans notre jeu. Nous allons donc réaliser ces modification, avant de coder la classe “PNGMapLoader”, qui effectuera le chargement et le décodage de l'image. Pour commencer, nous allons modifier le fichier “index.html” !

Première chose à faire : inclure notre futur fichier “PNGMapLoader.js” :

<script type="text/javascript" src="src/PNGMapLoader.js"></script>

Ensuite, nous allons ajouter un second Canvas, pour pouvoir lire les pixels. Nous n'utiliserons pas le premier Canvas, pour plus de clarté. De plus, ce second Canvas sera utilisé uniquement pour le chargement de carte, nous pouvons donc le rendre invisible, dans la mesure ou l'utilisateur n'a pas besoin de le voir :

<canvas id="pngloader" style="display: none;" width="20" height="20"></canvas>
Dans cet exemple, le Canvas a une taille de 20 par 20 pixels. Cette taille n'a pas vraiment d'importance (surtout du fait que le Canvas sera invisible), mais elle doit être supérieure ou égale à la taille des cartes que vous chargerez.

Ensuite, nous allons modifier le fichier “core.js”. Pour commencer, vous pouvez supprimer la variable “map”, qui n'aura plus d'utilité puisque c'est l'image PNG qui définira la carte. Ensuite, nous allons avoir besoin d'un “objet de transfert”, qui va permettre de transformer les codes couleurs en tuiles. Nous allons donc assigner à chaque couleur une fonction qui renvoie la tuile correspondante.

Ici, pour des raisons de simplicité, nous allons utiliser un code comme ceci : “r.g.b.a”, sachant tout de même que dans de nombreux cas, utiliser les composantes séparément peut être très utile également.

Nous allons donc créer une fonction qui renvoie une tuile dans le cas de blanc, et rien dans le cas contraire (le gris foncé) :

var tiles =
{
        "255.255.255.255": function(){return new Tile ( TileType.DRAW, gfx.solTAlea(200,200,128), true );},
        "15.15.15.255":null
};

La dernière modification à faire dans ce fichier est au niveau de la fonction “play.onClick”. Cette fois, nous n'allons plus utiliser de double boucle for pour placer les tuiles, c'est la classe “PNGMapLoader” qui s'en chargera. Nous allons donc appeller notre future méthode “load” de la classe PNGMapLoader, en lui envoyant trois choses :

  1. - La carte isométrique, pour qu'elle puisse ajouter les tuiles (ici, la variable “myMap”).
  2. - L'URL de la carte à charger (ici, “maps/map.png”).
  3. - L'objet de transfert (ici, “tiles”).

Nous allons donc remplacer toute la double boucle for par cette ligne :

PNGMapLoader.load ( myMap, "maps/map.png", tiles  );

Maintenant, nous n'avons plus qu'à créer notre classe PNGMapLoader !

IV. Classe PNGMapLoader

La classe PNGMapLoader contiendra donc une seule méthode statique “load”. Nous allons donc avoir une structure très simple comme ceci :

/**
 * Utilitaire PNGMapLoader
 */
var PNGMapLoader =
{
        /**
         * Fonction load.
         * Map $isoMap : carte du jeu.
         * string $url : URL du fichier à télécharger.
         * object $eqs : Objet assignant les couleurs aux tuiles.
         */
        load: function ( $isoMap, $url, $eqs )
        {
 
        }
};

Nous n'avons donc plus qu'à coder la méthode load. Première étape : charger l'image. Nous commençons donc par créer une variable “loader”, qui sera une instance de la classe Image :

var loader = new Image();

Nous lui indiquons ensuite son URL grâce à la propriété “src” :

loader.src = $url;

Et, par la suite, nous devrons donc attendre que l'image soit chargée pour l'utiliser :

loader.onload = function ( $e )
{
        //Suite du code
};
L'objet “$e” en paramètre, correspond à l'évènement “onLoad” qui sera envoyé.

Nous commençons donc par récupérer l'image chargée, fournie par la propriété “target” de l'évènement :

var png = $e.target;

Nous pouvons ensuite référencer le Canvas destiné au traçage de cette image :

var context = document.getElementById("pngloader").getContext("2d");

Et nous n'avons alors plus qu'à tracer cette image, grâce à la méthode “drawImage ( image, x, y )” :

context.drawImage ( png, 0, 0 );

Nous allons, enfin, récupérer les pixels de cette image grâce à la méthode “getImageData”. Cette méthode nécessite quatre paramètres, qui vont définir un rectangle, correspondant à la zone à récupérer (x, y, largeur, hauteur). Pour la position de départ (x, y), nous choisissons (0, 0), puisque nous avons tracé notre image à partir de ce point ; pour la hauteur et la largeur, nous allons récupérer les propriétés de cette image (png.width, png.height).

var imageData = context.getImageData ( 0, 0, png.width, png.height);

Enfin, nous allons pouvoir réaliser une double boucle “for” pour lire les pixels de chaque tuile :

for ( var i = 0; i < png.width; i++ )
{
        for ( var j = 0; j < png.height; j++ )
        {
                //Traitement d'une tuile.
        }
}

Mais, là, ça se complique un peu ! En effet, notre variable “imageData” n'est pas un tableau 2D. Il s'agit en effet d'un tableau à une seule dimension, contenant tous les pixels à la suite, et quatre données par pixels ! Dans le cas de notre carte 5×5, nous avons donc 5 * 5 * 4 cases soit 100 cases ! (5×5 cases, contenant chacune les quatre composantes). Nous allons donc devoir définir une variable position, et calculer à quel endroit nous devons nous trouver dans ce tableau à une seule dimension.

Si nous partons du principe que “i” représente une ligne, nous allons donc devoir le multiplier par le nombre de tuiles par ligne, et additionner “j”, qui correspondra à la position dans cette ligne. Ensuite, sachant qu'il y a quatre composantes pour chaque pixel, nous multiplions tout par quatre :

var position = ((i * png.width) + j) * 4;

Nous avons donc 4 cases associées à la position en cours : celle à l'index “position”, puis “position + 1”, “position + 2”, et “position + 3” (respectivement r, g, b et a). Nous pouvons donc les définir simplement :

var r = imageData.data[position];
var g = imageData.data[position+1];
var b = imageData.data[position+2];
var a = imageData.data[position+3];

Ici, nous avons choisi de les rassembler pour plus de simplicité :

var total = r+"."+g+"."+b+"."+a;

Nous pouvons alors utiliser l'objet “$eqs” pour récupérer l'éventuelle fonction qui permet d'avoir la tuile à ajouter :

var tile = $eqs[total];
Pensez bien au fait que l'objet de transfert renvoie des fonctions ! La variable “tile” est donc assignée, à ce moment, soit à une fonction, soit à “null”.

Nous pouvons alors, si la variable “tile” est définie, l'ajouter à la carte, à la position (i, j, 0), en pensant bien à écrire (“tile()” et non “tile”) :

if ( tile !== null )
{
        $isoMap.addTile ( tile(), i, j, 0 );
}

Et … notre classe “PNGMapLoader” est terminée ! Voici un petit récapitulatif du code qu'elle contient :

/**
 * Utilitaire PNGMapLoader
 */
var PNGMapLoader =
{
        /**
         * Fonction load.
         * Map $isoMap : carte du jeu.
         * string $url : URL du fichier à télécharger.
         * object $eqs : Objet assignant les couleurs aux tuiles.
         */
        load: function ( $isoMap, $url, $eqs )
        {
                var loader = new Image();
                loader.src = $url;
 
                loader.onload = function ( $e )
                {
                        var png = $e.target;
 
                        var context = document.getElementById("pngloader").getContext("2d");
 
                        context.drawImage ( png, 0, 0 );
 
                        var imageData = context.getImageData ( 0, 0, png.width, png.height);
 
                        for ( var i = 0; i < png.width; i++ )
                        {
                                for ( var j = 0; j < png.height; j++ )
                                {
                                        var position = ((i * png.width) + j) * 4;
 
                                        var r = imageData.data[position];
                                        var g = imageData.data[position+1];
                                        var b = imageData.data[position+2];
                                        var a = imageData.data[position+3];
 
                                        var total = r+"."+g+"."+b+"."+a;
                                        var tile = $eqs[total];
 
                                        if ( tile !== null )
                                        {
                                                $isoMap.addTile ( tile(), i, j, 0 );
                                        }
                                }
                        }
                };
        }
};

V. Conclusion

Nous avons ainsi vu comment lire une carte au format PNG, un format courant dans les jeux à base de tuiles, et un peu plus complexe à gérer dans le cas d'un jeu en JavaScript.

Navigation rapide : Sommaire