Introduction à la création d’un jeu HTML5 avec CreateJS

createjs2

Pour cet article nous allons réaliser Step-By-Step un jeu de type Match3 (Tetris, Triple Town, Bejeweled, Naturalchimie, …). Vous pouvez dès maintenant accéder au jeu final.

Capture d’écran 2013-05-16 à 12.46.16

CreateJS n’est pas un moteur de jeu, c’est une suite de bibliothèques Javascript qui fournit de nombreux outils nécessaires à la création d’un jeu HTML5.

Chaque lib est utilisable en stand-alone :

  • EaselJS – Lib manipulation de la balise <canvas>
  • TweenJS – Permet de créer des animations fluides et personnalisables
  • SoundJS – Permet de manipuler la balise <audio>
  • PreloadJS – Lib de gestion de chargement de vos assets : sprites, audio, …

Nous allons également utiliser jMatch3, une petite lib que j’ai créée pour cet article et KeyMaster qui nous permettra de gérer les touches du clavier.

Je propose de créer un clone de Naturalchimie, mais vous devriez être capable de créer n’importe quelle variante à la fin de cet article.

Pour les assets nous allons utiliser les gemmes open sources récupérées sur OpenGameArt.

Sommaire

Initialisation – Source, Démo

Dans un premier temps nous allons créer une page HTML en y incluant les libs javascript nécessaires, une balise <canvas> ainsi qu’un futur fichier main.js qui contiendra le jeu.

Ajoutons dans la balise body, l’attribut onload avec comme valeur une fonction javascript qui sera alors exécutée une fois la page complétement chargée.

main.js

(function() {
    window.initialize = function() {

    };
})();

Je vous invite à tricher un peu en téléchargeant ce zip qui vous servira de base pour la suite.

Retour au sommaire.

Afficher une image – Source, Démo

Si vous testez la démo en ligne, il est possible que l’image ne s’affiche pas tout de suite ou pas du tout car nous n’utilisons pas encore le preload, en local l’image devrait s’afficher correctement.

Dans un premier temps nous allons créer un Stage. Un stage sert à contenir tous vos objets qui apparaîtront sur le <canvas>.

(function() {

    var stage;

    window.initialize = function() {

        // Créer un nouveau stage lié au canvas.
        stage=new createjs.Stage(document.getElementById(&quot;match_3&quot;));

Ensuite ajoutons un objet Bitmap à notre stage.


        // Créer objet Bitmap
        var bitmap = new createjs.Bitmap(&quot;assets/blueDude.png&quot;);

        // L'ajouter sur le stage
        stage.addChild(bitmap);

        // Mettre à jour le stage pour afficher le bitmap
        stage.update();

    };

})();

Dans cet article nous n’utiliserons pas les Spritesheet et BitmapAnimation, je vous invite à lire cet article complet et à décortiquer le code source de cette démo pour avoir une bonne idée de leur utilisation.

Retour au sommaire.

Initialisation de la grille – Source

Nous allons utiliser la lib jMatch3, qui permet de créer une grille remplie de cases vides par défaut.

Créons une classe Game qui nous permettra d’instancier chaque nouvelle partie avec une nouvelle Grille.

(function() {

    // Créer une classe Game
    function Game(stage) {

        // Créer une grille jMatch3
        this.grid = new jMatch3.Grid({
            width: 6,
            height: 7,
            gravity: &quot;down&quot;
        });

    }

    window.initialize = function() {

        var stage = new createjs.Stage(document.getElementById(&quot;match_3&quot;));

        // Créer une instance de Game
        var game = new Game(stage);

        var bitmap = new createjs.Bitmap(&quot;assets/blueDude.png&quot;);
        stage.addChild(bitmap);
        stage.update();

    };

})();

Retour au sommaire.

Construire des Gemmes – Source

Nous allons créer une classe Gem qui va permettre de construire de nouvelles gemmes et les affecter à des cases de notre Grille.

Cette classe prendra 2 paramètres : game qui fera référence à la partie en cours et type qui va définir le type de gemme afin de gérer les correspondances avec des gemmes de type identique.

Le type va également nous servir à charger le bon bitmap.

    // Créer la classe Gem
    function Gem(game, type) {
        this.game = game; // Référence à game
        this.type = type;

        // Créer et référencer le bitmap de la gemme
        this.bitmap = new createjs.Bitmap(&quot;assets/&quot;+type+&quot;.png&quot;);

        // L'ajouter sur le stage
        this.game.stage.addChild(this.bitmap);
    }

    window.initialize = function() {

        var stage = new createjs.Stage(document.getElementById(&quot;match_3&quot;));
        var game = new Game(stage);

        // Créer une gemme
        new Gem(game, &quot;blueDude&quot;);
        stage.update();
    };

Ensuite nous allons positionner la gem sur la grille en lui donnant 2 nouveaux paramètres : x et y.

Avec la lib jMatch3 il n’y a pas de notion de distance en pixel, chaque case occupe 1 unité, par exemple la case tout en haut à gauche aura pour coordonnées x=0 et y=0, sa case voisine de droite aura pour coordonnées x=1, y=0 etc…

C’est pourquoi il faut faire le calcul soi-même pour placer le bitmap sur le <canvas>, en multipliant la position de la gemme sur la grille par la taille de la gemme.

bitmap.x = x*GEMSIZE;
bitmap.y = y*GEMSIZE;

Il faut donc commencer par définir la taille d’une gemme, dans notre cas c’est 48×48.

    // Définir la constante pour la taille d'une gemme
    var GEMSIZE = 48;

Puis lors du positionnement de notre bitmap il faut multiplier la position de la gemme par sa taille, profitons-en pour créer la méthode .move(x, y) de notre gemme.

    // Créer la méthode pour déplacer une gemme
    Gem.prototype.move = function(x, y) {
        // Nouvelle position sur la grille
        this.x = x;
        this.y = y;
        // Nouvelle position en pixel sur le canvas
        this.bitmap.x = this.x * GEMSIZE;
        this.bitmap.y = this.y * GEMSIZE;
    };

Retour au sommaire.

Faire tomber une gemme – Source, Démo

Il faudrait maintenant permettre à notre gemme de tomber. C’est là que TweenJS entre en jeu.

Mais avant il faut définir un ticker. Le ticker est une fonction qui va s’exécuter entre chaque nouvelle frame de votre animation, donc d’une manière générale, 60 fois par seconde.

Dans notre ticker nous allons simplement demander au stage de se mettre à jour pour que nous puissions voir l’animation se dérouler.

    window.initialize = function() {

        // ...

        // Création du ticker
        createjs.Ticker.addEventListener(&quot;tick&quot;, function(event) {
            // Mise à jour du stage
            stage.update(event);
        });

        // ...

    };

Nous devons rajouter une constante MARGINTOP qui correspond à la marge au dessus de la grille, dans notre cas elle correspond à 2 hauteurs de gemme.

    var MARGINTOP = 2;

Puis nous allons créer la méthode .fall(x, y) qui va nous permettre de créer l’animation d’une gemme qui chute.

    // Créer la méthode fall
    Gem.prototype.fall = function(x, y) {

        // Créer une animation TweenJS sur le bitmap de la gemme
        createjs.Tween.get(this.bitmap).to({
            x: x * GEMSIZE, // Position X à la fin de l'animation
            y: MARGINTOP * GEMSIZE + y * GEMSIZE // Position Y ...
        }, 500, createjs.Ease.cubicOut);

    };

Pour cet article nous utilisons une animation de type cubicOut, mais de nombreux autres sont disponibles ici, essayez cette démo pour voir les différents types en action.

Nous pouvons maintenant appeler la méthode .fall(x, y) sur notre gemme pour observer le résultat.

    window.initialize = function() {

        // ...

        var gem = new Gem(game, &quot;blueDude&quot;, 2, 0);
        // Appeler la méthode fall()
        gem.fall(2, 6);

    };

Retour au sommaire.

Contrôles au clavier – Source, Démo

Ajoutons la possibilité de contrôler cette gemme au clavier, avec les touches droite et gauche et la faire tomber avec la touche bas.

Utilisons la lib Keymaster.

window.initialize = function() {

        // ...

        var gem = new Gem(game, &quot;blueDude&quot;, 2, 0);

        // Brancher une action sur la touche gauche
        key('left', function() {
            // Déplacer la gemme vers la gauche
            gem.move(gem.x-1, gem.y);
        });

        // Touche droite
        key('right', function() {
            // Déplacer la gemme vers la droite
            gem.move(gem.x+1, gem.y);
        });

        // Touche bas
        key('down', function() {
            // Faire tomber la gemme
            gem.fall(gem.x, 6);
        });

    };

Retour au sommaire.

Refactor de Game – Source

Nous allons refondre la classe Game pour qu’elle puisse gèrer elle-même la création de nouvelles gemmes ainsi que la gestion du clavier.


    function Game(stage) {

        // ...

        var currentGem;

        // La méthode newGem()
        this.newGem = function() {
            // Créer et référencer une nouvelle gemme
            currentGem = new Gem(this, &quot;blueDude&quot;, 0, 0);
        };

        this.newGem();

        // Lier des actions à des touches du clavier
        var keys = {
            left: function() {
                if (currentGem) {
                    currentGem.move(currentGem.x-1, currentGem.y);
                }
            },
            right: function() {
                if (currentGem) {
                    currentGem.move(currentGem.x+1, currentGem.y);
                }
            },
            up: function() {

            },
            down: function() {
                if (currentGem) {
                    currentGem.fall(currentGem.x, 6);
                }
            }
        };

        // Gérer les actions en fonction de la touche pressée
        this.handleKeyPressed = function(key) {
            keys[key]();
        };

    }

Il faut brancher les touches du clavier à la méthode que nous venons de créer :

    window.initialize = function() {

        // ...

        var game = new Game(stage);

        key('left', function() {
            game.handleKeyPressed('left');
        });

        key('right', function() {
            game.handleKeyPressed('right');
        });

        key('down', function() {
            game.handleKeyPressed('down');
        });

        key('up', function() {
            game.handleKeyPressed('up');
        });

    };

Retour au sommaire.

Lier une gemme à la grille – Source, Démo

Lorsqu’une gemme est tombée il nous faut maintenant lier notre gemme à la case qu’elle occupe sur la grille et créer une nouvelle gemme.

Commençons par créer une nouvelle gemme quand la dernière est tombée. Pour ça il suffit de rajouter un appel à la fin de notre animation TweenJS.

Nous devons référencer notre gem actuelle dans une variable car nous perdons le contexte d’exécution : var gem = this;


    Gem.prototype.fall = function(x, y) {
        // Référencer la gemme courante
        var gem = this;

        createjs.Tween.get(this.bitmap).to({
            x: x * GEMSIZE,
            y: MARGINTOP * GEMSIZE + y * GEMSIZE
        }, 500, createjs.Ease.cubicOut).call(function() {
            // Créer une nouvelle gemme
            gem.game.newGem();
        });

    };

Il est temps de lier nos gemmes à des cases de la grille jMatch3. Nous allons créer une fonction drop() sur les gemmes. Cette fonction permettra de récupérer la dernière case libre sur une colonne donnée dans notre grille puis de faire tomber notre gemme jusqu’à cette case. Ce qui nous permettra de les empiler.

    // La méthode drop()
    Gem.prototype.drop = function() {

        // Récupérer la column courante de la gemme
        var column = this.game.grid.getColumn(this.x);

        // Récupérer la dernière case libre
        var lastEmpty = jMatch3.Grid.getLastEmptyPiece(column);

        // Si une case est disponible
        if (lastEmpty) {

            // Il faut lier la case à notre gemme
            lastEmpty.object = this;

            // Et faire tomber la gemme jusqu'à cette case
            this.fall(lastEmpty.x, lastEmpty.y);
        }
    }

Il ne faut pas oublier de désactiver la currentGem lorsque drop est appelé sur celle ci, sinon cela peut provoquer des bugs lorsque le joueur effectue des actions sur une gemme qui est en train de tomber.


        var keys = {
            // ...
            down: function() {
                if (currentGem) {
                    currentGem.drop();
                    // Désactiver la gemme courante
                    currentGem = false;
                }
            }
        };

Retour au sommaire.

Gérer les correspondances – Source, Démo

Si 3 gemmes ou plus du même type sont collées, elle doivent être détruites. C’est certainement la partie la plus délicate de cet article, il va falloir redoubler d’attention.

Voici le scénario que nous souhaitons mettre en place :

Lorsque le joueur a fait tomber une gemme il faut ->

  1. Vérifier si des correspondances existent, s’il n’y en a pas le scénario s’arrête ici.
  2. Si des correspondances existent, détruire les gemmes associées, la gravité s’appliquera.
  3. Rejouer l’étape 1.

Nous allons utiliser la lib jMatch3 pour récupérer les correspondances grâce à la méthode .getMatches().

    Game.prototype.handleMatches = function() {

        // Récupérer les correspondances
        var matches = this.grid.getMatches();

        // Si une ou plusieurs correspondances sont trouvées
        if (matches) {

            // Parcourir chaque correspondance
            this.grid.forEachMatch(function(matchingPieces, type) {

                // Parcourir chaque case de la correspondance
                for (var i in matchingPieces) {

                    // Récupérer la gemme associée à la case
                    var gem = matchingPieces[i].object;

                    // Supprimer la gemme du stage
                    gem.game.stage.removeChild(gem.bitmap);
                }
            });

            // Détruire toutes les correspondances
            this.grid.clearMatches();
        }

    }

Nous pouvons désormais appeler cette méthode lorsqu’une gemme est tombée.


    Gem.prototype.fall = function(x, y) {

        var gem = this;
        createjs.Tween.get(this.bitmap).to({
            x: x * GEMSIZE,
            y: MARGINTOP * GEMSIZE + y * GEMSIZE
        }, 500, createjs.Ease.cubicOut).call(function() {
            gem.game.newGem();

            // Gérer les correspondances quand une gemme est tombée
            gem.game.handleMatches();
        });

    };

Retour au sommaire.

Cascade – Source

Créons une méthode handleFalling qui va :

  • Appliquer la gravité sur la grille
  • Récupérer toutes les gemmes à faire tomber
  • Faire tomber chaque gemme sur le <canvas>
  • Appeler une nouvelle fois handleMatches lorsqu’elles sont toutes tombées
  • Ou créer une nouvelle gemme à la fin de la cascade

    // La méthode handleFalling
    Game.prototype.handleFalling = function() {

        // Nous appliquons la gravité sur notre grille
        var fallingPieces = this.grid.applyGravity();

        // Si la gravité a fait tomber au moins 1 case
        if (fallingPieces.length &gt; 0) {

            // Définir un compteur de gemmes à faire tomber
            var hasFall = 0;

            // Parcourir les cases tombées
            for (var i in fallingPieces) {

                // On récupère la case
                var piece = fallingPieces[i];

                // Référencer le game courant
                var game = this;

                // Faire tomber la gemme
                piece.object.fall(piece.x, piece.y, function() {
                    // Incrémenter le compteur de gemmes tombées
                    hasFall += 1

                    // Si toutes les gemmes sont tombées
                    if (hasFall === fallingPieces.length) {
                        // Gérer les correspondances
                        game.handleMatches();
                    }
                });

            }
        } else {
            // Si aucune case n'est tombée
            // La cascade se termine ici

            // Créer une nouvelle gemme
            this.newGem();

        }

    };

A la fin de .handleMatches() nous appelons .handleFalling() pour créer la cascade.

    Game.prototype.handleMatches = function() {
        // ...

        // Gérer les gemmes à faire tomber
        this.handleFalling();
    };

Dans la méthode drop nous rajoutons un callback à l’appel de la méthode .fall(). Dans ce dernier nous appelons .handleMatches() qui va initier la cascade.

    Gem.prototype.drop = function() {
        var column = this.game.grid.getColumn(this.x);
        var lastEmpty = jMatch3.Grid.getLastEmptyPiece(column);
        if (lastEmpty) {
            lastEmpty.object = this;

            // Référencer la gemme courante
            var gem = this;
            this.fall(lastEmpty.x, lastEmpty.y, function() {
                // Gérer les correspondances quand la gemme est tombée
                gem.game.handleMatches();
            });
        }
    }

Enfin il faut refondre la méthode .fall() pour qu’elle puisse accepter le callback en paramètre.

    Gem.prototype.fall = function(x, y, callback) {

        callback = callback || function() {};

        createjs.Tween.get(this.bitmap).to({
            x: x * GEMSIZE,
            // End x position
            y: MARGINTOP * GEMSIZE + y * GEMSIZE
        }, 500, createjs.Ease.cubicOut).call(function() {
            // Appel du callback
            callback();
        });

    };

Retour au sommaire.

Gemmes aléatoires – Source, Démo

Nous allons créer un tableau de gemmes pour pouvoir créer une gemme au hasard.

    var GEMTYPES = [&quot;blueDude&quot;, &quot;cyanDude&quot;, &quot;greenDude&quot;, &quot;magentaDude&quot;, &quot;orangeDude&quot;, &quot;pinkDude&quot;, &quot;redDude&quot;, &quot;yellowDude&quot;];

Et piocher un type au hasard dans ce tableau lors de la création d’une gemme

        this.newGem = function() {
            currentGem = new Gem(this, GEMTYPES[Math.floor(Math.random() * GEMTYPES.length)], 0, 0);
        };

Retour au sommaire.

Débloquer des gemmes – Source, Démo

Maintenant nous voulons que la création d’une nouvelle gemme aille piocher, non pas dans le tableau de toutes les gemmes disponibles, mais dans un nouveau tableau de gemmes découvertes propres à chaque partie.

Partons sur 3 gemmes au départ, le joueur devra débloquer les suivantes en combinant 3 gemmes du même type.

Commençons par créer un nouveau tableau de gemmes découvertes sur chaque partie.

        // Créer un tableau de gemmes découvertes
        this.discoveredGems = [GEMTYPES[0], GEMTYPES[1], GEMTYPES[2]];

        var currentGem;
        this.newGem = function() {
            currentGem = new Gem(this, this.discoveredGems[Math.floor(Math.random() * this.discoveredGems.length)], 0, 0);
        };

Désormais nous souhaitons débloquer une nouvelle gemme lorsque le joueur arrive à faire une correspondance avec le dernier type de gemme découvert.

Mais en plus nous souhaitons remplacer la première gemme de chaque correspondance par une gemme de type supérieur.

Pour faire cela nous allons appeler une fonction .handleUpgrade() lorsque des correspondances sont trouvées :

    Game.prototype.handleMatches = function() {

        var matches = this.grid.getMatches();
        if (matches) {

            // Créer un tableau de cases à upgrade
            var piecesToUpgrade = [];

            this.grid.forEachMatch(function(matchingPieces, type) {

                // Ajouter la première case et son type dans le tableau
                piecesToUpgrade.push({
                    piece: matchingPieces[0],
                    type: type
                });

                for (var i in matchingPieces) {
                    var gem = matchingPieces[i].object;
                    gem.game.stage.removeChild(gem.bitmap);
                }

            });
            this.grid.clearMatches();

            // Upgrade les gemmes
            this.handleUpgrade(piecesToUpgrade);

        }
        this.handleFalling();

    };

Maintenant il reste à coder la méthode .handleUpgrade().


    // La méthode .handleUpgrade()
    Game.prototype.handleUpgrade = function(piecesToUpgrade) {

        // Parcourir chaque case de la grille à upgrade
        for (var i in piecesToUpgrade) {

            // Récupérer la case
            var pieceToUpgrade = piecesToUpgrade[i];

            // Récupérer le type de la gemme améliorée
            var upgradedType = GEMTYPES[GEMTYPES.indexOf(pieceToUpgrade.type) + 1];

            // Si le type est défini
            if (typeof upgradedType !== &quot;undefined&quot;) {

                // Si ce type n'a pas encore été découvert par le joueur
                if (this.discoveredGems.indexOf(upgradedType) === -1) {

                    // L'ajouter dans les types découvert
                    this.discoveredGems.push(upgradedType);
                }

                // Et créer une gemme améliorée sur cette case de la grille
                pieceToUpgrade.piece.object = new Gem(this, upgradedType, pieceToUpgrade.piece.x, pieceToUpgrade.piece.y + MARGINTOP);

            }

        }
    }

Retour au sommaire.

Groupe de gemmes – Source, Démo

Le mécanisme de base fonctionne, améliorons un peu le gameplay en contrôlant non pas une seule gemme mais 2.

Il faut d’abord créer une nouvelle classe GemGroup et les fonctions de contrôles vides : .drop() .move() et le nouveau .rotate()

    // Créer la classe GemGroup
    function GemGroup(game) {

        // Créer et référencer 2 Gemmes
        var gems = {
            first: new Gem(game, game.discoveredGems[Math.floor(Math.random() * game.discoveredGems.length)], 0, 0),
            second: new Gem(game, game.discoveredGems[Math.floor(Math.random() * game.discoveredGems.length)], 0, 1)
        };

        this.drop = function() {

        };

        this.move = function() {

        };

        this.rotate = function() {

        };

    }

Dans notre classe Game il faut remplacer .newGem() par .newGemGroup() :


    function Game(stage) {
        this.stage = stage;
        this.grid = new jMatch3.Grid({
            width: 6,
            height: 7,
            gravity: &quot;down&quot;
        });

        this.discoveredGems = [GEMTYPES[0], GEMTYPES[1], GEMTYPES[2]];

        // Remplacer currentGem par currentGemGroup
        var currentGemGroup;

        // Remplacer newGem par newGemGroup
        this.newGemGroup = function() {
            // Créer un nouveau GemGroup
            currentGemGroup = new GemGroup(this);
        };

        this.newGemGroup();

        // Remplacer currentGem par currentGemGroup
        var keys = {
            left: function() {
                if (currentGemGroup) {
                    currentGemGroup.move(-1);
                }
            },
            right: function() {
                if (currentGemGroup) {
                    currentGemGroup.move(+1);
                }
            },
            up: function() {
                if(currentGemGroup) {
                    currentGemGroup.rotate();
                }
            },
            down: function() {
                if (currentGemGroup) {
                    currentGemGroup.drop();
                    currentGemGroup = false;
                }
            }
        };
        this.handleKeyPressed = function(key) {
            keys[key]();
        };

    }

Normalement vous devriez avoir 2 gemmes l’une en dessous de l’autre. Et les contrôles ne font plus rien car nous les avons attachés à des méthodes vides.

Retour au sommaire.

Déplacer un groupe de gemmes – Source, Démo

Commençons par la fonction .move().

Seule la position x nous intéresse pour le groupe de gemmes car y sera toujours égale à 0.

Initialisons donc x et la méthode .move() qui ne fera qu’incrémenter ou décrémenter x et mettre à jour les positions des gemmes en conséquence.


    function GemGroup(game) {

        // Position du groupe de gemmes
        var x = 0;

        var gems = {
            first: new Gem(game, game.discoveredGems[Math.floor(Math.random() * game.discoveredGems.length)], 0, 0),
            second: new Gem(game, game.discoveredGems[Math.floor(Math.random() * game.discoveredGems.length)], 0, 1)
        };

        this.drop = function() {

        };

        // La méthode move
        this.move = function(amount) {

            // Incrémente x
            x += amount;

            // Et déplace chaque gemme
            gems.first.move(x, gems.first.y);
            gems.second.move(x, gems.second.y);
        };

        this.rotate = function() {

        };

    }

Retour au sommaire.

Tourner un groupe de gemmes – Source, Démo

Quatre cas sont possibles :

cases

Nous allons donc créer un tableau avec ces 4 cas, et à chaque fois qu’une rotation sera demandée, le cas suivant sera effectué.

Dans ce tableau nous allons également indiquer pour chaque cas, l’ordre dans lequel les gemmes doivent tomber, la gemme la plus basse doit toujours tomber en premier.

        var x = 0;

        // Définir un tableau des différents cas
        var patterns = [{
            first: { x: 0, y: 0 },
            second: { x: 0, y: 1 },
            order: ['second', 'first']
        }, {
            first: { x: 1, y: 1 },
            second: { x: 0, y: 1 },
            order: ['second', 'first']
        }, {
            first: { x: 0, y: 1 },
            second: { x: 0, y: 0 },
            order: ['first', 'second']
        }, {
            first: { x: 0, y: 1 },
            second: { x: 1, y: 1 },
            order: ['first', 'second']
        }];

Nous définissons quel est le cas actuellement utilisé.

var currentPattern = 0;

Ne pas oublier d’utiliser notre système à la création d’une gemme.

        var gems = {
            first: new Gem(game, game.randomGemType(), x + patterns[currentPattern].first.x, patterns[currentPattern].first.y),
            second: new Gem(game, game.randomGemType(), x + patterns[currentPattern].second.x, patterns[currentPattern].second.y)
        };
    Game.prototype.randomGemType = function() {
        return this.discoveredGems[Math.floor(Math.random() * this.discoveredGems.length)];
    }

Comme la ligne est un peu longue on en profite pour refactor un peu en créant une méthode qui récupère un type de gem aléatoirement.

Maintenant nous pouvons coder la méthode .rotate() qui va incrémenter le currentPattern puis mettre à jour les positions des gemmes. Comme c’est un doublon avec la fonction .move() nous allons créer une nouvelle méthode .updatePositions()


        // La méthode updatePositions
        this.updatePositions = function () {
            // Récupérer le cas actuel de positions
            var pattern = patterns[currentPattern];

            // Placer les gemmes en fonction du cas actuel
            gems.first.move(x + pattern.first.x, pattern.first.y);
            gems.second.move(x + pattern.second.x, pattern.second.y);
        }

        this.move = function(amount) {
            x += amount;

            // Mettre à jour les positions des gemmes
            this.updatePositions();
        };

        // La méthode rotate
        this.rotate = function() {

            // Mettre à jour le cas actuel
            currentPattern = (currentPattern + 1) % patterns.length;

            // Mettre à jour les positions des gemmes
            this.updatePositions();
        };

Retour au sommaire.

Rester dans le viewport – Source, Démo

Avant de coder la méthode .drop() on s’aperçoit qu’il y a un problème. Si le groupe de gemmes déborde en dehors de notre grille sur la gauche ou sur la droite, ça risque de générer une erreur.

Fixons donc d’abord des limites :

        this.move = function(amount) {

            // Calculer la future position X
            var newX = x + amount;

            // Si le cas actuel est le cas 1 (#2) ou 3 (#4)
            // La valeur max de X sera 4 sinon 5
            var maxX = (currentPattern === 1 || currentPattern === 3) ? 4 : 5;

            // Si la nouvelle valeur de x ne risque pas de déclencher de problème
            if (newX &gt;= 0 &amp;&amp; newX &lt;= maxX) {
                // Mettre à jour la valeur actuelle de X
                x = newX;
            }

            this.updatePositions();
        };

        this.rotate = function() {

            currentPattern = (currentPattern + 1) % patterns.length;

            // Si on se trouve au bord du viewport
            // Et que nous sommes dans le cas 1 (#2) ou le cas 3 (#4)
            if (x === 5 &amp;&amp; (currentPattern === 1 || currentPattern === 3)) {
                // Nous devons décaler x de vers la gauche
                x = 4;
            }

            this.updatePositions();
        };

Retour au sommaire.

Faire tomber un groupe de gemmes – Source, Démo

Il faut dans un premier temps parcourir l’ordre dans lequel les gemmes doivent tomber. Et refactor notre methode .drop() sur les gemmes pour qu’elles puissent accepter un callback en paramètre.

De cette façon nous n’allons plus appeler .handleMatches() à chaque fois qu’une gemme est tombée mais seulement une fois que toutes les gemmes du groupe sont tombées.

        this.drop = function() {

            // Récupérer le cas actuel
            var pattern = patterns[currentPattern];

            // Compteur de gemmes tombées
            var dropped = 0;

            // Total de gemmes à faire tomber
            var gemsCount = pattern.order.length;

            // Parcourir le tableau d'ordre
            for (var i in pattern.order) {

                // faire tomber chaque gemme dans l'ordre
                gems[pattern.order[i]].drop(function() {
                    // Incrémenter le compteur
                    dropped += 1;

                    // Si toutes les gemmes sont tombées
                    if (dropped === gemsCount) {
                        // Gérer les correspondances
                        game.handleMatches();
                    }

                });
            }
        };

Il faut refactor la méthode drop des gemmes pour qu’elle accepte le callback.

    Gem.prototype.drop = function(callback) {
        var column = this.game.grid.getColumn(this.x);
        var lastEmpty = jMatch3.Grid.getLastEmptyPiece(column);
        if (lastEmpty) {
            lastEmpty.object = this;

            // On appelle la méthode fall avec le callback
            this.fall(lastEmpty.x, lastEmpty.y, callback);
        }
    }

Retour au sommaire.

Score – Source, Démo

Nous allons créer un div dans le html où nous souhaitons que le score s’affiche.

&lt;/pre&gt;
&lt;div id=&quot;score&quot;&gt;&lt;/div&gt;
&lt;pre&gt;

Initialisons le score à 0 pour chaque nouvelle partie et créons une méthode .addToScore() qui va mettre à jour le score et l’afficher dans la page.

        this.score = 0;
        this.addToScore(0);
    // La méthode addToScore
    Game.prototype.addToScore = function(amount) {
        // Incrémenter le score
        this.score += amount;

        // Mettre à jour le div
        document.getElementById(&quot;score&quot;).innerHTML = this.score;
    };

Maintenant il faut appeler la méthode addToScore à chaque fois qu’une correspondance est trouvée.

Game.prototype.handleMatches = function() {
            var game = this;
            this.grid.forEachMatch(function(matchingPieces, type) {

                // Ajoutons au score un montant calculé par rapport au type de gemme
                game.addToScore((GEMTYPES.indexOf(type) + 1) * matchingPieces.length);

Retour au sommaire.

Game Over – Source, Démo

Une partie se termine lorsque le joueur essaye de placer une gemme là où il n’y a plus de place.

C’est donc dans la méthode .drop() d’une gem qu’on va afficher le game over.

Rajoutons un else : dans le cas où il n’y a plus de place dans la colonne.

Nous effaçons tout ce qu’il y a sur le canvas puis nous déclenchons la fonction gameOver() qui affichera une alert() avant de démarrer une nouvelle partie.

    Gem.prototype.drop = function(callback) {
        var column = this.game.grid.getColumn(this.x);
        var lastEmpty = jMatch3.Grid.getLastEmptyPiece(column);
        if (lastEmpty) {
            lastEmpty.object = this;
            this.fall(lastEmpty.x, lastEmpty.y, callback);
        } else {

            // Supprimer toutes les gemmes du canvas
            this.game.stage.removeAllChildren();

            // Déclencher la fonction gameOver()
            gameOver();

        }
    }

Il reste à définir la fonction gameOver() que nous attachons sur window pour la définir en tant que globale.

    window.initialize = function() {

        var stage = new createjs.Stage(document.getElementById(&quot;match_3&quot;));
        createjs.Ticker.addEventListener(&quot;tick&quot;, function(event) {
            stage.update(event);
        });

        var game = new Game(stage);

        // Créer la fonction gameOver sur window
        window.gameOver = function() {
            // Alert avec le score du joueur pour cette partie
            alert(&quot;Game Over - Your score : &quot;+ game.score);

            // Création d'une nouvelle partie
            game = new Game(stage);
        }

        key('left', function() {
            game.handleKeyPressed('left');
        });

        key('right', function() {
            game.handleKeyPressed('right');
        });

        key('down', function() {
            game.handleKeyPressed('down');
        });

        key('up', function() {
            game.handleKeyPressed('up');
        });

    };

Retour au sommaire.

Preload – Source, Démo

Et pour finir nous voulons éviter les bugs d’affichage car les gemmes ne sont pas encore chargées. Pour cela nous allons utiliser la lib PreloadJS :

Il suffit de donner une liste de fichiers à charger et donner des instructions une fois qu’ils sont tous chargés.

Dans la fonction initalize() :

        var game;

        // Créer une nouvelle LoadQueue
        var preload = new createjs.LoadQueue();

        // Donner des instructions lorsque la liste de fichier est chargée
        preload.addEventListener(&quot;complete&quot;, function() {

            // Créer une partie
            game = new Game(stage);
        });

        // Définir la liste des fichiers à charger
        preload.loadManifest([
            &quot;assets/blueDude.png&quot;,
            &quot;assets/cyanDude.png&quot;,
            &quot;assets/greenDude.png&quot;,
            &quot;assets/magentaDude.png&quot;,
            &quot;assets/orangeDude.png&quot;,
            &quot;assets/pinkDude.png&quot;,
            &quot;assets/redDude.png&quot;,
            &quot;assets/yellowDude.png&quot;
        ]);

Retour au sommaire.

Ressources

Pour le prochain article de cette série nous allons créer un jeu avec Enchant.JS.

N’hésitez pas à utiliser les commentaires pour partager vos créations ou pour éclaircir certains points.

Have Fun !

2 thoughts on “Introduction à la création d’un jeu HTML5 avec CreateJS”

  1. Ce tutoriel est vraiment fantastique :)
    J’ai pris beaucoup de plaisir à le suivre, merci beaucoup pour le travail que vous faites et continuez ainsi !

    Christophe

Leave a Reply

Your email address will not be published. Required fields are marked *