URL: https://linuxfr.org/news/petit-jeu-en-html5-et-decouverte-de-crafty Title: Petit jeu en HTML5 et découverte de Crafty Authors: etenil BAud, Benoît Sibaud, Nÿco, palm123, ZeroHeure et NeoX Date: 2014-04-01T01:21:54+02:00 License: CC by-sa Tags: html5 et crafty Score: 26 Rien de tel que de créer son propre jeu... à jouer dans le navigateur. Petite introduction au développement de jeux sur web avec le moteur [Crafty](http://craftyjs.com/). C'est un moteur de jeu pour HTML5 écrit en javascript. Il fonctionne par entités et propose un rendu par DOM ou Canvas. Dans ce tutoriel, on va utiliser un rendu par DOM, qui est apparemment plus rapide que Canvas (c'est ce que dit la doc !). Nous allons donc nous servir de Crafty pour créer un petit tableau de jeu généré aléatoirement, et y déplacer un personnage, tout en gérant les collisions et animations du personnage et son environnement. *NdM : merci à etenil pour son journal.* ---- [Journal à l'origine de la dépêche](http://linuxfr.org/users/etenil--2/journaux/petit-jeu-en-html5-et-decouverte-de-crafty) [Le site de Crafty](http://craftyjs.com/) [Code du jeu](http://blog.etenil.net/static/crafty.zip) [Démo du jeu](http://blog.etenil.net/static/crafty/index.html) [Jouer à Crafty](http://blog.etenil.net/static/crafty/index.html) ---- Programmer des jeux vidéo avec Crafty ===================================== J'ai commencé à apprendre la programmation pour développer mes propres jeux vidéos. Beaucoup d'eau a coulé sous les ponts depuis et je me retrouve à développer surtout du web et des systèmes de base de données. Mon but initial de faire des jeux vidéos sombrant dans l'oubli... J'ai donc choisi de renouveler mon intérêt pour le développement de jeux et de regarder du côté des technologies web. Leur promesse d'être utilisable sur tout support est séduisante et c'est aussi la seule manière de faire des jeux pour Firefox OS (mon téléphone). Tout d'abord, trouver quelques sprites; c'est quand même plus intéressant à voir que des blocs colorés ! Une bonne ressource est [OpenGameArt.org](http://opengameart.org/content/oga-16x16-jrpg-sprites-tiles). J'en ai fait une petite sélection et y ai ajouté une fleur animée. ![Sprites du monde](http://blog.etenil.net/static/images/world.png) ![Sprites du perso](http://blog.etenil.net/static/images/character.png) Vous pouvez récupérer [le code du petit jeu](http://blog.etenil.net/static/crafty.zip), ça vous évitera d'être perdu. L'écran de chargement --------------------- Le fichier HTML qui va nous servir de base est pratiquement nu. ```html Crafty game test
``` Et voici la toute base de notre jeu, à mettre dans le fichier *game.js*. ```javascript window.onload = function() { var game = document.getElementById('game'); randRange = function(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } Crafty.init(320, 240, game); Crafty.sprite(16, "world.png", { grass1: [0,0], grass2: [1,0], grass3: [2,0], grass4: [3,0], flower: [0,1], bush1: [0,2], bush2: [1,2] }); Crafty.sprite(16, 18, "character.png", { player: [0,0] }); Crafty.scene("loading", function() { Crafty.load(["world.png", "character.png"], function() { Crafty.scene('main'); }); Crafty.background('#000'); Crafty.e("2D, DOM, Text") .attr({x: 140, y: 120}) .text('Loading...') .css({'text-align': 'center', 'color': '#FFF'}); }); Crafty.scene('loading'); } ``` La première chose qu'on fait ici, c'est initialiser le moteur Crafty. Pour cela, on lance `Crafty.init()` en définissant la taille de la zone de jeu (320x240), ainsi que la zone de jeu elle-même (le div `#game`). On définit ensuite les sprites. Les sprites se gèrent très facilement dans Crafty; il suffit d'indiquer le fichier de sprites, puis l'index de chaque sprite dans l'objet de mapping. ici `grass2` est situé à x=1 et y=0, c'est-à-dire 16px vers la droite, et collé au haut de l'image. Les sprites de personnages sont un peu différents car ceux-ci sont plus hauts que larges (16x18). Une fois définis, on pourra invoquer les sprites par leur petit nom directement, simplifiant ainsi les opérations. Notez que les sprites d'animation ne sont définis que par une seule image pour le moment, leur état neutre (voir `player`). Crafty utilise le concept de scène. Ici on a défini la scène de chargement, qui s'affiche jusqu'à ce que les objets nécessaires à la scène suivante soient chargés. Vous ne le verrez peut-être pas car c'est très court, mais le texte *loading...* devrait apparaitre brièvement sur un écran noir. La scène suivante `main` n'a pour l'instant pas été déclarée, conduisant à un écran noir. Il est temps de passer à la scène principale. Planter le décor ---------------- La zone de jeu va être générée en grande partie aléatoirement. Le sol sera tapissé de carrés d'herbe aléatoires, quelques buissons et fleurs animées. Les bords de l'écran seront quant à eux délimités par des buissons. Enfin notre personnage sera au milieu la zone de jeu. On va définir empiriquement qu'il y a 1 chance sur 50 d'avoir une fleur sur un carré de sol, et 1 chance sur 25 de trouver un buisson aléatoire. Il sera aisé plus tard d'ajuster ces valeurs si besoin. Je vous livre la fonction de génération de monde : ```javascript // Method to randomy generate the map function generateWorld() { // Generate the grass along the x-axis for (var i = 0; i < 20; i++) { // Generate the grass along the y-axis for (var j = 0; j < 15; j++) { grassType = randRange(1, 4); Crafty.e("2D, DOM, grass" + grassType) .attr({x: i * 16, y: j * 16}); // 1/50 chance of drawing a flower. if (i > 0 && i < 19 && j > 0 && j < 14 && randRange(0, 50) > 49) { var flower = Crafty.e("2D, DOM, SpriteAnimation, flower") .attr({x: i * 16, y: j * 16}) .reel("wind", 1000, 0, 1, 3) .animate("wind", 10) .bind("enterframe", function() { if (!this.isPlaying()) this.animate("wind", -1); }); } // 1/25 chance of drawing a bush. if (i > 0 && i < 19 && j > 0 && j < 14 && randRange(0, 25) > 24) { var bush = Crafty .e("2D, DOM, randbushes, bush"+randRange(1,2)) .attr({x: i * 16, y: j * 16}); } } } // Create the bushes along the x-axis which will form the boundaries for (var i = 0; i < 20; i++) { Crafty.e("2D, DOM, wall_top, bush"+randRange(1,2)) .attr({x: i * 16, y: 0, z: 2}); Crafty.e("2D, DOM, wall_bottom, bush"+randRange(1,2)) .attr({x: i * 16, y: 224, z: 2}); } // Create the bushes along the y-axis // We need to start one more and one less to not overlap the previous bushes for (var i = 1; i < 15; i++) { Crafty.e("2D, DOM, wall_left, bush" + randRange(1,2)) .attr({x: 0, y: i * 16, z: 2}); Crafty.e("2D, DOM, wall_right, bush" + randRange(1,2)) .attr({x: 304, y: i * 16, z: 2}); } } ``` Passons à la déclaration de la scène principale. ```javascript Crafty.scene("main", function() { generateWorld(); // Create our player entity with some premade components var player = Crafty.e("2D, DOM, player, controls") .attr({x: 152, y: 111, z: 1}) .reel("walk_left", 200, 6, 0, 2) .reel("walk_right", 200, 9, 0, 2) .reel("walk_up", 200, 3, 0, 2) .reel("walk_down", 200, 0, 0, 2); }); ``` Ici, on génère le monde aléatoirement, puis on place notre personnage principal en plein milieu de la zone de jeu. Les appels `.reel()` définissent les animations du personnage par rapport à l'arrangement des sprites. Les paramètres de `.reel()` sont le nom de l'animation, sa durée en ms, la position _x_ du sprite de départ, la position _y_ du sprite de départ, puis la longueur de l'animation en nombre de sprites (vers la droite). Pour l'instant le personnage est immobile, et les animations n'ont aucun effet. Nous allons remédier à cela. Premiers pas ------------ Crafty fournit deux contrôles de base. L'un permet d'aller à droite ou gauche et sauter, l'autre de se déplacer dans les quatre directions. Ce n'est pas très satisfaisant, nous allons définir nos propres contrôles qui permettront aussi des diagonales. Juste après la ligne `generateWorld();`, mettez le code suivant: ```javascript Crafty.c('CustomControls', { __move: {left: false, right: false, up: false, down: false}, _speed: 3, CustomControls: function(speed) { if (speed) this._speed = speed; var move = this.__move; this.bind('EnterFrame', function() { // Move the player in a direction depending on the booleans // Only move the player in one direction at a time (up/down/left/right) if (move.right) this.x += this._speed; else if (move.left) this.x -= this._speed; else if (move.up) this.y -= this._speed; else if (move.down) this.y += this._speed; }); this.bind('KeyDown', function(e) { // Default movement booleans to false move.right = move.left = move.down = move.up = false; // If keys are down, set the direction if (e.keyCode === Crafty.keys.RIGHT_ARROW) move.right = true; if (e.keyCode === Crafty.keys.LEFT_ARROW) move.left = true; if (e.keyCode === Crafty.keys.UP_ARROW) move.up = true; if (e.keyCode === Crafty.keys.DOWN_ARROW) move.down = true; }); this.bind('KeyUp', function(e) { // If key is released, stop moving if (e.keyCode === Crafty.keys.RIGHT_ARROW) move.right = false; if (e.keyCode === Crafty.keys.LEFT_ARROW) move.left = false; if (e.keyCode === Crafty.keys.UP_ARROW) move.up = false; if (e.keyCode === Crafty.keys.DOWN_ARROW) move.down = false; }); return this; } }); ``` Voici notre code de contrôle. La logique est assez simple, à chaque appui sur un bouton, on regarde l'état des touches fléchées pour déterminer les directions du mouvement en une combinaison de gauche, droite, bas et haut. Une fois le mouvement défini dans `__move`, crafty déplacera l'entité de `_speed` pixels dans les directions données lors du rafraîchissement d'écran. On va maintenant modifier la déclaration du personnage pour le faire se déplacer, et animer son mouvement. ```javascript var player = Crafty.e("2D, DOM, player, controls, CustomControls, SpriteAnimation") .attr({x: 160, y: 144, z: 1}) .CustomControls(1) .reel("walk_left", 200, 6, 0, 2) .reel("walk_right", 200, 9, 0, 2) .reel("walk_up", 200, 3, 0, 2) .reel("walk_down", 200, 0, 0, 2) .bind("EnterFrame", function(e) { if(this.__move.left) { if(!this.isPlaying("walk_left")) this.animate("walk_left", 10); } if(this.__move.right) { if(!this.isPlaying("walk_right")) this.animate("walk_right", 10); } if(this.__move.up) { if(!this.isPlaying("walk_up")) this.animate("walk_up", 10); } if(this.__move.down) { if(!this.isPlaying("walk_down")) this.animate("walk_down", 10); } }) .bind("KeyUp", function(e) { this.pauseAnimation(); this.resetAnimation(); }); ``` Notez la déclaration de *CustomControls* comme méthode controle, et la vitesse d'1px/image. Pour gérer les animations, on se greffe sur l'évènement *EnterFrame*, et on détermine l'animation à jouer par rapport à la direction de mouvement du personnage. Quand une touche est relachée, on arrête et rembobine l'animation pour revenir en position neutre (évènement *KeyUp*). À présent, le personnage devrait évoluer gaillardement sur votre écran, et passer allègrement à travers les buissons et hors de la zone de jeu... Collisions ---------- Il est temps de passer à la gestion des collisions. On va faire très simple et ne pas définir de hitbox. Crafty utilisera alors les sprites eux-mêmes comme hitbox. Pas terrible, mais simple. On va aussi ne s'occuper que des collisions entre le joueur et les buissons (quels qu'ils soient). Ça évitera au perso de sortir de la zone de jeu et le forcera à éviter les buissons dans l'herbe. Modifiez la déclaration du joueur comme suit : ```javascript var player = Crafty.e("2D, DOM, player, controls, CustomControls, SpriteAnimation, Collision") .attr({x: 160, y: 144, z: 1}) .CustomControls(1) .reel("walk_left", 200, 6, 0, 2) .reel("walk_right", 200, 9, 0, 2) .reel("walk_up", 200, 3, 0, 2) .reel("walk_down", 200, 0, 0, 2) .bind("EnterFrame", function(e) { if(this.__move.left) { if(!this.isPlaying("walk_left")) this.animate("walk_left", 10); } if(this.__move.right) { if(!this.isPlaying("walk_right")) this.animate("walk_right", 10); } if(this.__move.up) { if(!this.isPlaying("walk_up")) this.animate("walk_up", 10); } if(this.__move.down) { if(!this.isPlaying("walk_down")) this.animate("walk_down", 10); } }) .bind("KeyUp", function(e) { this.pauseAnimation(); this.resetAnimation(); }) .collision() .onHit("wall_left", function() { this.x += this._speed; this.pauseAnimation(); this.resetAnimation(); }) .onHit("wall_right", function() { this.x -= this._speed; this.pauseAnimation(); this.resetAnimation(); }) .onHit("wall_bottom", function() { this.y -= this._speed; this.pauseAnimation(); this.resetAnimation(); }) .onHit("wall_top", function() { this.y += this._speed; this.pauseAnimation(); this.resetAnimation(); }) .onHit("randbushes", function() { if(this.__move.left) this.x += this._speed; if(this.__move.right) this.x -= this._speed; if(this.__move.up) this.y += this._speed; if(this.__move.down) this.y -= this._speed; this.pauseAnimation(); this.resetAnimation(); }); ``` Après avoir déclaré l'utilisation du module *Collision*, on l'initialise avec `collision()`. Ensuite il suffit de gérer l'évènement `onHit` sur les objets intéressants. Les évènements de collisions sont résolus après le mouvement, mais avant le rendu. C'est pour cela qu'on fait reculer le personnage d'un pas (enfin, de `_speed`) lors de la détection d'une collision. Du point de vue du joueur, le personnage restera parfaitement immobile. On pourrait bien entendu faire autre chose lors de la collision, comme lancer un combat, ou envoyer un bonus tout en laissant le personnage passer dans le buisson. Les possibilités sont infinies ! Aller plus loin --------------- C'est donc une toute base de jeu, mais sans logique. On peut déjà améliorer le code sur beaucoup de points. Vous pourrez vous plonger dans la [documentation de Crafty](http://craftyjs.com/api/events.html) pour vous y atteler. On peut s'assurer que les buissons n'apparaissent pas sur des fleurs, et que le joueur n'apparaît pas sur des buissons. L'algorithme de génération du monde peut être largement amélioré pour grouper les types d'herbe et faire des zones herbues et d'autres moins. On pourra introduire d'autres types de sprites pour avoir un terrain un peu plus intéressant. Avec de gros sprites, on peut aussi introduire des éléments de décor comme des maisons, ou même des PNJ. Les hitbox des entités sont très maladroites. Il serait bien plus élégant de définir des hitbox sur les pieds du personnage et la base des buissons, afin de pouvoir passer derrière les éléments (il faudra aussi gérer l'axe *Z* des sprites pour le rendu !). En faisant des zones de sortie dans les buissons sur les rebords, on pourra lier d'autres tableaux, générés de façon procédurale eux aussi. Voire même générer plusieurs zones contigües à la fois et maintenir une structure de données pour que le jeu reste homogène. Et bien plus encore, autant que votre créativité permet !