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....
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 :
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)
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 :
- Rajouter des maillons du serpent lorsqu’il mange un fruit.
- 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 :
- Au chargement du jeu.
- 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 :
- Équilibrer la vitesse du serpent
- Réduire la HitBox d’un maillon du serpent
- Ajouter 2 maillons par défaut
- 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 :
- Site Officiel
- Getting Started with Crafty
- La documentation
- Forum de la communauté
- Annuaire de components par la communauté
- Crafty Boilerplate, un squelette de projet Crafty avec Backbone et RequireJS
- Crafty sur Twitter
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.