Journal Petit jeu en HTML5 et découverte de Crafty

Posté par . Licence CC by-sa
31
1
avr.
2014
Ce journal a été promu en dépêche : Petit jeu en HTML5 et découverte de Crafty.

Sommaire

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 tous support est séduisante et c'est aussi la seule manière de faire des jeux pour Firefox OS (mon téléphone).

Crafty 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 tuto, 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.

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. J'en ai fait une petite sélection et y ai ajouté une fleur animée.

Sprites du monde

Sprites du perso

Vous pouvez récupérer le code du petit jeu, ça vous évitera d'être perdu.

L'écran de chargement

Le fichier HTML qui va nous servir de base est pratiquement nu.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <script type="text/javascript" src="https://raw.githubusercontent.com/craftyjs/Crafty/release/dist/crafty.js"></script>
        <script type="text/javascript" src="game.js"></script>
        <style>
          body, html {
            margin: 0;
            padding: 0;
            overflow: hidden;
          }
          #game {
            margin: 100px auto 0;
          }
        </style>
        <title>Crafty game test</title>
      </head>
      <body>
        <div id="game"></div>
      </body>
    </html>

Et voici la toute base de notre jeu, à mettre dans le fichier game.js.

    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éfinit 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 en générée aléatoirement en grande partie. 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/50 d'avoir une fleur sur un carré de sol, et 1/25 chance 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:

    // 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.

    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 ça.

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:

    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.

        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:

        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 dropper 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 pour vous y atteler.

On peut s'assurer que les buissons ne spawnent pas sur des fleurs, et que le joueur ne spawne 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 zone hebues 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 un structure de données pour que le jeu reste homogène.

Et bien plus encore, autant que votre créativité permet!

  • # Lien de démo

    Posté par . Évalué à 10.

    À la demande de plusieurs amis, j'ai mit le jeu directement sur mon serveur si vous voulez juste essayer.

  • # future dépêche

    Posté par (page perso) . Évalué à 4.

    J'ai promu ce journal très intéressant en dépêche. Mais la dépêche ne sera pas publiée tout de suite, on va la garder en réserve—à moins que l'auteur nous dise "Niet!".

    "La liberté est à l'homme ce que les ailes sont à l'oiseau" Jean-Pierre Rosnay

    • [^] # Re: future dépêche

      Posté par . Évalué à 3.

      Ah merci. J'ai hésité à le proposer en dépêche, mais l'article ne me semblait pas assez approfondi pour ça.

      • [^] # Re: future dépêche

        Posté par (page perso) . Évalué à 2.

        Si tu es d'accord, j'aimerai pouvoir garder la dépêche au frais et la sortir quand il n'y aura rien de très passionnant dans l'actu. Ça lui donnera plus de lecteurs et de commentaires.

        "La liberté est à l'homme ce que les ailes sont à l'oiseau" Jean-Pierre Rosnay

  • # Paf la balle à ton voisin

    Posté par (page perso) . Évalué à 7.

    J'avais commencé un petit jeu de ballon pour tester Crafty.js avec une IA basée sur des behaviour trees et la bibliothèque machinejs.

    Les idées de ce cadriciel sont très bonnes, mais j'ai arrêté, car:

    • la documentation n'était pas du tout à jour.
    • la gestion des collisions était trop simple: j'aurais préféré utiliser un moteur physique, je crois qu'il y avait un plugin box2d, mais je crois qu'il n'était pas encore fonctionnel.
    • les performances étaient catastrophiques.

    Ces points sont peut être corrigés aujourd'hui, mais depuis je suis passé à playn pour porter mes jeux.

    http://devnewton.bci.im

  • # Multi-touch, mobile, tablette

    Posté par (page perso) . Évalué à 3.

    Apparemment, ce n'est pas compatible pour les interfaces multi-touch du type mobile et tablette… car le jeu se joue au clavier, c'est bien cela ?

    • [^] # Re: Multi-touch, mobile, tablette

      Posté par . Évalué à 2.

      Oui c'est le cas pour ce petit jeu. Néanmoins Crafty supporte les évènements tactiles, donc il est possible de faire des jeux utilisables sur mobile. Mais je ne me suis pas étendu pour ce premier contact.

  • # Magic numbers

    Posté par . Évalué à 3. Dernière modification le 02/04/14 à 09:10.

    Hello,

    Merci pour le petit exemple, je trouve que ça peut faire d'excellents exercices de programmation pour débutants.

    Par contre, pour des raisons pédagogiques, je ne comprend pas pourquoi truffer le code de "magic numbers". Non seulement c'est une pratique très critiquable qui rend le code très dur à maintenir, mais en plus c'est illisible : au choix, parmi

    Crafty.init(320, 240, game);
    

    ou

    Crafty.init(world_width_px, world_height_px, game);
    

    , je trouve que le deuxième est beaucoup plus pédagogique.

    • [^] # Re: Magic numbers

      Posté par (page perso) . Évalué à 3.

      Ou plus simplement:

      Crafty.init();

      Les résolutions durcodées tuent des chatons, trouent la couche d'ozone et mangent leurs enfants.

      http://devnewton.bci.im

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.