Accueil > Intranet Michel Buffa > Master 1 IFI 2015-2016 cours Web Sciences / HTML5 / JS

Master 1 IFI 2015-2016 cours Web Sciences / HTML5 / JS

De $1

Introduction

Ce cours a pour but de vous familiariser avec la programmation graphique en JavaScript. Lors de cette session nous utiliserons beaucoup l'élément <canvas> de HTML5 pour dessiner et une API (Application Programming Interface) standard elle aussi de HTML5 pour l'animation à 60 images/s (si possible), intitulée RequestAnimationFrame.

Ce sera l'occasion de rappeler les principales technologies du Web, et voir comment à travers des exemples simples de programmation graphique, on peut mettre en application des outils mathématiques de niveau lycée.

JavaScript a l'avantage de fonctionner directement dans un navigateur Web, les outils minimums nécessaires seront dont un navigateur web récent (> 2012). Optionellement, un bon éditeur de code source comme Sublime Text pourra être un plus.

Nous travaillerons ensembles sur l'écriture d'un petit moteur d'animation reprenant les caractéristiques principales d'un moteur graphique pour écrire un jeu vidéo:

  • Boucle d'animation à 60 images / seconde,
  • Interaction avec l'utilisateur via clavier / souris,
  • Animation basée sur le temps (en effet, selon la machine cible, la complexité de la scène graphique, des calculs, on ne pourra pas toujours être exactement à 60 images/s, il faudra donc calculer les déplacements des objets graphiques à l'écran en fonction du temps réellement écoulé depuis la dernière image).
  • Détection de collisions,
  • Transformation d'un exemple à priori "simple et amusant" pour résoudre un vrai problème de recherche appliqué à la dépollution des océans !

Première partie : introduction des technologies Web, panorama général

Ressources à garder dans un coin et à regarder si on est curieux :

JavaScript :

CSS :

  • http://miageprojet2.unice.fr/Intranet_de_Michel_Buffa/Exemples_interactifs_CSS
  • Les tutoriaux de codeacademy.com sont très bien et gratuits

HTML5 :

Seconde partie : écriture d'un moteur d'animation

Remarque : La suite de ce document est une version simplifiée du chapitre sur la programmation de jeux du MOOC HTML5 partie 2. Je vous conseille d'aller voir le MOOC pour avoir beaucoup plus de détails, des vidéos, des exemples supplémentaires etc.

Tout jeu, logiciel animé utilise ce qu'on appelle communément une "boucle d'animation" ou une "game loop" dans le jargon des créateurs de jeu vidéo.

Cette boucle est le composant principal. Elle sépare la logique du "jeu" et sa couche graphique des interactions de l'utilisateur via le clavier, souris ou manette.

Les applications classiques, comme un traitement de texte, une base de données, une calculatrice, ne font rien quand l'utilisateur ne fait rien. Ce sont des applications qui n'agissent que lorsque l'utilisateur interagit avec l'application, en tapant sur le clavier par exemple. Quand l'utilisateur ne fait rien, l'application en fait qu'attendre qu'il fasse quelque chose !

gameloop.png

Dans le cas d'un jeu c'est différent. Il attend toujours les interactions de l'utilisateur mais au lieu de ne rien faire d'autre que attendre, le jeu va déplacer, dessiner, animer des objets graphiques à une fréquence élevée, tester des collisions, jouer de la musique, etc. Pour des raisons historiques, on a fixé le seuil de fluidité à 60 images / secondes.

Il y a plusieurs manière de coder une "game loop" en JavaScript.

Utilisation de la fonction setInterval(...)

La fonction setInterval permet d'appeler une autre fonction à intervalle régulier (en ms). 

Voici un exemple en ligne: http://jsbin.com/yuhule/3/edit

setInterval example.jpg

Code source :

  1. var addStarToTheBody = function(){  
  2.     document.body.innerHTML += "*";  
  3. };  
  4.    
  5. //this will add one star to the document each 200ms (1/5s)  
  6. setInterval(addStarToTheBody, 200);  

Beaucoup mieux: utilisation de RequestAnimationFrame

 HTML5 a introduit à la demande des développeurs de jeux et d'applications musicales une API permettant de faire une "game loop" à 60 images par seconde, avec de nombreuses optimisations :

  • La boucle sera exécutée à une fréquence la plus près possible de 60 images/s,
  • On pourra connaitre très précisément le temps écoulé entre deux frames d'animation,
  • Si la zone à animer n'est pas visible (onglet caché, navigateur iconifié, page scrollée), seuls les ordres non graphiques seront exécutés, cela permet d'économiser énormément de CPU, de faire des jeux sur mobiles qui ne videront pas la batterie,etc.

Exemple typique : http://jsbin.com/gixepe/2/edit

Code source :

  1. window.onload = function init() {  
  2.    // called after the page is entirely loaded  
  3.    requestAnimationFrame(mainloop);  
  4. };    
  5.    
  6. function mainloop(time) {  
  7.    document.body.innerHTML += "*";  
  8.    
  9.    // call back itself every 60th of second  
  10.    requestAnimationFrame(mainloop);  
  11. }  

Notez que la dernière ligne de la boucle d'animation est simplement une demande au navigateur de re-exécuter cette même boucle d'animation (requestAnimationFrame = "je demande une nouvelle frame d'animation). Le paramètre est la fonction qui implémente la boucle d'animation. C'est une "sorte" d'appel récursif si ce n'est que cet appel est "asynchrone" : en attendant le rappel, le navigateur n'est pas bloqué, d'autres instructions peuvent s'exécuter etc.

Notez qu'on peut avoir plusieurs game loops simultanément dans un même programme. Elles sont toutes "en tâche de fond", non bloquantes et entre chaque exécution le programme peut réagir aux interactions clavier, souris, etc.

Implémentation d'un petit "moteur de jeu" 

Voici le petit squelette d'un jeu:

  1. var GF = function(){  
  2.    
  3.     var mainLoop = function(time){  
  4.         //main function, called each frame  
  5.         requestAnimationFrame(mainLoop);  
  6.     };  
  7.    
  8.     var start = function(){  
  9.         requestAnimationFrame(mainLoop);  
  10.     };  
  11.    
  12.     //our GameFramework returns a public API visible from outside its scope  
  13.     return {  
  14.         start: start  
  15.     };  
  16. };  

Et voici comment avec ce squelette on créera un jeu :

 

  1. var game = new GF();  
  2.    
  3. // Launch the game, start the animation loop etc.  
  4. game.start();  

Rajoutons donc quelque chose dans la game loop :

 

  1. var mainLoop = function(time){  
  2.    //main function, called each frame  
  3.    document.body.innerHTML = Math.random();  
  4.    
  5.    // call the animation loop every 1/60th of second  
  6.    requestAnimationFrame(mainLoop);  
  7. };  

Et voici l'exemple complet testable :  http://jsbin.com/kafehi/3/edit

Compter le nombre d'images par seconde

Voici une fonction "measureFPS" qui permet de compter le nombre d'images par seconde :

 

  1. // vars for counting frames/s, used by the measureFPS function  
  2.     var frameCount = 0;  
  3.     var lastTime;  
  4.     var fpsContainer;  
  5.     var fps;   
  6.    
  7.     var measureFPS = function(newTime){  
  8.    
  9.          // test for the very first invocation  
  10.          if(lastTime === undefined) {  
  11.            lastTime = newTime;   
  12.            return;  
  13.          }  
  14.    
  15.         //calculate the difference between last & current frame  
  16.         var diffTime = newTime - lastTime;   
  17.    
  18.         if (diffTime >= 1000) {  
  19.             fps = frameCount;      
  20.             frameCount = 0;  
  21.             lastTime = newTime;  
  22.         }  
  23.    
  24.         //and display it in an element we appended to the   
  25.         // document in the start() function  
  26.        fpsContainer.innerHTML = 'FPS: ' + fps;   
  27.        frameCount++;  
  28.     };  

On appellera cette fonction depuis la boucle principale :

 

  1. var mainLoop = function(time){  
  2.         //main function, called each frame   
  3.         measureFPS(time);  
  4.    
  5.         // call the animation loop every 1/60th of second  
  6.         requestAnimationFrame(mainLoop);  
  7.     };  

Dans la fonction start() du moteur de jeu, nous créons un simple élément <div> html pour afficher le nombre d'images par seconde :

 

  1. var start = function(){  
  2.         // adds a div for displaying the fps value  
  3.         fpsContainer = document.createElement('div');  
  4.         document.body.appendChild(fpsContainer);  
  5.    
  6.         requestAnimationFrame(mainLoop);  
  7.     };  

Exemple testable en ligne :  http://jsbin.com/kafehi/5/edit

Et oh ???? Où sont les graphismes ???? L'élément CANVAS de HTML5

Ah ah... pour faire du dessin il faut utiliser l'élément <canvas> de HTML5 et son API JavaScript.

Voici un exemple qui dessine un petit monstre dans la boucle d'animation, à l'aide de fonctions de dessin de rectangles : http://jsbin.com/pagipi/2/edit

monster.jpg

Code HTML :

  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.   <meta charset="utf-8">  
  5.   <title>JS Bin</title>  
  6. </head>  
  7. <body>  
  8.    
  9.   <canvas id="myCanvas" width="200" height="200"></canvas>  
  10.    
  11. </body>  
  12. </html>  

Code CSS très simple, pour voir la bordure du canvas : 

 

  1. canvas {  
  2.   border1px solid black;  
  3. }  

Bonnes pratiques pour utiliser le canvas :

  1. Utiliser une fonction init() appelée uniquement quand la page est entièrement chargée,
  2. Dans cette fonction : récupérer un pointeur sur l'élément canvas à l'aide de l'API du DOM de HTML5,
  3. Récupérer un objet spécial appelé le "contexte graphique", qui lui seul permettra de dessiner, changer les couleurs, etc...
  4. Alors seulement on pourra dessiner.

Code JavaScript de l'exemple :

  1. // useful to have them as global variables  
  2. var canvas, ctx, w, h;   
  3.    
  4.    
  5. window.onload = function init() {  
  6.     // called AFTER the page has been loaded  
  7.     canvas = document.querySelector("#myCanvas");  
  8.    
  9.     // often useful  
  10.     w = canvas.width;   
  11.     h = canvas.height;    
  12.    
  13.     // important, we will draw with this object  
  14.     ctx = canvas.getContext('2d');  
  15.    
  16.     // ready to go !  
  17.     drawMyMonster();  
  18. };  
  19.    
  20. function drawMyMonster() {  
  21.     // draw a big monster !  
  22.     // head  
  23.     ctx.strokeRect(10, 10, 100, 100);  
  24.    
  25.     // eyes  
  26.     ctx.fillRect(30, 30, 10, 10);  
  27.     ctx.fillRect(75, 30, 10, 10);  
  28.    
  29.     // nose  
  30.     ctx.strokeRect(55, 50, 10, 40);  
  31.    
  32.     // mouth  
  33.    ctx.strokeRect(45, 94, 30, 10);  
  34.    
  35.    // teeth  
  36.    ctx.fillRect(48, 94, 10, 10);  
  37.    ctx.fillRect(62, 94, 10, 10);  
  38. }  

Introduction aux transformations géométriques 2D

L'API du canvas contient des fonctions propres à la gestion des repères / aux transformations géométriques très pratiques. Par ailleurs, on peut empiler/sauvegarder et dépiler/restaurer des contextes graphiques, ce qui va permettra de travailler avec plusieurs repères.

Par exemple, le code ci-dessous est équivalent au code de l'exemple précédent, si ce n'est que le monstre est maintenant dessiné en (0, 0) mais on translate le repère avant de le dessiner :

 

  1. function drawMyMonster(x, y) {  
  2.     // draw a big monster !  
  3.     // head  
  4.    
  5.     // save the context  
  6.     ctx.save();  
  7.    
  8.     // translate the coordinate system, draw relative to it  
  9.     ctx.translate(x, y);  
  10.    
  11.     // (0, 0) is the top left corner of the monster.  
  12.     ctx.strokeRect(0, 0, 100, 100);  
  13.    
  14.     // eyes  
  15.     ctx.fillRect(20, 20, 10, 10);  
  16.     ctx.fillRect(65, 20, 10, 10);  
  17.    
  18.     // nose  
  19.     ctx.strokeRect(45, 40, 10, 40);  
  20.    
  21.     // mouth  
  22.    ctx.strokeRect(35, 84, 30, 10);  
  23.    
  24.    // teeth  
  25.    ctx.fillRect(38, 84, 10, 10);  
  26.    ctx.fillRect(52, 84, 10, 10);  
  27.    
  28.    // restore the context  
  29.    ctx.restore();  
  30. }  

L'appel de cette fonction est un peu différent maintenant :

  1. // Try to change the parameter values to move  
  2.     // the monster  
  3.     drawMyMonster(10, 10);  

Vous pouvez tester cette version ici : http://jsbin.com/pagipi/3/edit, essayez donc de changer les valeurs des paramètres !

Animer le monstre, l'inclure dans notre moteur d'animation

Nous allons modifier légèrement le moteur d'animation développé au début de ce tutorial :

  • Ajout du canvas à la page HTML,
  • Ajout de la fonction init()
  • Ajout de la fonction qui dessine le monstre,
  • Dans la boucle d'animation nous allons : 1) effacer le canvas, 2) dessiner le monstre, 3) rappeler la boucle d'animation

Version en ligne à tester : http://jsbin.com/kafehi/7/edit

Code HTML : 

  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.   <meta charset="utf-8">  
  5.   <title>JS Bin</title>  
  6. </head>  
  7. <body>  
  8.   <canvas id="myCanvas"  width="200" height="200"></canvas>  
  9. </body>  
  10. </html>  

Code JavaScript complet :

  1. // Inits  
  2. window.onload = function init() {  
  3.   var game = new GF();  
  4.   game.start();  
  5. };  
  6.    
  7.    
  8. // GAME FRAMEWORK STARTS HERE  
  9. var GF = function(){  
  10.     // Vars relative to the canvas  
  11.     var canvas, ctx, w, h;   
  12.    
  13.     // vars for counting frames/s, used by the measureFPS function  
  14.     var frameCount = 0;  
  15.     var lastTime;  
  16.     var fpsContainer;  
  17.     var fps;   
  18.    
  19.     var measureFPS = function(newTime){  
  20.    
  21.          // test for the very first invocation  
  22.          if(lastTime === undefined) {  
  23.            lastTime = newTime;   
  24.            return;  
  25.          }  
  26.    
  27.         //calculate the difference between last & current frame  
  28.         var diffTime = newTime - lastTime;   
  29.    
  30.         if (diffTime >= 1000) {  
  31.             fps = frameCount;      
  32.             frameCount = 0;  
  33.             lastTime = newTime;  
  34.         }  
  35.    
  36.         //and display it in an element we appended to the   
  37.         // document in the start() function  
  38.        fpsContainer.innerHTML = 'FPS: ' + fps;   
  39.        frameCount++;  
  40.     };  
  41.    
  42.      // clears the canvas content  
  43.      function clearCanvas() {  
  44.        ctx.clearRect(0, 0, w, h);  
  45.      }  
  46.    
  47.      // Functions for drawing the monster and maybe other objects  
  48.      function drawMyMonster(x, y) {  
  49.        // draw a big monster !  
  50.        // head  
  51.    
  52.        // save the context  
  53.        ctx.save();  
  54.    
  55.        // translate the coordinate system, draw relative to it  
  56.        ctx.translate(x, y);  
  57.    
  58.        // (0, 0) is the top left corner of the monster.  
  59.        ctx.strokeRect(0, 0, 100, 100);  
  60.    
  61.        // eyes  
  62.        ctx.fillRect(20, 20, 10, 10);  
  63.        ctx.fillRect(65, 20, 10, 10);  
  64.    
  65.        // nose  
  66.        ctx.strokeRect(45, 40, 10, 40);  
  67.    
  68.        // mouth  
  69.        ctx.strokeRect(35, 84, 30, 10);  
  70.    
  71.        // teeth  
  72.        ctx.fillRect(38, 84, 10, 10);  
  73.        ctx.fillRect(52, 84, 10, 10);  
  74.    
  75.       // restore the context  
  76.       ctx.restore();   
  77.     }  
  78.    
  79.     var mainLoop = function(time){  
  80.         //main function, called each frame   
  81.         measureFPS(time);  
  82.    
  83.         // Clear the canvas  
  84.         clearCanvas();  
  85.    
  86.         // draw the monster  
  87.         drawMyMonster(10+Math.random()*10, 10+Math.random()*10);  
  88.    
  89.         // call the animation loop every 1/60th of second  
  90.         requestAnimationFrame(mainLoop);  
  91.     };  
  92.    
  93.     var start = function(){  
  94.         // adds a div for displaying the fps value  
  95.         fpsContainer = document.createElement('div');  
  96.         document.body.appendChild(fpsContainer);  
  97.    
  98.         // Canvas, context etc.  
  99.         canvas = document.querySelector("#myCanvas");  
  100.    
  101.         // often useful  
  102.         w = canvas.width;   
  103.         h = canvas.height;    
  104.    
  105.         // important, we will draw with this object  
  106.         ctx = canvas.getContext('2d');  
  107.    
  108.         // start the animation  
  109.         requestAnimationFrame(mainLoop);  
  110.     };  
  111.    
  112.     //our GameFramework returns a public API visible from outside its scope  
  113.     return {  
  114.         start: start  
  115.     };  
  116. };  

Remarquez que 99% de la fonction init() de l'exemple précédent a été déplacé dans la fonction start() du moteur d'animation. Notez aussi que pour voir le monstre bouger on a ajouté des valeurs aléatoires à sa position, ainsi il "tremble".

Interactions avec l'utilisateur

Gestion du clavier

En JavaScript on définit des "écouteurs" que l'on associe à certains éléments HTML (par exemple le canvas) et qui vont écouter certains événements (appui sur une touche, déplacement ou click souris), de manière "non blocante" (on dit : "asynchrone"). Comme pendant ce temps, la boucle d'animation tourne, on va simplement changer des "états" que l'on stocke dans un objet JavaScript et ces états seront consultés 60 fois par seconde depuis la boucle d'animation. 

On veut aussi pouvoir détecter l'appui de touches simultanés et la souris (déplacements, click), en même temps, dans la boucle. On va donc utiliser un objet qui contiendra la liste des touches enfoncées, une valeur pour dire si un bouton de la souris est enfoncé ou non, la position du curseur de la souris, etc...

Détection de l'appui sur une touche :

  1. window.addEventListener('keydown'function(event) {  
  2.     if (event.keyCode === 37) {  
  3.         //left arrow was pressed  
  4.     }  
  5. }, false);  

Ici on a défini un écouteur sur l'objet "window" (la fenêtre entière, la page web complète), pour l'événement "keydown", et la fonction qui traitera cet événement est le dernier paramètre. Elle prend en paramètre l'événement détecté. C'est un objet JavaScript qui a dans ses propriétés des informations relatives à l'événement : ici le code de la touche pressée.

Liste des codes :

JSKeyCodes.jpg

 On peut tester interactivement les codes ici : http://www.asquare.net/javascript/tests/KeyCode.html

Ajout de la détection de touches dans notre moteur d'animation

Exemple en ligne : http://jsbin.com/kafehi/9/edit

inputKeys.jpg

Code source :

  1. // Inits  
  2. window.onload = function init() {  
  3.   var game = new GF();  
  4.   game.start();  
  5. };  
  6.    
  7.    
  8. // GAME FRAMEWORK STARTS HERE  
  9. var GF = function(){  
  10.     // Vars relative to the canvas  
  11.     var canvas, ctx, w, h;   
  12.    
  13.     // vars for counting frames/s, used by the measureFPS function  
  14.     var frameCount = 0;  
  15.     var lastTime;  
  16.     var fpsContainer;  
  17.     var fps;   
  18.    
  19.     // vars for handling inputs  
  20.     var inputStates = {};  
  21.    
  22.     var measureFPS = function(newTime){  
  23.    
  24.          // test for the very first invocation  
  25.          if(lastTime === undefined) {  
  26.            lastTime = newTime;   
  27.            return;  
  28.          }  
  29.    
  30.         //calculate the difference between last & current frame  
  31.         var diffTime = newTime - lastTime;   
  32.    
  33.         if (diffTime >= 1000) {  
  34.             fps = frameCount;      
  35.             frameCount = 0;  
  36.             lastTime = newTime;  
  37.         }  
  38.    
  39.         //and display it in an element we appended to the   
  40.         // document in the start() function  
  41.        fpsContainer.innerHTML = 'FPS: ' + fps;   
  42.        frameCount++;  
  43.     };  
  44.    
  45.      // clears the canvas content  
  46.      function clearCanvas() {  
  47.        ctx.clearRect(0, 0, w, h);  
  48.      }  
  49.    
  50.      // Functions for drawing the monster and maybe other objects  
  51.      function drawMyMonster(x, y) {  
  52.        // draw a big monster !  
  53.        // head  
  54.    
  55.        // save the context  
  56.        ctx.save();  
  57.    
  58.        // translate the coordinate system, draw relative to it  
  59.        ctx.translate(x, y);  
  60.    
  61.        // (0, 0) is the top left corner of the monster.  
  62.        ctx.strokeRect(0, 0, 100, 100);  
  63.    
  64.        // eyes  
  65.        ctx.fillRect(20, 20, 10, 10);  
  66.        ctx.fillRect(65, 20, 10, 10);  
  67.    
  68.        // nose  
  69.        ctx.strokeRect(45, 40, 10, 40);  
  70.    
  71.        // mouth  
  72.        ctx.strokeRect(35, 84, 30, 10);  
  73.    
  74.        // teeth  
  75.        ctx.fillRect(38, 84, 10, 10);  
  76.        ctx.fillRect(52, 84, 10, 10);  
  77.    
  78.       // restore the context  
  79.       ctx.restore();   
  80.     }  
  81.    
  82.     var mainLoop = function(time){  
  83.         //main function, called each frame   
  84.         measureFPS(time);  
  85.    
  86.         // Clear the canvas  
  87.         clearCanvas();  
  88.    
  89.         // draw the monster  
  90.         drawMyMonster(10+Math.random()*10, 10+Math.random()*10);  
  91.         // check inputStates  
  92.         if (inputStates.left) {  
  93.             ctx.fillText("left", 150, 20);  
  94.         }  
  95.         if (inputStates.up) {  
  96.             ctx.fillText("up", 150, 50);  
  97.         }  
  98.        if (inputStates.right) {  
  99.             ctx.fillText("right", 150, 80);  
  100.         }  
  101.         if (inputStates.down) {  
  102.             ctx.fillText("down", 150, 120);  
  103.         }   
  104.         if (inputStates.space) {  
  105.             ctx.fillText("space bar", 140, 150);  
  106.         }   
  107.    
  108.         // call the animation loop every 1/60th of second  
  109.         requestAnimationFrame(mainLoop);  
  110.     };  
  111.    
  112.     var start = function(){  
  113.         // adds a div for displaying the fps value  
  114.         fpsContainer = document.createElement('div');  
  115.         document.body.appendChild(fpsContainer);  
  116.    
  117.         // Canvas, context etc.  
  118.         canvas = document.querySelector("#myCanvas");  
  119.    
  120.         // often useful  
  121.         w = canvas.width;   
  122.         h = canvas.height;    
  123.    
  124.         // important, we will draw with this object  
  125.         ctx = canvas.getContext('2d');  
  126.         // default police for text  
  127.         ctx.font="20px Arial";  
  128.    
  129.        //add the listener to the main, window object, and update the states  
  130.       window.addEventListener('keydown'function(event){  
  131.           if (event.keyCode === 37) {  
  132.              inputStates.left = true;  
  133.           } else if (event.keyCode === 38) {  
  134.              inputStates.up = true;  
  135.           } else if (event.keyCode === 39) {  
  136.              inputStates.right = true;  
  137.           } else if (event.keyCode === 40) {  
  138.              inputStates.down = true;  
  139.           }  else if (event.keyCode === 32) {  
  140.              inputStates.space = true;  
  141.           }  
  142.       }, false);  
  143.    
  144.       //if the key will be released, change the states object   
  145.       window.addEventListener('keyup'function(event){  
  146.           if (event.keyCode === 37) {  
  147.              inputStates.left = false;  
  148.           } else if (event.keyCode === 38) {  
  149.              inputStates.up = false;  
  150.           } else if (event.keyCode === 39) {  
  151.              inputStates.right = false;  
  152.           } else if (event.keyCode === 40) {  
  153.              inputStates.down = false;  
  154.           } else if (event.keyCode === 32) {  
  155.              inputStates.space = false;  
  156.           }  
  157.       }, false);  
  158.    
  159.    
  160.         // start the animation  
  161.         requestAnimationFrame(mainLoop);  
  162.     };  
  163.    
  164.     //our GameFramework returns a public API visible from outside its scope  
  165.     return {  
  166.         start: start  
  167.     };  
  168. };  

Detection des événements souris

Pour la souris on va procéder de même si ce n'est que l'on va s'intéresser à trois types d'évènements : déplacement, appui et relâchement d'un bouton.

La détection de la position de la souris nécessite un petit traitement particulier car en HTML les éléments peuvent avoir une bordure et des marges intérieures et extérieures. La position du coin en haut à gauche (0, 0) doit être ajustée en fonction de ces marges. C'est ce qui est fait dans la fonction qui renvoit la position du curseur un peu plus bas dans cette section.

Exemple en ligne : http://jsbin.com/bizudu/4/edit

mouseListener.jpg

Code source :

  1. var canvas, ctx;  
  2. var inputStates = {};  
  3.    
  4. window.onload = function init() {  
  5.     canvas = document.getElementById('myCanvas');  
  6.     ctx = canvas.getContext('2d');  
  7.    
  8.     canvas.addEventListener('mousemove'function (evt) {  
  9.         inputStates.mousePos = getMousePos(canvas, evt);  
  10.         var message = 'Mouse position: ' + inputStates.mousePos.x + ',' + inputStates.mousePos.y;  
  11.         writeMessage(canvas, message);  
  12.     }, false);  
  13.    
  14.       canvas.addEventListener('mousedown'function (evt) {  
  15.         inputStates.mousedown = true;  
  16.         inputStates.mouseButton = evt.button;  
  17.         var message = "Mouse button " + evt.button + " down at position: " + inputStates.mousePos.x + ',' + inputStates.mousePos.y;  
  18.         writeMessage(canvas, message);  
  19.     }, false);  
  20.    
  21.         canvas.addEventListener('mouseup'function (evt) {  
  22.         inputStates.mousedown = false;  
  23.         var message = "Mouse up at position: " + inputStates.mousePos.x + ',' + inputStates.mousePos.y;  
  24.         writeMessage(canvas, message);  
  25.     }, false);  
  26. };  
  27.    
  28. function writeMessage(canvas, message) {  
  29.     var ctx = canvas.getContext('2d');  
  30.     ctx.save();  
  31.     ctx.clearRect(0, 0, canvas.width, canvas.height);  
  32.     ctx.font = '18pt Calibri';  
  33.     ctx.fillStyle = 'black';  
  34.     ctx.fillText(message, 10, 25);  
  35.     ctx.restore();  
  36. }  
  37.    
  38. function getMousePos(canvas, evt) {  
  39.     // necessary to take into account CSS boudaries  
  40.     var rect = canvas.getBoundingClientRect();  
  41.     return {  
  42.         x: evt.clientX - rect.left,  
  43.         y: evt.clientY - rect.top  
  44.     };  
  45. }  

Ajout de l'écouteur souris dans le moteur d'animation

Exemple en ligne : http://jsbin.com/kafehi/12/edit

mouseEvents.jpg

Code source :

  1. // Inits  
  2. window.onload = function init() {  
  3.   var game = new GF();  
  4.   game.start();  
  5. };  
  6.    
  7.    
  8. // GAME FRAMEWORK STARTS HERE  
  9. var GF = function(){  
  10.     // Vars relative to the canvas  
  11.     var canvas, ctx, w, h;   
  12.    
  13.     // vars for counting frames/s, used by the measureFPS function  
  14.     var frameCount = 0;  
  15.     var lastTime;  
  16.     var fpsContainer;  
  17.     var fps;   
  18.    
  19.     // vars for handling inputs  
  20.     var inputStates = {};  
  21.    
  22.     var measureFPS = function(newTime){  
  23.    
  24.          // test for the very first invocation  
  25.          if(lastTime === undefined) {  
  26.            lastTime = newTime;   
  27.            return;  
  28.          }  
  29.    
  30.         //calculate the difference between last & current frame  
  31.         var diffTime = newTime - lastTime;   
  32.    
  33.         if (diffTime >= 1000) {  
  34.             fps = frameCount;      
  35.             frameCount = 0;  
  36.             lastTime = newTime;  
  37.         }  
  38.    
  39.         //and display it in an element we appended to the   
  40.         // document in the start() function  
  41.        fpsContainer.innerHTML = 'FPS: ' + fps;   
  42.        frameCount++;  
  43.     };  
  44.    
  45.      // clears the canvas content  
  46.      function clearCanvas() {  
  47.        ctx.clearRect(0, 0, w, h);  
  48.      }  
  49.    
  50.      // Functions for drawing the monster and maybe other objects  
  51.      function drawMyMonster(x, y) {  
  52.        // draw a big monster !  
  53.        // head  
  54.    
  55.        // save the context  
  56.        ctx.save();  
  57.    
  58.        // translate the coordinate system, draw relative to it  
  59.        ctx.translate(x, y);  
  60.    
  61.        // (0, 0) is the top left corner of the monster.  
  62.        ctx.strokeRect(0, 0, 100, 100);  
  63.    
  64.        // eyes  
  65.        ctx.fillRect(20, 20, 10, 10);  
  66.        ctx.fillRect(65, 20, 10, 10);  
  67.    
  68.        // nose  
  69.        ctx.strokeRect(45, 40, 10, 40);  
  70.    
  71.        // mouth  
  72.        ctx.strokeRect(35, 84, 30, 10);  
  73.    
  74.        // teeth  
  75.        ctx.fillRect(38, 84, 10, 10);  
  76.        ctx.fillRect(52, 84, 10, 10);  
  77.    
  78.       // restore the context  
  79.       ctx.restore();   
  80.     }  
  81.    
  82.     var mainLoop = function(time){  
  83.         //main function, called each frame   
  84.         measureFPS(time);  
  85.    
  86.         // Clear the canvas  
  87.         clearCanvas();  
  88.    
  89.         // draw the monster  
  90.         drawMyMonster(10+Math.random()*10, 10+Math.random()*10);  
  91.         // check inputStates  
  92.         if (inputStates.left) {  
  93.             ctx.fillText("left", 150, 20);  
  94.         }  
  95.         if (inputStates.up) {  
  96.             ctx.fillText("up", 150, 40);  
  97.         }  
  98.        if (inputStates.right) {  
  99.             ctx.fillText("right", 150, 60);  
  100.         }  
  101.         if (inputStates.down) {  
  102.             ctx.fillText("down", 150, 80);  
  103.         }   
  104.         if (inputStates.space) {  
  105.             ctx.fillText("space bar", 140, 100);  
  106.         }  
  107.         if (inputStates.mousePos) {   
  108.             ctx.fillText("x = " + inputStates.mousePos.x + " y = " + inputStates.mousePos.y, 5, 150);  
  109.         }  
  110.       if (inputStates.mousedown) {   
  111.             ctx.fillText("mousedown b" + inputStates.mouseButton, 5, 180);  
  112.         }  
  113.    
  114.         // call the animation loop every 1/60th of second  
  115.         requestAnimationFrame(mainLoop);  
  116.     };  
  117.    
  118.    
  119.     function getMousePos(evt) {  
  120.         // necessary to take into account CSS boudaries  
  121.         var rect = canvas.getBoundingClientRect();  
  122.         return {  
  123.             x: evt.clientX - rect.left,  
  124.             y: evt.clientY - rect.top  
  125.         };  
  126.     }  
  127.    
  128.     var start = function(){  
  129.         // adds a div for displaying the fps value  
  130.         fpsContainer = document.createElement('div');  
  131.         document.body.appendChild(fpsContainer);  
  132.    
  133.         // Canvas, context etc.  
  134.         canvas = document.querySelector("#myCanvas");  
  135.    
  136.         // often useful  
  137.         w = canvas.width;   
  138.         h = canvas.height;    
  139.    
  140.         // important, we will draw with this object  
  141.         ctx = canvas.getContext('2d');  
  142.         // default police for text  
  143.         ctx.font="20px Arial";  
  144.    
  145.        //add the listener to the main, window object, and update the states  
  146.       window.addEventListener('keydown'function(event){  
  147.           if (event.keyCode === 37) {  
  148.              inputStates.left = true;  
  149.           } else if (event.keyCode === 38) {  
  150.              inputStates.up = true;  
  151.           } else if (event.keyCode === 39) {  
  152.              inputStates.right = true;  
  153.           } else if (event.keyCode === 40) {  
  154.              inputStates.down = true;  
  155.           }  else if (event.keyCode === 32) {  
  156.              inputStates.space = true;  
  157.           }  
  158.       }, false);  
  159.    
  160.       //if the key will be released, change the states object   
  161.       window.addEventListener('keyup'function(event){  
  162.           if (event.keyCode === 37) {  
  163.              inputStates.left = false;  
  164.           } else if (event.keyCode === 38) {  
  165.              inputStates.up = false;  
  166.           } else if (event.keyCode === 39) {  
  167.              inputStates.right = false;  
  168.           } else if (event.keyCode === 40) {  
  169.              inputStates.down = false;  
  170.           } else if (event.keyCode === 32) {  
  171.              inputStates.space = false;  
  172.           }  
  173.       }, false);  
  174.    
  175.       // Mouse event listeners  
  176.       canvas.addEventListener('mousemove'function (evt) {  
  177.           inputStates.mousePos = getMousePos(evt);  
  178.       }, false);  
  179.    
  180.       canvas.addEventListener('mousedown'function (evt) {  
  181.             inputStates.mousedown = true;  
  182.             inputStates.mouseButton = evt.button;  
  183.       }, false);  
  184.    
  185.       canvas.addEventListener('mouseup'function (evt) {  
  186.           inputStates.mousedown = false;  
  187.       }, false);        
  188.    
  189.    
  190.         // start the animation  
  191.         requestAnimationFrame(mainLoop);  
  192.     };  
  193.    
  194.     //our GameFramework returns a public API visible from outside its scope  
  195.     return {  
  196.         start: start  
  197.     };  
  198. };  

Remarquez qu'on a ajouté tous les écouteurs dans la fonction start() qui est exécutée au tout début de la création du moteur.

Déplacement du joueur à l'aide des flèches du clavier, changement de vitesse avec la souris

Exemple en ligne : http://jsbin.com/bemebi/2/edit

On a simplement créé un objet correspondant au monstre/joueur, contenant des propriétés telles que sa position et sa vitesse :

  1. // The monster !  
  2.     var monster = {  
  3.       x:10,  
  4.       y:10,  
  5.       speed:1  
  6.     };  

Ces propriétés seront utilisées depuis l'intérieur de la game loop, à la fois pour savoir où dessiner le monstre mais aussi, en fonction de l'état du clavier et de la souris, on modifiera ces valeurs pour déplacer le monstre. 

Voici la nouvelle boucle d'animation, qui est typique d'un jeu vidéo :

  1. var mainLoop = function(time){  
  2.         //main function, called each frame   
  3.         measureFPS(time);  
  4.    
  5.         // Clear the canvas  
  6.         clearCanvas();  
  7.    
  8.         // draw the monster  
  9.         drawMyMonster(monster.x, monster.y);  
  10.    
  11.         // Check inputs and move the monster  
  12.         updateMonsterPosition();  
  13.    
  14.         // call the animation loop every 1/60th of second  
  15.         requestAnimationFrame(mainLoop);  
  16.     };  

Et voici le code source de la partie qui va gérer les déplacements en fonction de l'état du clavier et de la souris. Cette fonction est appelée 60 fois par seconde :

  1. function updateMonsterPosition() {  
  2.       monster.speedX = monster.speedY = 0;  
  3.         // check inputStates  
  4.         if (inputStates.left) {  
  5.             ctx.fillText("left", 150, 20);  
  6.             monster.speedX = -monster.speed;  
  7.         }  
  8.         if (inputStates.up) {  
  9.             ctx.fillText("up", 150, 40);  
  10.            monster.speedY = -monster.speed;  
  11.         }  
  12.        if (inputStates.right) {  
  13.             ctx.fillText("right", 150, 60);  
  14.             monster.speedX = monster.speed;  
  15.         }  
  16.         if (inputStates.down) {  
  17.             ctx.fillText("down", 150, 80);  
  18.             monster.speedY = monster.speed;  
  19.         }   
  20.         if (inputStates.space) {  
  21.             ctx.fillText("space bar", 140, 100);  
  22.         }  
  23.         if (inputStates.mousePos) {   
  24.             ctx.fillText("x = " + inputStates.mousePos.x + " y = " + inputStates.mousePos.y, 5, 150);  
  25.         }  
  26.        if (inputStates.mousedown) {   
  27.             ctx.fillText("mousedown b" + inputStates.mouseButton, 5, 180);  
  28.             monster.speed = 5;  
  29.         } else {  
  30.           // mouse up  
  31.           monster.speed = 1;  
  32.         }  
  33.    
  34.         monster.x += monster.speedX;  
  35.         monster.y += monster.speedY;  
  36.    
  37.     }  

Donc 60 fois par seconde, si vous regardez les dernières ligne de ce code, on va ajouter à la position (x, y) un incrément (la vitesse en x, et en y du monstre). Su aucune touche n'est enfoncée, ces incréments valent zéro, le monstre ne bouge pas. Si la flèche vers le bas est enfoncée, alors la vitesse en Y vaut monster.speed, c'est-à-dire 1 (valeur dans la création du monstre), si on appuie aussi sur la flèche à droite, la vitesse en X vaut aussi 1, le monstre se déplace en diagonale. Si on relâche une touche, la vitesse correspondante revaut zéro. Si un bouton de la souris est enfoncé, la vitesse monster.speed vaut 5 : le monstre se déplacera plus vite, si on relache le bouton, la vitesse revient à 1, etc...

JavaScript objet, animer plusieurs objets, détection de collisions

Exemples vu en cours (en plus des exemples du PDF ci-dessus) :

Autre version avec les états (game over, jeu en cours, etc) et c'est la version qu'on a éclaté en fichiers et qui est présente dans le zip ci-dessous (ouvrir index.html pour lancer) : http://jsbin.com/fopomo/15/edit

Présentation powerpoint données à des élèves de terminale, contient les exemples de collisions entre balles (voir aussi le chapitre dans le PDF consacré aux collisions)

Seconde journée : Web Sockets, applications multi-participants, multimédia (sons, etc.)

Support de cours présentant à la fin les WebSocketshttp://mainline.i3s.unice.fr/HTML5sl...hapter5FR.html

  • A FAIRE: TP sur les WebSockets avec NodeJS, comment écrire un chat "mono salle" et "multi salles" avec la librairie Socket.io et le serveur NodeJS
  • A ETUDIER ENSUITETP sur Websockets version "mono salle", mais montrant comment partager des positions de joueurs + dessin dans un canvas. Cette version n'utilise pas jQuery dans sa page HTML

Seconde partie : adapter le code du TP pour l'inclure dans ce que vous avez fait lors de la première journée (le moteur de jeu) et partager la position du joueur, des ennemis, etc.

  • Vous commencerez par mettre la partie HTML du chat dans la page HTML de votre jeu,
  • Pensez à inclure dans un fichier chat.js la partie JavaScript du chat,
  • Pensez à isoler dans des fichiers séparés les parties de votre jeu, comme nous avons fait à la fin de la première journée, on utilise les WebSockets, donc il faut un serveur, donc on ne travaille plus avec jsbin.com mais avec une vraie structure de projet.
  • Faites en sorte que le serveur serve la page HTML de votre jeu,
  • Vérifiez que le jeu marche et que le chat marche,
  • Maintenant, en vous inspirant du TP sur les WebSockets, partie "à étudier ensuite", regardez comment on peut utiliser le canal du chat pour échanger des objets.
  • Essayez de transférer des positions de joueurs, affichez les dans le chat pour débugger. Apprenez à voir les trames des websockets dans le debugger de votre navigateur (demandez au prof si vous avez oublié ce qu'on vous a montré)
  • Essayez de faire des "états" dans votre jeu (menu, jeu, game over), un des derniers exemples vus à la fin de la journée 1 faisait cela,
  • Essayez de mettre des effets sonores dans votre jeu (à l'aide de l'API Web Audio mais vous pourrez aussi utiliser une librairie comme Howler.js ou Tone.js si vous préférez). Pour la musique vous pourrez aussi soit la streamer à l'aide d'un élément <audio> soit utiliser WebAudio qui est plus adapté aux jeux vidéo.

 

Sujet de mini projet (à rendre pour la prochaine séance, ou peut-être lors d'une séance supplémentaire, à voir)

Sujet : faire un jeu multi-participants, qui puisse gérer plusieurs parties simultanées (ex : 4 personnes jouent, mais le jeu peut gérer n parties de 2 à 4 personnes).

Il peut s'agir d'un jeu abstrait (avec juste des formes géométriques) comme celui-ci: http://jeuweb-amsel.rhcloud.com:8000/, ou d'un jeu de survie (on doit éviter des balles et tenir le plus longtemps mais par exemple on peut pousser des balles vers les autres joueurs, et toutes les 10s de nouvelles balles arrivent...), cela peut être un shoot em up avec des sprites, jouables en co-op (comme celui présenté en classe), et utilisant une feuille de sprites comme celle-ci (spritesheet de gridrunner IOS) :

Pour me faire plaisir: refaire le jeu Gridrunner (gratuit sous Android, 0.99 euros sur IOS)

Feuille de sprite du jeu Gridrunner (que m'a gentiment envoyé son auteur, je serai très très très très très heureux si un groupe refaisait un clone de Gridrunner multi, même si ce n'est que le menu principal, quelques niveaux et le game over, Jeff Minter, son auteur, est le parrain du cours HTML5 Games que je fais avec le W3C, et GridRunner sur Ipad est mon jeu préféré -de loin- sur Ipad, mais il existe aussi sur Android, sur Mac OS et sous Windows je pense...) 

gridrunner.png

Autres possibilités :

Ou bien vous pouvez vous inspirer d'un de ces jeux HTML5 (mais attention, pas de pompage direct, je veux du code original, basé sur le moteur de jeu, avec des web sockets etc) : HTML5games.zip, ou une idée personnelle.

Sons et musiques

Pour les sons des jeux "old school", vous pouvez aller chercher sur freesound.org ou bien carrément utiliser une librairie de synthèse comme jsfx.js : https://github.com/egonelbre/jsfx, spécialement faite pour générer des sons synthétisés très années 80 / chiptunes / 8 bits. J'adore :-) Ici on peut lire l'article de blog de son auteur + y'a une démo interactive dans l'article http://blog.jetienne.com/blog/2014/0...ebaudiox-jsfx/

Pour les sons normaux : http://goldfirestudios.com/blog/104/...script-Library ou https://github.com/TONEnoTONE/Tone.js/ ou encore https://github.com/rserota/wad

Sprites

Particules (explosions etc)

 

Herberger une application NodeJS

Tout est là: https://github.com/joyent/node/wiki/node-hosting, à vous de choisir le bon hébergeur. En général c'est gratuit soit pour une durée déterminée, soit si votre application n'est pas très grosse (c'est souvent le cas).

 

 

Mots clés:
FichierTailleDateAttaché par 
 HTML5games.zip
Aucune description
247.75 Ko06:18, 13 Fév 2015MichelBuffaActions
Images (1)
Voir 1 - 1 sur 1 images | Voir tout
Aucune description
gridrunne...  Actions
Commentaires (0)
Vous devez être connecté pour poster un commentaire.