Petit jeu en HTML5 et découverte de Crafty

Posté par . Édité par BAud, Benoît Sibaud, Nÿco, palm123, ZeroHeure et NeoX. Modéré par Nÿco. Licence CC by-sa
Tags :
26
22
avr.
2014
Jeu

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.

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.

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 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. 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é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 :

    // 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 à 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:

    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 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 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 !

  • # Petit jeu en HTML5

    Posté par . Évalué à 7. Dernière modification le 22/04/14 à 12:41.

    "Petit jeu en HTML5", je vois souvent la référence à HTML5 quand on parle de jeu ou d'application purement "web", mais est-ce que ce n'est pas un abus de langage ? (non ce n'est pas un jeu de mots :P). Les 3/4 du code permettant d'en faire un jeu est plutôt écrit en Javascript d'où mon interrogation. Voilà un petit jeu en Javascript/CSS3/HTML5.

    de même que nous profitons des avantages que nous apportent les inventions d'autres, nous devrions être heureux d'avoir l'opportunité de servir les autres au moyen de nos propres inventions ;et nous devrions faire cela gratuitement et avec générosité

    • [^] # Re: Petit jeu en HTML5

      Posté par . Évalué à -2.

      Qui toune sous plateforme
      * GNU/Linux/Xorg - Web/Javascript/CSS3/HTML5
      * GNU/Linux/wayland - Web/Javascript/CSS3/HTML5
      * Linux/Android - Chrome/Javascript/CSS3/HTML5
      * Win32 - Chrome/Javascript/CSS3/HTML5
      * …

    • [^] # Re: Petit jeu en HTML5

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

      d'après wikipedia HTML5 : « Dans le langage courant, HTML5 désigne souvent un ensemble de technologies Web (HTML5, CSS3 et JavaScript) permettant notamment le développement d'applications (cf. DHTML). »
      Et ça marche parce que la page, ce qui est rendu est de l'HTML, le javascript étant là pour le transformer, ajouter de l'interaction. Mais ça reste du HTML à la base.

  • # DOM plus rapide que Canvas: tout à fait crédible

    Posté par (page perso) . Évalué à 10. Dernière modification le 22/04/14 à 13:29.

    Je travaille sur la partie graphique de Gecko et je trouve tout à fait crédible que le rendu par DOM d'un jeu soit plus rapide que le rendu par Canvas 2D.

    Le canvas 2D est dérivé de CoreGraphics lui-même dérivé de PostScript. Ce type d'API n'a pas été conçu pour des graphismes en temps réel et encore moins sur GPU, et y est inefficace pour plusieurs raisons:
    - API impérative consistant en une série d'opérations, mais qui ne ressemblent pas au type d'opérations qu'un GPU effectue, et donc difficile à traduire.
    - Non-séparation du rendu et de la déclaration des ressources (par exemple drawImage() déclare que le canvas va accéder à une image et demande immédiatement son rendu).
    - Existence de primitives intrinsèquement difficiles à optimiser sur GPU (courbes, texte…)
    - Attente d'une très haute qualité d'anti-crénelage.

    Alors qu'avec le rendu d'éléments DOM, tout est déclaré à l'avance, et la scène est décrite de façon déclarative, ce qui permet au navigateur de s'organiser pour optimiser son utilisation du GPU. Bien sûr, c'est encore assez tordu et il reste bien des façons de se planter, mais ça peut facilement être mieux que canvas 2D.

    Dans Firefox sur X11, pour que le rendu DOM aille vite, aller dans about:config et essayer: layers.acceleration.force-enabled=true, gfx.xrender.enabled=false. On travaille à ce que ce soit activé par défaut d'ici à quelques mois. C'est déjà par défaut sur les autres plateformes.

    Note: à la question de savoir quelle API Web serait vraiment efficace pour le rendu de jeux, la réponse est bien sûr WebGL :-) dans Firefox sur X11, à nouveau, essayer avec layers.acceleration.force-enabled=true.

    • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

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

      à la question de savoir quelle API Web serait vraiment efficace pour le rendu de jeux, la réponse est bien sûr WebGL

      C'est aussi très efficace pour exclure les GPU à pipeline fixe et pour planter les machines avec des drivers peu solides :-)

      http://devnewton.bci.im

      • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

        Posté par (page perso) . Évalué à 6. Dernière modification le 22/04/14 à 13:58.

        Objections ô combien légitimes étant donnée la promesse faite par le Web de compatibilité universelle :-)

        Ceci dit, on peut quand même relativiser:
        - Les GPUs non-intégrés non-mobiles supportent entièrement le pipeline programmable requis par WebGL depuis que le GeForce 6 est sorti en 2004, il y a 10 ans, suivis par les Radeon l'année suivante je crois;
        - Les GPUs intégrés d'Intel le supportent entièrement depuis les GMA X3000 en 2006;
        - Sous Windows, Direct3D 9 est capable de tirer parti d'un support partiel, ce qui donne une accélération correcte dès le Intel GMA 945, encore plus ancien;
        - Les appareils mobiles (téléphones) ont tous un GPU depuis quasi toujours, et ils supportent tous entièrement ce pipeline (car on a exprès choisi de se limiter aux specs d'OpenGL ES 2) depuis plusieurs années, même dans le bas de gamme (par exemple en Qualcomm, dès le Adreno 200; il faudrait redescendre sur des vieux Adreno 130 pour ne pas avoir ce support).

        Quant aux problèmes de pilotes bugués, c'est sûr que ça a été un gros problème, mais l'avantage du développeur Web, c'est que le développeur de navigateur est forcé de gérer ce problème pour lui, par exemple en implémentant des contournements de bugs, et en poussant les fabricants de GPU à s'assurer qu'ils exécutent sans heurt la très grosse suite de tests officiels de WebGL.

        • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

          Posté par . Évalué à 2.

          En parlant de WebGL, sais tu s'il existe des moteurs de jeu en JavaScript tirant parti de cette techno (tout comme Crafty pour le 2D)? Peux-tu en recommander quelques-uns?

          • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

            Posté par (page perso) . Évalué à 4. Dernière modification le 22/04/14 à 15:03.

            Il existe des dizaines de bibliothèques de rendu en JavaScript pouvant tirer parti de WebGL; la plus populaire est probablement three.js, mais une petite recherche t'en dégotera bien d'autres.

            (Edit: oups, tu avais demandé des moteurs de jeu, pas juste des biblis de rendu. La réponse est encore oui il y en a et une recherche en trouvera, mais je n'ai pas de nom particulier qui me vienne à l'esprit. Si tu feuillettes le blog de learningwebgl.com/blog tu en verras beaucoup passer)

            Note aussi que Emscripten est capable non seulement de compiler du C/C++ vers JavaScript, mais aussi de traduire les appels OpenGL en appels WebGL. C'est du sérieux, c'est ce que les moteurs commerciaux Unreal et Unity utilisent pour exporter des jeux entiers vers le Web.

            • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

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

              Note aussi que Emscripten est capable non seulement de compiler du C/C++ vers JavaScript, mais aussi de traduire les appels OpenGL en appels WebGL.

              Idem pour playn qui est aussi capable de faire du canvas, ce qui permet de gérer les anciens et les nouveaux GPU.

              http://devnewton.bci.im

          • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

            Posté par . Évalué à 1.

            Pas vraiment une recommendation mais une liste toute prête.

        • [^] # Re: DOM plus rapide que Canvas: tout à fait crédible

          Posté par . Évalué à 2.

          Les GPUs non-intégrés non-mobiles supportent entièrement le pipeline programmable requis par WebGL depuis que le GeForce 6 est sorti en 2004, il y a 10 ans, suivis par les Radeon l'année suivante je crois;

          Oui mais quand sont elles sorties en version entrée/moyen de gamme ?

          Ca serait intéressant d'avoir la proportion des configs qui marche bien avec opengl (pas désactivé a cause du matériel ou des question de sécurité).

  • # Merci

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

    Juste un simple merci pour avoir pris le temps d'écrire cette dépêche très détaillée.
    Cela semble très intéressant.

    ++

    • [^] # Re: Merci

      Posté par . Évalué à 2.

      C’est triste, j’ai cru à une interface web pour le moteur Crafty.

  • # Suite

    Posté par . Évalué à 1.

    Je suis en train de faire un petit jeu d'exploration/aventure spatiale sur Crafty en ce moment. Il n'est pas encore fonctionnel mais vous pouvez en trouver le code sur BitBucket (oui je sais, c'est mal).

    Sinon, un ami m'a aussi signalé que MelonJS est un autre bon moteur de jeu en JavaScript.

  • # Quintus?

    Posté par . Évalué à 1.

    J'avais commence a regarder quintus: http://html5quintus.com/
    et un tutorial la: http://html5quintus.com/documentation#.U1bA2abjmOo
    Ca a l'air tres similaire. Est ce que quelqu un a essaye les 2?

Suivre le flux des commentaires

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