Ceci est le premier article d'une nouvelle série, chaque mois j'essayerai de vous faire découvrir un nouveau moteur de jeu HTML5 via la réalisation d'un petit jeu Step-By-Step (Pour reprendre le principe de " One Game A Month" ). Crafty est un moteur de jeu en Javascript parmi d'autres, sa particularité majeure est son système de composition....

crafty

Ceci est le premier article d’une nouvelle série, chaque mois j’essayerai de vous faire découvrir un nouveau moteur de jeu HTML5 via la réalisation d’un petit jeu Step-By-Step (Pour reprendre le principe de « One Game A Month« ).

Crafty est un moteur de jeu en Javascript parmi d’autres, sa particularité majeure est son système de composition. Le principe est de greffer des composants (components) aux objets du jeu.

Par exemple si vous souhaitez créer un personnage qui peut se déplacer sur une carte, il suffit d’y ajouter le component 2D.

Si vous voulez gérer les collisions du personnage avec l’environnement, il faudra y ajouter le component Collision.

Un component peut lui même en contenir d’autres, par exemple pour qu’un joueur puisse contrôler son personnage au clavier et à la souris, il faudra créer un component Playable qui contiendra Mouse et Keyboard :

composition

Il faut savoir que Crafty n’est pas encore complétement stable, mais il a une communauté de contributeurs actifs. Le moteur est donc en constante évolution.

Je vais construire un petit jeu de type « snake« , Step-By-Step, pour vous montrer comment débuter avec Crafty et explorer quelques techniques sans aller jusqu’à organiser votre code ni utiliser d’autres libs.

Pour commencer, j’ai fait un tour sur OpenGameArt en quête d’assets pour le jeu.

Les tiles de BrowserQuest m’ont servi à fabriquer une petite map, et avec ce set d’items j’ai créé une spritesheet de notre serpent et quelques items.

Etape 1 – Structure (démo, source)

Commençons par créer une page HTML simple en y incluant la crafty-min.js et game.js que l’on va créer par la suite :

    <script src="assets/crafty-min.js"></script>
    <script src="game.js"></script>

Ensuite il faut créer le fichier game.js et initialiser Crafty, nous travaillerons uniquement sur ce fichier pour ce jeu.

// On s'assure que la page est bien chargée
window.onload = function () {
    // Initialisation de Crafty
    Crafty.init(400, 400); // Les paramètres sont (largeur, hauteur) en pixel
};

Etape 2 – Scène (démo, source)

Il faut préciser à Crafty que nous souhaitons utiliser le Canvas :

// Initialisation de Crafty
Crafty.init(400, 400);
// Initialisation du Canvas
Crafty.canvas.init();

Dans ce jeu il y aura 2 scènes : « main » pour la phase de jeu et « menu » qui sera l’écran d’accueil. Pour le moment nous allons créer la scène « main » :

// Création de la scene principale
Crafty.scene("main", function() {

});

Notre scène maintenant initialisée, nous allons y ajouter une image de fond qui représentera la map avec Crafty.e().

Crafty.e() sert à créer une entité en jeu avec, en paramètres, les components de cette entité :

// Création de la scene principale
Crafty.scene("main", function() {
    // Ajout de la map en image de fond
    Crafty.e("2D, Canvas, Image").image("assets/map.png");
});

Ici nous utilisons les components :

  • « Image » pour avoir accès à la méthode image() qui permet … d’ajouter une image.
  • « 2D » pour positionner l’image en x et y.
  • « Canvas » pour la faire apparaître sur le <canvas>

Maintenant que notre scène est prête, nous allons la déclencher une fois notre image préchargée :

// Chargement des assets
Crafty.load(["assets/map.png"], function () {
    // Déclenchement de la scène principale
    Crafty.scene("main");
});

Crafty.load() s’occupe de précharger un tableau d’images, puis exécute le callback : ici le lancement de la scène « main ».

Etape 3 – Initialisation du serpent (démo, source)

Maintenant nous allons créer le serpent (qui sera en fait une chaîne de carapaces de tortues vides).

Tout d’abord il faut référencer le sprite de la carapace dans Crafty :

// On map notre spritesheet "assets/items.png" avec des frames de 32x32 px
Crafty.sprite(32, "assets/items.png", {
    apple: [0, 0], // Cet élément se positionne à 0 frame en x et 0 frame en y
    fruits: [1, 0], // 1 frame en x et 0 en y
    egg: [2, 0],
    shell: [3, 0],
    flask: [4, 0]
});

Sans oublier d’ajouter la spritesheet dans le loader :

Crafty.load(["assets/map.png", "assets/items.png"], function () {
    Crafty.scene("main");
});

Maintenant nous pouvons créer notre component « Snake » grâce à Crafty.c().

// Création du composant Snake
Crafty.c("Snake", {
    init: function() {
        // Ajout des composants :
        // - 2D pour le placement
        // - Canvas pour la méthode d'affichage
        // - shell le sprite à afficher
        this.addComponent("2D, Canvas, shell");

        // Positionnement du serpent sur le canvas
        this.attr({
            x: 100,
            y: 200
        });
    }
});

Le premier paramètre de Crafty.c() est le nom du component, le second est le component lui même.

Lorsqu’une entité est créée la fonction init() de ses components est automatiquement appelée.

addComponent() permet de greffer d’autres composants à l’entité.

Notez ici que nous avons ajouté le composant « shell » qui a été créé par Crafty.sprite(), il permet d’accéder au sprite de la carapace.

attr() permet de modifier certains attributs de votre entité, comme par exemple la position (x, y) la rotation ou la taille (w, h).

Pour finir nous allons ajouter le serpent dans la scène principale :

Crafty.scene("main", function() {
    Crafty.e("2D, Canvas, Image").image("assets/map.png");
    Crafty.e("Snake");
});

Etape 4 – Collisions (démo, source)

Dans cette étape nous allons ajouter des collisions et des contrôles au clavier sur le serpent.

Pour le moment créons simplement un component « Wall » en lui greffant Collision.

// Composant Mur
Crafty.c("Wall", {
    init: function() {
        this.addComponent("2D, Canvas, Collision");
    }
});

Maintenant créons le component « Walls » qui contiendra nos 4 murs.

// Composant Murs, contient les 4 murs qui délimitent la zone de jeu
Crafty.c("Walls", {
    init: function() {

        // Création du mur Nord
        Crafty.e("Wall")
            .attr({x: 16, y: 16, w: 368, h: 16}) // Positionnement du mur
            .collision(new Crafty.polygon([0,0], [368,0], [368, 16], [0, 16])); // Hitbox du mur

        // Mur Est
        Crafty.e("Wall")
            .attr({x: 368, y: 16, w: 16, h: 336})
            .collision(new Crafty.polygon([0,0], [16,0], [16, 336], [0, 336]));

        // Mur Sud
        Crafty.e("Wall")
            .attr({x: 16, y: 336, w: 368, h: 16})
            .collision(new Crafty.polygon([0,0], [368,0], [368, 16], [0, 16]));

        // Mur Ouest
        Crafty.e("Wall")
            .attr({x: 16, y: 16, w: 16, h: 336})
            .collision(new Crafty.polygon([0,0], [16,0], [16, 336], [0, 336]));

    }
});

Ici nous donnons une taille et une position sur chaque mur grâce à attr() puis nous définissions la box de collision (Hitbox) avec .collision().

Pour tracer cette Hitbox nous utilisons new Crafty.polygon()  avec en paramètres chaque point de votre polygone.

Maintenant nous pouvons ajouter les murs à la scène principale :

Crafty.scene("main", function() {
    Crafty.e("2D, Canvas, Image").image("assets/map.png");
    Crafty.e("Walls");
    Crafty.e("Snake");
});

Maintenant nous allons tester les collisions en ajoutant les contrôles au clavier pour diriger le serpent.

Crafty.c("Snake", {
    init: function() {
        // ... code d'intialisation

        // Direction actuelle du Snake
        this.currentDirection = "e";

        // On déplace le serpent entre chaque frame
        this.bind("EnterFrame", function() {
            this.move(this.currentDirection, 1.2);
        });

        // Changement de direction lorsque les touches directionnelles sont pressées
        this.bind('KeyDown', function(e) {
            this.currentDirection = {
                38: "n", // Flèche du haut: nord
                39: "e", // Flèche de droite: est
                40: "s", // Flèche du bas: sud
                37: "w"  // Flèche de gauche: ouest
            }[e.keyCode] || this.currentDirection;
        });

        // Si le serpent touche un mur, on relance la partie
        this.onHit("Wall", function(){
            Crafty.scene("main");
        });
    }
});

bind() permet de se brancher sur un évènement. Ici on va se brancher sur l’évènement « EnterFrame » qui est déclenché à chaque frame (donc environ 60 fois par seconde).

move() provient du component « 2D » et permet de déplacer votre entité de x pixels dans une direction donnée (nord, sud, …).

L’événement « KeyDown » provient du component « Keyboard » et est déclenché lorsqu’une touche est pressée. Le callback de cet événement a en paramètres des informations sur ce dernier. Ce qui nous intéresse ici c’est le code de la touche : e.keyCode. Nous pouvons donc affecter une direction en fonction de la touche pressée.

onHit() permet de détecter une collision. Ici nous souhaitons détecter si le serpent heurte un mur : « Wall » et relancer la scène principale si c’est le cas.

Etape 5 – Fruits (démo, source)

Maintenant que nous avons un serpent (ou du moins sa tête) qui se déplace, nous allons rajouter des fruits à récolter, qui plus tard donneront des points.

Créons un nouveau component « Food » :

// Composant Food
Crafty.c("Food", {
    init: function() {
        this.addComponent("2D, Canvas, fruits, Collision");
        this.attr({
            w: 32,
            h: 32,
            // On ajoute un fruit positionné aléatoirement sur le terrain
            x: Crafty.math.randomInt(32, 336),
            y: Crafty.math.randomInt(32, 304)
        });
    }
});

Crafty.math.randomInt() permet de générer un nombre aléatoire avec en paramètres : nombreMin, nombreMax. Cela nous permet de positionner les fruits aléatoirement sur la map.

Ensuite nous pouvons ajouter un premier fruit à la scène principale.

Crafty.scene("main", function() {
    Crafty.e("2D, Canvas, Image").image("assets/map.png");
    Crafty.e("Walls");
    Crafty.e("Snake");
    Crafty.e("Food");
});

Maintenant il faut augmenter la vitesse du serpent lorsqu’il mange un fruit.

// Création du composant Snake
Crafty.c("Snake", {
    init: function() {

        // Code tronqué pour une meilleure lisibilité ...

        // Vitesse de déplacement du serpent
        this.speed = 1;

        // On déplace le serpent entre chaque frame
        this.bind("EnterFrame", function() {
            this.move(this.currentDirection, this.speed);
        });

        // Si on attrape un fruit
        this.onHit("Food", function(collisions){

            // Destruction du fruit
            collisions[0].obj.destroy();

            // Création d'un nouveau fruit
            Crafty.e("Food");

            // Augmentation de la vitesse
            this.speed += 0.125;

        });

    }
});

onHit() fournit un tableau des différents objets avec lesquelles le serpent est entré en collision. Dans notre cas nous voulons accéder au premier donc collisions[0].

Lorsque le serpent touche un fruit il faut le faire disparaître : collisions[0].obj.destroy();

Ensuite nous créons un nouveau fruit et augmentons la vitesse du serpent.

Etape 6 – Le corps du serpent (démo, source)

snake6

Maintenant il faut rajouter un maillon au corps du serpent lorsque ce dernier mange un fruit.

Pour ça il va falloir refactor notre component « Snake » et en créer un nouveau pour chaque partie de son corps :  « SnakePart« .

Commençons par créer le component « SnakePart » en reprenant des bouts de « Snake« .

// Création du composant des bouts du serpent
Crafty.c("SnakePart", {
    init: function() {
        this.addComponent("2D, Canvas, shell, Collision");
    },
    // La tête du serpent
    head: function(snake) {

        // Position par défaut
        this.attr({ x: 100, y: 200 });

        this.speed = 1;
        this.direction = "e";

        this.bind("EnterFrame", function() {
            this.move(this.direction, this.speed);
        });

        // Si le serpent touche un mur, on relance la partie
        this.onHit("Wall", function(){
            Crafty.scene("main");
        });

        // Si on attrape un fruit
        this.onHit("Food", function(collision) {

            // Destruction du fruit
            collision[0].obj.destroy();

            // Création d'un nouveau fruit
            Crafty.e("Food");

            // Augmentation de la vitesse
            this.speed += 0.075;

        });
        return this;
    }
});

Il y aura 2 types de « SnakePart » : la tête (head) et le corps (body) que nous allons créer après.

Les choses communes à la tête et au serpent peuvent rester dans init(), mettons le reste dans head qui est le premier maillon de la chaîne, celui qui dirige.

Ensuite le component « Snake » va être recyclé en conteneur :

// Création du composant Snake
Crafty.c("Snake", {
    init: function() {
        this.addComponent("2D, Canvas, Keyboard");

        // Tête du serpent
        this.head = Crafty.e("SnakePart").head(this);

        // La queue du serpent
        this.tail = this.head;

        // Changement de direction lorsque les touches directionnelles sont pressées
        this.bind('KeyDown', function(e) {

            this.head.direction = {
                38: "n",
                39: "e",
                40: "s",
                37: "w"
            }[e.keyCode] || this.head.direction;

        });

    }
});

Lorsqu’un serpent est créé, sa tête est également créée et référencée. Le serpent ne contenant pour l’instant qu’une seule partie, la tête et la queue sont une seule et même partie.

Désormais lorsque l’on change de direction, c’est la tête du serpent qui prend en compte la nouvelle direction.

A présent, nous voulons :

  1. Rajouter des maillons du serpent lorsqu’il mange un fruit.
  2. Faire en sorte que chaque maillon suive le précédent.

// Création du composant des bouts du serpent
Crafty.c("SnakePart", {
    init: function() {
        this.addComponent("2D, Canvas, shell, Collision");

        // Tableau des dernières positions de cette partie du serpent
        this.steps = [];

        // On enregistre les 10 dernières positions de cette partie
        this.bind("EnterFrame", function() {

            // A chaque frame, l'ancienne position est stockée
            this.steps.push({ x: this.x, y: this.y });

            // Si il y a plus de 10 positions enregistrées
            if(this.steps.length > 10) {
                // on supprime la plus ancienne
                this.steps.shift();
            }

        });
    },
    // La tête du serpent
    head: function(snake) {

        // Code tronqué pour une meilleure lisibilité ...

        // Si on attrape un fruit
        this.onHit("Food", function(collision) {

            // Code tronqué pour une meilleure lisibilité ...

            // On rajoute un bout au corps du serpent
            snake.tail.append(snake);

        });
        return this;
    },
    // Corps du serpent
    body: function(snake, parent) {

        // Position par défaut
        this.attr(parent.steps[0]);

        // chaque partie du corps du serpent suivra la précédente
        this.bind("EnterFrame", function() {
            this.attr(parent.steps[0]);
        });

        return this;
    },
    append: function(snake) {
        snake.tail = Crafty.e("SnakePart").body(snake, this);
    }
})

Pour que les différents maillons se suivent, il faut enregistrer les dernières positions du maillon précédent. Cette partie est commune à la tête et au corps, on peut donc la mettre dans init().

L’unique particularité d’une partie du corps du serpent est de suivre la précédente, donc dans l’événement « EnterFrame » on remplace la position courante par l’ancienne position du maillon précédent.

Puis on crée la fonction append() qui rajoute un maillon à la chaîne.

Etape 7 – Score (démo, source)

Le jeu commence à prendre forme, maintenant il faudrait ajouter un écran d’accueil et un score.

Tout d’abord nous allons référencer le score dans une variable :

window.onload = function () {

    // Votre score actuel
    var currentScore = 0;

    // ..
});

Puis fabriquons le component « Score« .

// Création du composant Score
Crafty.c("Score", {
    init: function() {
        this.addComponent("2D, DOM, Text");
        this.attr({ x: 40, y: 40, w: 200 });

        // Paramètres CSS à la jQuery
        this.css({ font: '16px Verdana', color: "white" });

        // Réinitialisation du score
        currentScore = 0;
    },
    // Incrémentation et display du score
    increment: function(by) {
        currentScore += by;
        this.display();
        return this;
    },
    display: function() {
        // Affichage du score à l'écran
        this.text("Score: "+currentScore);
        return this;
    }
});

Pour l’affichage du score nous utilisons le « DOM » et le component « Text » pour pouvoir bénéficier de la mise en forme en CSS. Mais sachez qu’on peut également manipuler du texte via Canvas.

Puis on crée une entité « Score » dans le serpent :

Crafty.c("Snake", {
    init: function() {
        // Code tronqué pour une meilleure lisibilité ...
        // Création et affichage du score
        this.score = Crafty.e("Score").display();

    }
});

Il ne reste plus qu’a incrémenter le score lorsque le serpent attrape un fruit :

// Code tronqué pour une meilleure lisibilité ...
Crafty.c("SnakePart", {
    head: function(snake) {

        this.onHit("Food", function(collision) {
            // On incrémente le score en fonction de la vitesse actuelle
            snake.score.increment(this.speed*1000);
        });

    }
});

Il reste à créer la scène « menu » :

Crafty.scene("menu", function() {

    // Si un score est enregistré on l'affiche sur le menu
    if(currentScore !== 0) {
        Crafty.e("2D, DOM, Text")
            .attr({ x: 40, y: 40, w: 200 })
            .css({ font: '16px Verdana', color: "black" })
            .text("Your score is: "+currentScore);
    }

    // Instructions pour démarrer une partie
    Crafty.e("2D, DOM, Text, Keyboard")
        .attr({ x: 40, y: 80, w: 200 })
        .css({ font: '16px Verdana', color: "black" })
        // Instructions
        .text("Press arrow key to start")
        // Si une flèche directionnelle est pressée, on lance une partie
        .bind('KeyUp', function(e) {
            if(e.keyCode  === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) {
                Crafty.scene("main");
            }
        });
});

Dans cette scène, nous affichons le score si il y en a un et l’instruction pour démarrer la partie.

Si le joueur appuie sur l’une des flèches directionnelles, la scène principale est lancée.

Il faut maintenant remplacer le lancement de la scène main par la scène menu à 2 endroits :

  1. Au chargement du jeu.
  2. Lorsqu’une partie se termine.

Crafty.load(["assets/map.png", "assets/items.png"], function () {
    Crafty.scene("menu");
});

// Si le serpent touche un mur, on relance la partie
this.onHit("Wall", function(){
    Crafty.scene("menu");
});

Etape 8 – Peaufiner (démo, source)

A partir de là une multitude d’options s’offrent à nous pour améliorer le jeu.

Personnellement j’en vois 4 qui semblent importantes :

  1. Équilibrer la vitesse du serpent
  2. Réduire la HitBox d’un maillon du serpent
  3. Ajouter 2 maillons par défaut
  4. Verrouiller la direction opposée à la direction actuelle

Le premier point est facile, c’est du paramétrage, je propose la configuration suivante :

this.speed = 1;

 

// Si on attrape un fruit
this.onHit("Food", function(collision) {
    // Code tronqué pour une meilleure lisibilité ...

    // Augmentation de la vitesse
    this.speed += 0.075;
});

Ensuite nous allons affiner la HitBox des maillons du serpent :

Crafty.c("SnakePart", {
    init: function() {
        // Code tronqué pour une meilleure lisibilité ...
        this.collision(new Crafty.polygon([8,8], [24,8], [24, 24], [8, 24]))
    }
});

Puis ajoutons 2 maillons par défaut lorsque le serpent est créé :

Crafty.c("Snake", {
    init: function() {
        // Code tronqué pour une meilleure lisibilité ...

        // Tête du serpent
        this.head = Crafty.e("SnakePart").head(this);

        // La queue du serpent
        this.tail = this.head;

        // On rajoute 2 parties au corps du serpent
        this.tail.append(this, true);
        this.tail.append(this, true);
    }
});

Pour finir, on verrouille la direction opposée :

// Changement de direction lorsque les touches directionnelles sont pressées
this.bind('KeyDown', function(e) {

    // Stockage de l'ancienne direction
    var oldDirection = this.head.direction;

    // Stockage de la nouvelle direction
    var newDirection = {
        38: "n",
        39: "e",
        40: "s",
        37: "w"
    }[e.keyCode] || this.head.direction;

    // Si la nouvelle direction n'est pas l'opposé de l'ancienne, on modifie la direction de notre serpent.
    if(newDirection !== {"n":"s", "s":"n", "e":"w", "w":"e"}[oldDirection]) {
        this.head.direction = newDirection;
    }

});

C’est terminé, on a une petite démo jouable. Évidemment cet article est juste une introduction, il y a des tas de thèmes à aborder autour de la réalisation de jeux HTML5, dont certains feront probablement l’objet d’articles sur ce blog.

Voici quelques ressources pour aller plus loin :

Pour le prochain article de cette série, le moteur de jeu que j’ai choisi est ImpactJS. J’essayerai de préparer un jeu un peu plus fun pour l’occasion.

Si vous vous mettez à Crafty, je vous invite à poster vos jeux en commentaires.

Commentaires

Vous devez vous inscrire ou vous connecter pour poster un commentaire