Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Chapitre 4 : La classe Map

Par gnicos (Nicolas Gauville), le 13 juillet 2012
Navigation rapide :

<< Chapitre 3 - Sommaire - Chapitre 5 >>

La classe Map aura plusieurs rôles :

- Lister les tuiles qu'elle contient.

- Générer l'affichage (placer les tuiles, les afficher, gérer les profondeurs).

- Permettre un accès facile aux tuiles (par exemple, récupérer une tuile en indiquant une position).

Nous allons donc, comme dans le chapitre précèdent, créer cette classe.

I. Structure de la classe Map

Notre classe Map va donc devoir assurer un certain nombre de rôles, et nous allons donc commencer par lister les propriétés et méthodes que nous allons devoir créer pour réaliser tout ça.

Structure

Propriétés

Array tiles

Un tableau pour lister toutes les tuiles de la carte. Ce tableau sera un simple tableau à une seule dimension, listant les tuiles au fur et a mesure des ajouts et suppressions sur la carte.

int tilesWidth, int tilesHeight

Pour placer les tuiles, nous auront besoin de connaître leurs dimensions de base. Ces deux paramètres seront définis par défaut, mais pourrons toujours être modifiés si besoin.

int offsetX, int offsetY

Décalage (offset) de la carte en X et en Y, de la même façon que pour les tuiles. Tant que nous n'utilisons pas de scrolling, nous pourrons définir ces paramètres manuellement pour centrer la carte dans le Canvas.

Méthodes

addTile ( $tile, $x, $y, $z )

Méthode pour ajouter une tuile à la carte. Les paramètres sont simples : la tuile à ajouter (instance de notre précédente classe Tile), et sa position sur la carte.

removeTile ( $tile )

Une seconde méthode pour retirer une tuile de la carte.

getTileAt ( $x, $y, $z )

Une méthode pour récupérer facilement une tuile aux coordonnées demandées, ce qui sera particulièrement utile pour les collisions par exemple.

update ()

Une méthode pour mettre à jour la carte, c'est à dire replacer les tuiles au bon endroit, à la bonne profondeur, etc.

dispose ()

Enfin, la méthode pour détruire la carte quand elle n'est plus utile.

Nous n'avons plus qu'à coder tout ça !

En JavaScript

On commence comme la dernière fois, par définir la classe :

/**
 * Class Map extends Container
 */
var Map = function ()
{};

Notre classe Map sera héritière de la classe “Container”, fournie par EaselJS. Pour réaliser l'héritage, nous allons utiliser notre variable “p” (utilisée, comme la dernière fois, pour accéder au prototype) :

var p = Map.prototype = new Container();

A la suite, on définit simplement nos propriétés en leur donnant leur valeur par défaut (pour les tuiles, j'ai choisi des dimensions de 128x64px, mais, évidement, vous pouvez utiliser ce que vous voulez).

/**
 * object tiles
 * Tableau de tuiles (une seule dimension).
 */
p.tiles = [];
 
/**
 * int TileWidth
 * Largeur des tuiles (px).
 */
p.tilesWidth = 128;
 
/**
 * int TileHeight
 * Hauteur des tuiles (px).
 */
p.tilesHeight = 64;
 
/**
 * int offsetX
 * Décalage en X.
 */
p.offsetX = 0;
 
/**
 * int offsetY
 * Décalage en Y.
 */
p.offsetY = 0;

Maintenant, nous pouvons nous attaquer aux méthodes !

II. Ajouter et supprimer des tuiles

Méthode addTile

On commence simplement par définir la position de la tuile en fonction des paramètres reçus :

$tile.posX = $x;
$tile.posY = $y;
$tile.posZ = $z;

Nous pouvons également définir la propriété “map” de la tuile :

$tile.map = this;

On ajoute la tuile au tableau de tuiles via la méthode “push” de la classe Array :

this.tiles.push ( $tile );

Et on ajoute la tuile à la liste d'affichage :

this.addChild ( $tile.content );

Vous constaterez que la dessus, le code est très similaire à ce que nous pourrions obtenir en ActionScript 3.

Enfin, une dernière chose que nous pouvons faire, c'est exécuter la méthode update pour replacer les tuiles. Ce n'est pas obligatoire : si nous ne le faisons pas, le jeu sera un peu plus rapide à l’exécution dans la mesure ou la méthode update ne sera pas exécutée à chaque fois que l'on ajoute une tuile, mais il faudra, dans ce cas, le faire manuellement à la fin. Globalement, dans la mesure ou nos cartes seront petites pour l'instant, nous pouvons le faire ou non selon nos préférences.

this.update ();

Notre méthode addTile est donc définie ainsi :

/**
 * void addTile
 * @purpose : Ajoute une tuile à la carte.
 * tile $tile : Tuile à ajouter à la carte.
 * int $x : position en X de l'endroit ou ajouter la tuile (en nombre de cases).
 * int $y : position en Y de l'endroit ou ajouter la tuile (en nombre de cases).
 * int $z : position en Z de l'endroit ou ajouter la tuile (en nombre de cases).
 */
p.addTile = function ( $tile, $x, $y, $z )
{
        $tile.posX = $x;
        $tile.posY = $y;
        $tile.posZ = $z;
        $tile.map = this;
 
        this.tiles.push ( $tile );
        this.addChild ( $tile.content );
 
        this.update ();
};

Méthode removeTile

La méthode removeTile n'est pas plus compliquée, il suffit de connaître les méthodes “indexOf” et “splice” de la classe Array pour s'en sortir sans trop de problèmes.

Notre méthode removeTile devra faire ceci :

  1. - Supprimer la tuile du tableau de tuiles.
  2. - Supprimer la tuile de la liste d'affichage.
  3. - Détruire la tuile (méthode “dispose” de la classe Tile).

Et donc, en JavaScrit : on commence par chercher la tuile à supprimer dans le tableau.

La méthode “Array.indexOf” retourne l'index de l'objet demandé dans le tableau. Si l'objet n'y est pas, la valeur -1 est retournée.
var index = this.tiles.indexOf ( $tile );

Ici, on pense à bien vérifier que l'index est différent de ”-1”. Si la valeur est -1, on peut signaler une erreur.

Signaler les erreurs correctement est intéressant pour le débogage, car les erreurs ne sont pas affichées directement à l'utilisateur (donc on ne perturbe pas les utilisateurs si ils rencontrent une erreur dans notre programme), mais elles s'affichent dans l'inspecteur web (sur Google Chrome ou Safari, Ctrl/Cmd+shift+I puis cliquez sur le bouton ShowConsole en bas à droite), et permettent ainsi de savoir ce qui ne vas pas, quand ça ne marche pas. (tout comme nous pouvions le faire en ActionScript).

En JavaScript, cela nous donne ceci :

if ( index === -1 )
{
        throw new Error ( 'Map.removeTile : la tuile à supprimer ne fait pas partie de la carte !' );
}

Ensuite, on supprime la tuile du tableau avec la méthode “splice” :

this.tiles.splice ( index, 1 );
Pas besoin de “return” ou autre dans notre condition “if (index === -1)”, le fait de signaler l'erreur arrête automatiquement l’exécution de la fonction.

On retire la tuile de la liste d'affichage :

this.removeChild ( $tile.content );

Et on détruit la tuile :

$tile.dispose();

Notre fonction removeTile ressemble alors à ça :

/**
 * void removeTile
 * @purpose : Enlève une tuile de la carte
 * tile $tile : Tuile à supprimer de la carte.
 */
p.removeTile = function ( $tile )
{
        var index = this.tiles.indexOf ( $tile );
 
        if ( index === -1 )
        {
                throw new Error ( 'Map.removeTile : la tuile à supprimer ne fait pas partie de la carte !' );
        }
 
        this.removeChild ( $tile.content );
        this.tiles.splice ( index, 1 );
 
        $tile.dispose();
};
Note : la méthode “indexOf” de la classe Array n'est pas prise en charge par InternetExplorer pour les versions inférieures à la version 9. Si vous souhaitez assurer la compatibilité avec ces versions, il faut alors redéfinir la méthode “indexOf” :
if(!Array.indexOf)
{
     Array.prototype.indexOf = function(obj)
     {
          for ( var i = 0; i < this.length; i++ )
          {
               if( this[i] === obj )
               {
                    return i;
               }
          }
 
          return -1;
     }
}

Merci à Badwolf pour cette remarque ;)

III. Placer nos tuiles

A. La théorie

Chaque tuile à sa place

Pour commencer, on va utiliser un debut de map comme base. Les coordonnées sont placées au milieu des tuiles, en rouge et en jaune.

Nous allons commencer par observer leur position (en pixels). Nous pouvons exprimer ces positions en fonction de deux données : la hauteur et la largeur des tuiles (comme c'était fait dans le cas de tuiles carrées).

Notons que nous prenons ici le sommet haut des tuiles comme point d'origine. A vrai dire, le point d'origine n'a pas d'importance, tant qu'il est identique pour chaque tuile.

Ainsi, on observe que la tuile de coordonnées [0,1] a pour position [L/2,H/2], ou la tuile [2,0] a pour position [-2L/2,-2H/2] soit [-L,H].

Pour simplifier, on peut définir deux nouvelles variables A et B, telles que A = L/2 et B = H/2. (le but étant de trouver le lien entre les coordonnées en nombre de tuiles et la position en pixels, nous allons tenter d'avoir l'expression la plus simple possible pour les positions, d'où l'intérêt des variables A et B).

Nous pouvons ici mieux voir le lien entre les coordonnées et les positions :

Ainsi on peu établir une formule qui marchera pour toutes les tuiles et qui va nous permettre de calculer les positions en fonction des coordonnées.

Pour une tuile de coordonnées [I,J], on a donc comme position [( J - I ) * A , (J + I ) * B], comme ceci :

Nous savons donc maintenant comment placer nos tuiles isométriques.

Le zSorting

Le z-sorting consiste à placer chaque tuile à la bonne profondeur. Nous pourrions encore nous en passer à la création de la carte, si nous ajoutons les tuiles dans le bon ordre, mais une fois que nous ajoutons un joueur, le z-sorting est indispensable !

Cette notion n'a rien de très compliqué en soit, mais elle reste quand même l'une des difficultés qui bloque bien souvent les débutants.

Si vous n'avez pas compris l'utilité du z-sorting, je vais vous donner un exemple de tuiles mal placées sans z-sorting :

Pour trier nos tuiles, en profondeur, l'idée va être de leur attribuer un nombre en fonction de leur position (en nombre de tuiles), et de les trier en les comparant sur ce nombre.

Trouver un nombre qui permette de faire ce tri est assez simple. Pour prendre en compte les coordonnées X et Y, nous pouvons, par exemple, les ajouter, et donc avoir nb = X + Y

Cependant, si nous mettons plusieurs tuiles à la même coordonnée, nous devrons prendre en compte la valeur Z : nb = X + Y + Z.

Il reste tout de même un problème : la coordonnée Z ne doit pas avoir le même poids dans le calcul que les coordonnées X et Y, pour régler cela, on peu se contenter, par exemple, de multiplier X et Y. Par exemple, nb = 5 ( X + Y ) + Z.

B. La pratique

Nous allons donc réaliser notre méthode “update”, qui va placer les tuiles, et les trier pour régler leur profondeur.

On place nos tuiles

Pour commencer, nous allons devoir placer chaque tuile une a une. Pour cela, le plus simple reste d'utiliser la méthode “foreach” de la classe Array :

/**
 * void update
 * @purpose : Met à jour l'affichage.
 */
p.update = function ()
{
        /**
         * Mise à jour des tuiles.
         */
        this.tiles.forEach ( function($tile)
        {
 
        });
};

Nous n'avons plus, ensuite, qu'à appliquer notre formule sur “$tile.content” :

$tile.content.x = ( $tile.posY - $tile.posX ) * ($tile.map.tilesWidth/2) + ($tile.offsetX + $tile.map.offsetX);
$tile.content.y = ( $tile.posY + $tile.posX ) * ($tile.map.tilesHeight/2) + ($tile.offsetY + $tile.map.offsetY);

Par dessus, par dessous

Pour trier les tuiles, nous allons utiliser la méthode “sortChildren” de la classe Container, dont notre classe Map hérite. Cette méthode s'utilise de la même façon que la méthode “sort” de la classe Array.

La documentation d'EaselJS nous indique ceci :

void sortChildren ( sortFunction )

Performs an array sort operation on the child list.

Parameters:

sortFunction <Function> the function to use to sort the child list. See javascript's Array.sort documentation for details.

Returns: void

La méthode envoyé en paramètre sera construite comme ceci :

function ( $child_a, $child_b )
{
     /**
      * On compare $child_a et $child_b
      * On renvoie -1, 0, ou 1 pour dire si $child_a doit être devant ou derrière $child_b, ou si il n'y a pas de priorité (0)
      */
}

Notre méthode update() va donc ressembler à ça :

/**
 * void update
 * @purpose : Met à jour l'affichage.
 */
p.update = function ()
{
        /**
         * Mise à jour des tuiles.
         */
        this.tiles.forEach ( function($tile)
        {
                $tile.content.x = ( $tile.posY - $tile.posX ) * ($tile.map.tilesWidth/2) + ($tile.offsetX + $tile.map.offsetX);
                $tile.content.y = ( $tile.posY + $tile.posX ) * ($tile.map.tilesHeight/2) + ($tile.offsetY + $tile.map.offsetY);
        });
 
        /**
         * Tri des tuiles pour gérer les profondeurs.
         */
        this.sortChildren (function($a,$b)
        {
 
        });
};

Le problème, ici, c'est que notre méthode “sortChildren” accédera aux objets “content” de nos instances de la classe Tile, et nous ne pourrons donc pas accéder aux propriétés “posX, posY, posZ” sur les variables $a et $b. Pour parer le problème, nous pouvons éventuellement réferencer l'instance de la classe Tile depuis l'objet “content”, dans notre boucle forEach :

p.update = function ()
{
        /**
         * Mise à jour des tuiles.
         */
        this.tiles.forEach ( function($tile)
        {
                $tile.content.x = ( $tile.posY - $tile.posX ) * ($tile.map.tilesWidth/2) + ($tile.offsetX + $tile.map.offsetX);
                $tile.content.y = ( $tile.posY + $tile.posX ) * ($tile.map.tilesHeight/2) + ($tile.offsetY + $tile.map.offsetY);
                $tile.content.tile = $tile;
        });
 
        /**
         * Tri des tuiles pour gérer les profondeurs.
         */
        this.sortChildren (function($a,$b)
        {
 
        });
};

Ensuite, il ne nous reste que le plus simple ! On commence par calculer les valeurs de $a et $b, en applicant la formule vue précédemment :

$n_a = 5 * ($a.tile.posX + $a.tile.posY) + $a.tile.posZ;
$n_b = 5 * ($b.tile.posX + $b.tile.posY) + $b.tile.posZ;

Et on les compares :

if ( $n_a > $n_b )
{
        return 1;
}
else if ( $n_a < $n_b )
{
        return -1;
}
else
{
        return 0;
}

Notre méthode update est donc comme ceci :

/**
 * void update
 * @purpose : Met à jour l'affichage.
 */
p.update = function ()
{
        /**
         * Mise à jour des tuiles.
         */
        this.tiles.forEach ( function($tile)
        {
                $tile.content.x = ( $tile.posY - $tile.posX ) * ($tile.map.tilesWidth/2) + ($tile.offsetX + $tile.map.offsetX);
                $tile.content.y = ( $tile.posY + $tile.posX ) * ($tile.map.tilesHeight/2) + ($tile.offsetY + $tile.map.offsetY);
                $tile.content.tile = $tile;
        });
 
        /**
         * Tri des tuiles pour gérer les profondeurs.
         */
        this.sortChildren (function($a,$b)
        {
                $n_a = 5 * ($a.tile.posX + $a.tile.posY) + $a.tile.posZ;
                $n_b = 5 * ($b.tile.posX + $b.tile.posY) + $b.tile.posZ;
 
                if ( $n_a > $n_b )
                {
                        return 1;
                }
                else if ( $n_a < $n_b )
                {
                        return -1;
                }
                else
                {
                        return 0;
                }
        });
};

IV. Et encore deux !

Il ne nous reste que deux petites méthodes à ajouter :

Méthode getTileAt

Cette méthode, permettant d'obtenir une tuile aux coordonnées demandées, sera bien pratique, par exemple pour tester les collisions.

Cette méthode va être assez simple à réaliser : nous utiliserons simplement une boucle forEach et une condition. Pour commencer, on définit une variable “r_tile” qui sera la tuile à renvoyer :

var r_tile = null;

On utilise ensuite une boucle foreach sur le tableau de tuiles :

this.tiles.forEach ( function($tile)
{
});

Ensuite, on teste les coordonnée de la tuile : pour que ce soit la bonne, il faut que les trois coordonnées soient identiques, auquel cas nous définissons la variable r_tile.

if ( $tile.posX === $x && $tile.posY === $y && $tile.posZ === $z )
{
        r_tile = $tile;
}

Enfin, on renvoie la valeur de “r_tile”. Notre méthode “getTileAt” est alors définie ainsi :

/**
 * tile getTileAt
 * @purpose : Renvoie la tuile à la position indiquée.
 * $x, $y, $z : position de la tuile demandée.
 */
p.getTileAt = function ( $x, $y, $z )
{ 
        var r_tile = null;
        this.tiles.forEach ( function($tile)
        {
                if ( $tile.posX === $x && $tile.posY === $y && $tile.posZ === $z )
                {
                        r_tile = $tile;
                }
        });
 
        return r_tile;
};

Méthode dispose

Comme d'habitude, nous finissons par la méthode dispose. Nous aurons ici deux choses à faire : supprimer toutes les tuiles de la carte, et supprimer les propriétés de notre classe :

Pour supprimer toutes les tuiles, on peut par exemple utiliser une boucle while, et notre méthode “removeTile” :

while ( this.tiles.length > 0 )
{
        this.removeTile ( this.tiles[0] );
}

Notre méthode dispose est alors tout simplement comme ceci :

/**
 * void dispose
 * @purpose : Détruit la carte.
 */
p.dispose = function ()
{
        while ( this.tiles.length > 0 )
        {
                this.removeTile ( this.tiles[0] );
        }
 
        this.tiles = null;
        this.tilesWidth = null;
        this.tilesHeight = null;
        this.offsetX = null;
        this.offsetY = null;
};

Conclusion

Nous avons maintenant nos deux classes principales : Tile et Map. Dans le prochain chapitre, nous allons enfin commencer à les utiliser avec quelques graphiques, pour avoir un premier résultat visuel.

Navigation rapide :

<< Chapitre 3 - Sommaire - Chapitre 5 >>