Retour au parcours HTML5

Gérez les images et les animations de vos jeux HTML5 comme un pro

Dans cet atelier apprenez les techniques des pros pour gérer le chargement et l’affichage de vos images dans un jeu Web codé en HTML5.

Tout d’abord nous allons coder un « Image Loader » pour pouvoir charger les images de votre jeu en une seule fois et afficher une barre de progression. Vous contournez ainsi le problème classique des images en HTML5 : qu’elles ne se chargent pas immédiatement et provoquent un bug dans votre jeu !

 

Ensuite nous construirons une classe « Sprite » super pratique, capable d’afficher des animations image par image, issues d’une « sprite sheet ». Ainsi, vous pourrez regroupez vos images au sein d’une seule image, comme le font les pros, et construisez puis affichez n’importe quelle séquence d’images afin de créer des animations.

Cette structure sera réutilisable dans vos propres jeux vidéo et elle est parfaitement évolutive pour coller à vos propres besoins spécifiques.

Adhérez aujourd'hui

Gamecodeur c’est à partir de 8 € / mois (facturé annuellement)

21 réflexions au sujet de “Programmation JavaScript / HTML5 : Gérez les images et les animations de vos jeux web comme un pro”

  1. Salut David,
    pour info, VSCode te permets d’ avoir la dimension de l’ image sans recourir à un logiciel exterieur 🙂

  2. Excellent tuto !

    (pour infos, tu peux taper directement dans ton invité de commande « cmd » pour executer un nouveau shell Dos ou « powershell » pour un nouveau shell powershell, ça va ouvrir une instance, au lieu de chercher partout l’un ou l’autre.
    Exit va fermer ces instances).

    Merci

  3. Bonjour David,

    Super Atelier que tu as fais là , tes conseils sont précieux et je gagne du temps en suivant tes cours.
    J’ai progressé mais j’ai encore beaucoup de choses à apprendre en JavaScript / HTML 5.
    Dans le chapitre chargement des images (avec les cartes)
    j’ai eu cette erreur :
    Uncaught DOMException: Failed to execute ‘drawImage’ on ‘CanvasRenderingContext2D’: The HTMLImageElement provided is in the ‘broken’ state.
    at Sprite.draw (http://127.0.0.1:5500/sprite.js:10:14)
    at http://127.0.0.1:5500/game.js:195:16
    at Array.forEach ()
    at draw (http://127.0.0.1:5500/game.js:194:16)
    at run (http://127.0.0.1:5500/main.js:14:5)

    dans le fichier game.js :
    for (let image of Object.values(imageLoader.getListImages())) {
    let mySprite = new Sprite(image);
    mySprite.x = Math.random() * 800;
    mySprite.y = Math.random() * 600;
    lstSprites.push(mySprite);
    }

    la classe Sprite ne prend pas pas en 1er parametre une reference d’objet mais plutôt un path non ?
    j’ai donc remplacé ceci :
    let mySprite = new Sprite(image);
    par
    let mySprite = new Sprite(image.src);

    ou bien on peut aussi changer le début de la boucle for :
    for (let image of imageLoader.getLstPaths()) {

    en rajoutant cette methode dans la classe imageLoader:
    getLstPaths() {
    return this.lstPaths;
    }

    Si cela peut aider d’autres élèves qui ont eu ce problème..

  4. en fait je crois que c’est parceque le constructor de la class Sprite change légèrement en cours de route, j’ai dû manquer l’information mais en retombant dessus un peu plus loin j’ai remarqué le changement (vidéo/ Nettoyage du projet :3min20)

    – supprimer la ligne this.img.src = pSrc
    – et remplacer this.img = new Image() // par this.img = pSrc

    c’était mon erreur en tout cas

  5. Vous êtes des boss ;), moi aussi j’ai loupé l’information et grâce au differente solution que vous avez apportées, j’ai chercher le lien et ça confirme ce que dit « piopio65 » « la classe Sprite ne prend pas en 1er parametre une reference d’objet mais plutôt un path ».

    Rappel: path = chemin.

    Merci à vous.

  6. Petite astuce pour éviter de copier/coller 50 fois la partie imageLoader.add(" :
    Avec Visual Studio Code il suffit de faire un clic molette avant le début de la première image puis de descendre jusqu’en bas tout en restant avant le début du nom de l’image puis de coller avec CTRL+V.

    Le même principe s’applique pour coller à la fin la partie ")

  7. Si jamais quelqu’un veux un truc un tout petit peut plus propre.

    Copiez le nom des images comme David la fait mettez tout dans un tableau et après faites une
    bloucle for of avec un string interpolation

    exemple :

    const imagesArray = [ tout les noms des "images.png" ]

    for (let element of imagesArray) {
    imageLoader.add(`images/${element}`);
    }

  8. Oui en effet, dans l’atelier 1, Sprite prends un path là où dans l’atelier imageLoader, il prend directement une Image() chargée par l’imageLoader.

  9. Bonjour David merci pour le cours, cependant pour les images au lieu de faire comme tu fais j’ai préféré utiliser le fichier liste.txt qui est généré pour charger les images.

    Si ça intéresse certain, j’ai procédé comme ça :

    Ajout d’une méthode addFolder et je fais un request du fichier

    async addFolder(pPath, pFileList = "list.txt"){
    //use html request for read file, require('fs') doesn't work
    let rawLines = await this.#requestFile(pPath, pFileList);
    let lines = rawLines.split(/\r?\n/);
    lines.forEach(line => {
    if (line.endsWith(".png") || line.endsWith(".jpg"))
    this.add(pPath + "/" + line);
    });
    }

    #requestFile(pPath, pFile){
    return new Promise((resolve, reject) => {
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function(s, e) {
    if(xmlhttp.status == 200 && xmlhttp.readyState == 4){
    resolve(xmlhttp.responseText);
    }
    };
    xmlhttp.open("GET",pPath + "/" + pFile, true);
    xmlhttp.send();
    });
    }

    Et ensuite dans le game.js je fais :

    async function load(){
    await imageLoader.addFolder("images/card");
    imageLoader.start(startGame);
    }

    Comme ça même si j’ajoute des images au dossier, vu que je charge tout le contenu, j’ai juste besoin de généré un nouveau liste.txt

  10. Si vous souhaitez tester votre barre de chargement, vous pouvez faire F12, (dans chrome), aller dans network, vous avez un select, par défaut il est écrit No throttling, tu le passes en Slow 3G.

    Bon code !

  11. Super tuto!!! J’avais déja remarque ce problem de chargement en paralelle mais ce ImageLoader est vraiment une solution efficace merci beaucoup.
    Pour le loading des images je comprends qu’il faille le faire en manuel mais n’y aurait-il pas une solution sur le server un script PHP qui récupérer tous les images et genère un fichier js par example?

  12. Pour les animations il y a t’il une raison particulière pour avoir utilisé la taille en pixel d’une frame du spritesheet?
    Je voudrais utiliser l’inverse setTileSheet.. setTileSheet(NbColmn, NbRow) de la j’aurai déduit la taille en pixel this.width/NbColmn this.height/NbRow…
    Est-ce possible ? Non recommandé?

  13. Tu fais comme tu veux. Perso je trouve plus naturel de donner la taille des tiles, vu que c’est la base pour moi (peu importe l’image qui les contient). Mais ton idée est pas plus mauvaise que la mienne, l’important est que ça colle à ton raisonnement.

  14. Bonjour, Je suis bloquée à la dernier vidéo et je comprend vraiment pas ou j’ai raté (Et sa fait 1h que je repasse mon code et celui de la vidéo en boucle pour voir une différence °w°)

    Dans la console lorsque j’inspecte sur google chrome il me marque cette erreur:

    sprite.js:40 Uncaught TypeError: Cannot read properties of undefined (reading ‘0’)
    at sprite.js:40:65
    at Array.forEach ()
    at Sprite.startAnimation (sprite.js:37:25)
    at ImageLoader.startGame [as callBack] (game.js:70:18)
    at ImageLoader.imageLoaded (ImageLoader.js:45:18)

    Et la voici mon sprite.js (de la ligne 9 à 43 et 56 à 74)

    this.currentFrame = 0;
    this.currentAnimation = null;

    this.tileSize = {
    x:0,
    y:0
    }
    this.tileSheet = false;

    this.animations = [];
    }

    addAnimation(pName, pFrames, pSpeed, pLoop = true) {
    let animation = {
    name: pName,
    frame: pFrames,
    speed: pSpeed,
    loop: pLoop
    }
    this.animations.push(animation);
    }

    startAnimation(pName) {
    if (this.currentAnimation != null) {
    if (this.currentAnimation.name == pName) {
    return;
    }
    }
    this.animations.forEach(animation => {
    if (animation.name == pName) {
    this.currentAnimation = animation;
    this.currentFrame = this.currentAnimation.frames[0];
    }
    });
    }

    draw(pCtx) {
    if (!this.tileSheet) {
    pCtx.drawImage(this.img, this.x, this.y);
    }
    else {
    let nbCol = this.img.width / this.tileSize.x;
    let c = 0;
    let l = 0;

    l = Math.floor(this.currentFrame / nbCol);
    c = this.currentFrame – (l * nbCol);

    let x = c * this.tileSize.x;
    let y = l * this.tileSize.y;

    pCtx.drawImage(this.img, x, y, this.tileSize.x, this.tileSize.y, this.x, this.y, this.tileSize.x * this.scaleX, this.tileSize.y * this.scaleY);
    }
    }
    }

    Merci de bien vouloir m’aider et de m’accorder une partie de votre temps ^^’
    (Au cas ou je met la function « startGame » de game.js)

    function startGame() {
    console.log(« StartGame »)

    lstSprites = [];

    // Player
    let imagePlayer = imageLoader.getImage(« images/player.png »);
    let spritePlayer = new Sprite(imagePlayer);
    spritePlayer.setTileSheet(30, 16);
    spritePlayer.x = 25 * 4;
    spritePlayer.currentFrame = 12;
    spritePlayer.setScale(4, 4);
    spritePlayer.addAnimation(« TURNRIGHT », [0, 1, 2, 3, 4, 5, 6, 7, 8], 10, false);
    spritePlayer.addAnimation(« TURNUP », [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], 10, false)
    spritePlayer.startAnimation(« TURNUP »);

    // Red Ennemy
    let imageEnnemy = imageLoader.getImage(« images/enemyred.png »);
    let spriteEnemy = new Sprite(imageEnnemy);
    spriteEnemy.setTileSheet(24, 24);
    spriteEnemy.currentFrame = 3;
    spriteEnemy.setScale(4, 4);

    lstSprites.push(spritePlayer);
    lstSprites.push(spriteEnemy);

    gameReady = true;
    }

  15. Je suis en ce moment au cours sur l’ ImageLoader. Ce n’est pas évidement de te suivre car tu part un peu dans tous les sens mais tu si appelles çà « tentaculaire », pour un support pédagogique çà m’a un peu paumé. Ceci dit le bon coté c’est que j’ai du retourner ta classe ImagesLoader loader dans tous les sens pour l’isoler de l’héritage du contexte précédant et la manipuler à gros coup de console.log et console.warn pour voir ce que je récupérait et j’ai aboutit à quelque chose qui fonctionne très bien et que je comprends surtout. Voici ma version :

    /* ——————————————————————————————
    ——————————————————————————————–
    ——————– CLASSE SERVANT AUX CHARGEMENTS DES IMAGES ———————
    ——————————————————————————————–
    ——————————————————————————————–*/
    class ImagesLoader {
    // ————————————————-
    // Constructeur des différentes variable à utiliser
    // ————————————————-
    // on stock la liste des images à charger, avec leur chemins respectifs dans un tableau
    // on stock les fichiers qui ont été chargé en mémoire
    // on initialise un compteur d’image pour en connaitre la fin
    constructor() {
    this.tblPath = [];
    this.tblLoaded = [];
    this.Counter = 0;
    }

    // ——————————————————
    // Méthode qui sert à ajouter les chemins dans le tableau
    // ——————————————————
    // on « pousse » P_path dans tblpath
    addPath(P_path) {
    this.tblPath.push(P_path)
    }

    // —————————————————————————————
    // Méthode qui sert à parcours les chemins du tableau et de charger les images en mémoires
    // —————————————————————————————
    // on boucle sur chaque élément du tableau tblPath
    // on instancie un objet Image
    // on retourne la méthode de fin de chargement quand l’image est chargée
    // monImage.onload = this.endLoading.bind(this);
    // on prends le chemin (element) du tablea tblPath et on s’en sert d’attribue à l’image instanciée
    // à ce jour je ne comprends pas encore pourquoi on utilise pas push pour générer un index
    //this.tblDone.push(monImage);
    startLoading(P_callback) {
    this.callBack = P_callback;
    this.tblPath.forEach(element => {
    if (!this.tblLoaded[element]) {
    let monImage = new Image();
    monImage.onload = () => this.endLoading(monImage, element);
    monImage.src = element;
    this.tblLoaded.push(monImage);
    this.tblLoaded[element] = true;
    }
    });
    }
    // —————————————————————————————
    // Méthode qui sert à comtper le nombre d’image chargées et de savoir qi tout a été chargé
    // —————————————————————————————
    // on le test pour voir s’il est égale à la longeur du tableau
    // si c’est le cas on a fini
    endLoading(P_monImage, P_element) {
    this.Counter++;
    console.log(this.Counter);
    console.log(« Image chargée : », P_element);

    if(this.Counter === this.tblPath.length) {
    console.warn(« Toutes les images ont ete chargées »);
    this.callBack();
    }
    };
    }

    // création d’une instance de la classe ImagesLoader sous le nom de Animation_Perso
    let animation_Perso = new ImagesLoader();
    console.warn(« Je créer un objet « , animation_Perso)

    // ———————————————————————
    // ———————————————————————
    // ———— CHARGEMENT DES IMAGES EN MÉMOIRE ————-
    // ———————————————————————
    // ———————————————————————
    // on ajoute des chemins d’image au tableau!
    animation_Perso.addPath(‘../assets/img/anim1.png’);
    animation_Perso.addPath(‘../assets/img/anim2.png’);
    animation_Perso.addPath(‘../assets/img/anim3.png’);
    animation_Perso.addPath(‘../assets/img/anim4.png’);
    animation_Perso.addPath(‘../assets/img/anim5.png’);
    animation_Perso.addPath(‘../assets/img/anim6.png’);
    animation_Perso.addPath(‘../assets/img/anim7.png’);
    animation_Perso.addPath(‘../assets/img/anim8.png’);
    animation_Perso.addPath(‘../assets/img/anim9.png’);
    console.warn(« Les chemins des images sont ajouté dans le tableau tblPath : »,animation_Perso.tblPath);

    // on execute le chargement des images dans le tableau tblDone à partir des chemins du tableau tblPath
    animation_Perso.startLoading(() => {
    console.warn(« Cette callback s’execute une seule fois quand toutes les images ont été chargées »);

    });
    console.warn(« Un nouveau tableau est remplis »,animation_Perso.tblLoaded)

Laisser un commentaire

Dialoguez avec les autres membres de la gamecodeur school.

Accédez maintenant à notre serveur Discord privé : Entraide, Game Jams, Partage de projets, etc.

Vous devez être membre de la Gamecodeur School Premium pour être autorisé à accéder au serveur.