Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Annexe : Pathfinding : modifier la classe Map

Par gnicos (Nicolas Gauville), le 22 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

Ce cours étant la deuxième partie sur le pathfinding, la lecture de la première partie est également nécessaire.

I. Mise en place

Pour réaliser et tester notre Pathfinding, nous allons créer une petite carte d'exemple. Pour les fichiers du moteur, nous prenons donc comme base ceux que nous avons réalisé à la fin du tutoriel “pas à pas”. Pour les graphismes, nous les réutiliserons également (à l'exception du bouton “play”), mais nous allons modifier le fichier “core.js” pour avoir une petite carte toute simple.

Nous allons donc réaliser une carte de 5×5 tuiles avec quelques murs (pour tester le pathfinding). Cette fois, par de gestion des déplacements clavier, puisque nous aurons bientôt les déplacements souris.

Nous pouvons donc avoir un code ça :

/**
 * Fichier "core.js"
 */
var gameview = document.getElementById('gameview'), stage;
stage = new Stage(gameview);
Ticker.setFPS(24);
Ticker.addListener(stage);
 
var myMap = new Map ();
 
myMap.offsetX = 400;
myMap.offsetY = 200;
 
stage.addChild ( myMap );
 
var player = new Player ( TileType.DRAW, gfx.mur ( 255, 0, 0 ), true );
myMap.addTile ( player, 3, 3, 1 );
 
var map = [     [0, 1, 0, 1, 0],
                [0, 0, 0, 0, 0],
                [0, 0, 0, 1, 0],
                [0, 1, 0, 0, 0],
                [0, 1, 0, 0, 0]];
 
 
for ( var i = 0; i < 5; i++ )
{
   for ( var j = 0; j < 5; j++ )
   {
           if ( map[i][j] === 0 )
           {
                   myMap.addTile ( new Tile ( TileType.DRAW, gfx.solTAlea(200,200,128), true ), i, j, 0 );
           }
           else
           {
                   myMap.addTile ( new Tile ( TileType.DRAW, gfx.murTAlea(128,128,128), false ), i, j, 2 );
           }
   }
}

Avec le résultat suivant : Visible également ici.

Nous allons maintenant attaquer la modification de la classe “map” !

II. La méthode update

Dans la classe “Map”, la première chose que nous allons modifier : c'est la méthode update. Nous allors la “casser” en deux : une méthode qui mettra à jour les positions, et l'autre pour modifier les profondeurs.

Pourquoi ? Tout simplement parce que vous vous rendrez vite compte que nous n'avons pas besoin de mettre à jour ces deux choses au même moment.

Nous avons donc notre fonction “update” :

/**
 * 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;
                }
        });
};

Que nous allons découper en deux fonctions :

/**
 * void updatePos
 * @purpose : Met à jour l'affichage.
 */
p.update = function ()
{
        this.updatePos();
        this.updateDepth();
};
 
/**
 * void updatePos
 * @purpose : Met à jour l'affichage (positions).
 */
p.updatePos = 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;
        });
};
 
/**
 * void updateDepth
 * @purpose : Met à jour l'affichage (profondeurs).
 */
p.updateDepth = function ()
{
        /**
         * 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;
                }
        });
};

III. Une méthode "getGraph"

Nous allons maintenant commencer à ajouter les méthodes dont nous allons avoir besoin dans la classe Map. Première méthode : “getGraph”. Cette méthode va convertir notre carte (tableau d'instances de la classe “Tile”) en tableau de nodes (tableau d'instances de la classe “Node”) compréhensible et utilisable par la méthode “Pathfinder.findPath()”.

Dans cette méthode, nous commençons par créer un tableau qui accueillera les nodes :

var graph = [];

Nous allons ensuite devoir boucler sur toutes les tuiles, par exemple avec une boucle for :

for ( var i = 0, max = this.tiles.length; i < max; i++ )
{
     //...
}

Nous définissons alors la tuile en cours :

var tile = this.tiles[i];

La, nous devons faire attention : le graphe doit être un tableau à deux dimensions, contrairement au tableau “tiles”. Si la ligne n'est pas définie, nous la définissons donc :

if ( graph[tile.posX] === undefined )
{
        graph[tile.posX] = [];
}

Enfin, nous ajoutons notre node, correspondant à la tuile en cours (position, et propriété “walkable”) :

graph[tile.posX][tile.posY] = new Node ( tile.posX, tile.posY, tile.walkable );

Nous avons ainsi notre méthode “getGraph” :

/**
 * Array getGraph
 * @purpose : Crée une carte de nodes pour le pathfinding.
 */
p.getGraph = function ()
{
        var graph = [];
 
        for ( var i = 0, max = this.tiles.length; i < max; i++ )
        {
                var tile = this.tiles[i];
 
                if ( graph[tile.posX] === undefined )
                {
                        graph[tile.posX] = [];
                }
 
                graph[tile.posX][tile.posY] = new Node ( tile.posX, tile.posY, tile.walkable );
        }
 
        return graph;
};

IV. Méthode "movePlayer"

Nous allons maintenant écrire une méthode “movePlayer ($x, $y)” qui permettra de déplacer le jouer avec le Pathfinding. Les paramètres $x et $y sont donc les coordonnées de la tuile d'arrivée.

Nous aurons besoin d'une propriété “player”, qui définit le joueur à déplacer. Nous définirons cette propriété dans la partie suivante. Ici, nous allons tout de même vérifier que cette propriété est définie, car nous ne pourrons pas réaliser de pathfinding sans joueur (ce qui tombe sous le sens).

if ( this.player === null )
if ( this.player === null )
{
        /**
         * Pas de joueur, donc pas de pathfinding.
         */
        return;
}

Ensuite, nous allons définir le graphe :

var graph = this.getGraph();

A partir de ce graphe, nous allons pouvoir définir les nodes de départ et d'arrivée. La node de départ est donc la node aux coordonnées du joueur, et la node d'arrivée la node aux coordonnées ($x, $y) :

var start = graph[player.posX][player.posY];
var end = graph[$x][$y];

Enfin, nous pouvons définir le chemin avec notre classe Pathfinder :

var path = Pathfinder.findPath ( graph, start, end );

Nous pouvons donc effectuer les mouvements ! On commence par boucler sur les nodes du chemin :

while ( path.length !== 0 )
{
    //...
}

On recupère la prochaine node avec la méthode “Array.shift()” :

var toTile = path.shift ();

Et, si le mouvement est réalisable, on l'execute :

this.player.move ( toTile.line - this.player.posX, toTile.col - this.player.posY );

Nous avons donc la fonction suivante :

/**
 * void movePlayer
 * int $x, int $y : coordonnées de la tuile d'arrivée
 */
p.movePlayer = function ( $x, $y )
{
        if ( this.player === null )
        {
                /**
                 * Pas de joueur, donc pas de pathfinding.
                 */
                return;
        }
 
        var graph = this.getGraph();
        var start = graph[player.posX][player.posY];
        var end = graph[$x][$y];
 
        var path = Pathfinder.findPath ( graph, start, end );
 
        while ( path.length !== 0 )
        {
                var toTile = path.shift ();
 
                this.player.move ( toTile.line - this.player.posX, toTile.col - this.player.posY );
        }
};

V. Modifications globales

Les nouvelles méthodes ont été ajoutées, mais il nous reste encore un peu de travail ! Pour commencer : la propriété “player”. Nous pensons donc à la définir dans la classe Map :

/**
 * Player player
 * Joueur contrôlable dans le cas de l'utilisation du pathfinding.
 */
p.player = null;

Mais aussi dans le fichier core.js !

myMap.player = player;

Enfin, nous allons modifier la méthode “addTile” : cette fois, nous allons déclencher la méthode “movePlayer” lorsque l'on clique sur une tuile, pour avoir notre déplacement à la souris !

$tile.content.onClick = function ()
{
        this.tile.map.movePlayer ( this.tile.posX, this.tile.posY );
};

Notre fonction “addTile” est alors 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 ();
 
        $tile.content.onClick = function ()
        {
                this.tile.map.movePlayer ( this.tile.posX, this.tile.posY );
        };
};

VI. Conclusion - ce qu'il reste à faire

Notre jeu exploite maintenant l'algorithme de Pathfinding, mais ce n'est pas suffisant ! Il va falloir ajouter quelque chose de primordial : les mouvements animés du joueur, de façon à ce que le déplacement soit visible, et que nous n'ayons pas cet effet de “téléportation” du joueur.

Dans le prochain chapitre, nous allons donc modifier la classe “Player”, pour réaliser nos mouvements animés !

Navigation rapide : Sommaire