Main menu
Dans ce premier tuto nous allons nous intéresser à la librairie Box2dWeb.
Cette librairie sera utilisée pour réaliser le moteur physique du jeu. Ici, nous allons nous pencher sur les fonctionnalités de base de la bibliothèque :
- Créer un « monde » ou environnement physique 2D dans un canvas HTML 5,
- Y ajouter des objets statiques et dynamiques,
- Configurer le mode debug de box2d.
Bref, une petite mise en bouche qui devrait, si tout va bien, nous mener à ce résultat :
Résultat final
Vous pouvez voir le résultat final ici.
Et pour récupérer les sources correspondantes, c’est ici.
Qu’est-ce que Box2d ?
Box2D est une bibliothèque logicielle libre de moteur physique 2D utilisée pour la réalisation de jeux ou d’applications. Initialement écrite en C++, elle a été portée vers de nombreux autres langages, tels que Java (JBox2d), Actionscript (Box2dFlash) et Javascript (Box2dJS).
La version qui nous intéresse est Box2dWeb, qui est en fait un portage de Box2dFlash vers Javascript.
Settings
Librairies
Avant de commencer à développer, voici la liste des librairies qui vont être utilisées dans ce projet et les liens pour les récupérer :
- Evidemment, la librairie Box2dWeb qui est au centre du projet,
- Mais aussi, jQuery, dont nous nous servirons très peu dans ce tuto, mais qu’il est toujours bon d’avoir sous la main lorsque l’on développe un projet en Javascript.
Structure du projet
Une fois les librairies récupérées, sortons notre plus fidèle IDE et initialisons un nouveau projet. Nous allons créer un nouveau projet contenant les répertoires et fichiers suivants :
- libs: répertoire contenant nos librairies javascript. On y place les 2 librairies récupérées juste avant,
- css: répertoire contenant nos feuilles de style. Ici un seul fichier css, subtilement nommé
style.css
, - js: répertoire contenant nos sources javascript. On y crée 2 fichiers :
box2dutils.js
etgip.js
, - index.html: à la racine, évidemment !
Play !
Les fichiers index.html et style.css
Dans le fichier index.html
, on place simplement un élément canvas (de 800*600 px) qui va nous permettre de manipuler les objets graphiques.
<div id="divCanvas"> <canvas width="800" height="600" id="gipCanvas"></canvas> </div>
Le canvas est placé dans une div ce qui va nous permettre de le centrer dans la page et d’appliquer une couleur de fond plus facilement, grâce aux styles du fichier style.css
:
#divCanvas { background-color: #808080; width: 800px; height: 600px; margin: auto; }
Pour finir, n’oublions pas d’importer l’ensemble des fichiers javascript et css dans le fichier index.html
:
<meta charset="utf-8" /> <title>Game in Progress - Box2d Web Tuto 1</title> <link href="css/style.css" rel="stylesheet" /> <!-- Import JS --> <script type="text/javascript" src="libs/Box2dWeb-2.1.a.3.min.js"></script> <script type="text/javascript" src="libs/jquery-1.9.0.min.js"></script> <script type="text/javascript" src="js/box2dutils.js"></script> <script type="text/javascript" src="js/gip.js"></script>
On notera au passage qu’avec HTML 5 l’attribut « type » des balises « link » et « script » est devenu obsolète. Plus besoin de spécifier qu’il s’agit d’un fichier de type css ou javascript. Dans le premier cas, la relation »stylesheet » indique implicitement la nature du fichier attendu, et dans le second cas, la balise elle même (« script ») est suffisamment explicite.
Et voilà, c’est fini pour la partie HTML ! Place maintenant au gros morceau : le code javascript.
Initialiser les fichiers javascript
Tout d’abord, nous allons initialiser les fichiers javascript. Ouvrons donc les 2 fichiers créés précédemment pour y insérer les quelques lignes suivantes :
(function(){ /** Contenu du script **/ }());
Vous avez sans doute déjà vu ce genre de chose. Il s’agit en fait d’englober tout le code dans une fonction anonyme. Le but est simple : nous créons une portée (ou scope) aux différentes déclarations de variables et de fonctions de notre fichier. Nous nous assurons ainsi qu’il n’y aura pas de perturbations avec les sources des autres fichiers chargés dans la page.
Si le sujet vous intéresse et que vous souhaitez en savoir plus, je vous laisse vous référer à cet article très bien écrit.
Le fichier box2dutils.js
Entrons maintenant dans le vif du sujet. Le fichier box2dutils.js
va nous servir de classe utilitaire. Il contiendra un ensemble de fonctions permettant de créer facilement des objets pour notre environnement physique. Il n’est pas possible en javascript d’effectuer d’import de packages ou de classes. Il faut donc passer par des déclarations de variables si l’on veut éviter de toujours appeler les adresses complètes des éléments box2d. Commençons donc par « inclure » les classes suivantes dans notre fichier :
// "Import" des classes box2dweb var b2World = Box2D.Dynamics.b2World; var b2Vec2 = Box2D.Common.Math.b2Vec2; var b2AABB = Box2D.Collision.b2AABB; var b2BodyDef = Box2D.Dynamics.b2BodyDef; var b2Body = Box2D.Dynamics.b2Body; var b2FixtureDef = Box2D.Dynamics.b2FixtureDef; var b2Fixture = Box2D.Dynamics.b2Fixture; var b2MassData = Box2D.Collision.Shapes.b2MassData; var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape; var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape; var b2DebugDraw = Box2D.Dynamics.b2DebugDraw; var b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef;
Il s’agit des classes les plus couramment utilisées pour les projets box2d. Nous n’utiliserons pas la totalité de ces dernières dans ce premier tuto, mais cela pourrait bien nous servir plus tard !
Créons maintenant notre classe utilitaire Box2dUtils. Commençons, par lui définir un constructeur et un ensemble de méthodes :
/** * Constructeur */ Box2dUtils = function() { } /** * Classe Box2dUtils */ Box2dUtils.prototype = { createWorld : function(context) { // Créer le "monde" 2dbox }, createBody : function(type, world, x, y, dimensions, fixed, userData) { // Créer un objet }, createBox : function(world, x, y, width, height, fixed, userData) { // Créer un objet "box" }, createBall : function(world, x, y, radius, fixed, userData) { // Créer un objet "ball" } }
Nous avons ici créé le patron de notre classe avec un constructeur, qui pour le moment reste vide, et les méthodes suivantes :
- createWorld : permettant d’instancier un monde 2dbox avec des propriétés physiques et dans lequel nous allons pouvoir manipuler nos objets,
- createBox et createBall : destinées à créer des objets de type « box » et « ball »,
- createBody : appelée par les deux précédentes pour la construction des objets et de leurs propriétés physiques.
Complétons maintenant le corps de ces méthodes. Penchons-nous dans un premier temps sur la méthode createWorld :
createWorld : function(context) { var world = new b2World( new b2Vec2(0, 10), // gravité true // doSleep ); // Définir la méthode d'affichage du débug var debugDraw = new b2DebugDraw(); // Définir les propriétés d'affichage du débug debugDraw.SetSprite(context); // contexte debugDraw.SetFillAlpha(0.3); // transparence debugDraw.SetLineThickness(1.0); // épaisseur du trait // Affecter la méthode de d'affichage du débug au monde 2dbox debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit); world.SetDebugDraw(debugDraw); return world; }
Qu’avons-nous fait ici ?
Nous avons créé une instance de box2DWorld en lui assignant deux propriétés :
- un vecteur définissant la gravité : les valeurs 0, 10 sont couramment utilisées car elles permettent de simuler la gravité terrestre qui est de 9.8 m/s^2,
- doSleep : qui permet d’indiquer si l’on autorise les objets du monde 2D à passer au repos lorsqu’ils n’ont plus d’interaction physique avec l’extérieur. Cela permet de nettement améliorer les performances.
Ensuite, nous avons défini les propriétés d’affichage du mode debug. Box2d n’est pas une bibliothèque graphique dans le sens où elle n’a pas pour objectif d’afficher des éléments graphiques à l’écran mais de simuler les propriétés physiques de ces éléments. Cependant, elle propose un mode debug qui offre un rendu visuel de notre simulation. Nous configurons donc le mode debug (b2DebugDraw) et l’assignons à l’objet world.
Passons maintenant à la création des objets. Nous avons défini deux méthodes de création d’objets : createBox pour créer des cubes, et createBall pour créer des sphères. Les corps de ces méthodes sont très proches :
createBox : function(world, x, y, width, height, fixed, userData) { // Définir les dimensions de la box var dimensions = { width: width, height: height }; // Appel à createBody() return this.createBody('box', world, x, y, dimensions, fixed, userData); }, createBall : function(world, x, y, radius, fixed, userData) { // Définir les dimensions de la ball var dimensions = { radius: radius }; // Appel à createBody() return this.createBody('ball', world, x, y, dimensions, fixed, userData); }
En fait, il existe très peu de différences entre ces deux objets, si ce n’est leur forme. On va donc faire appel à une méthode commune pour les créer, en spécifiant le type d’objet à créer et les dimensions souhaitées. Dans le cas d’une box : la largeur et la hauteur, dans le cas d’une ball : le rayon.
Voyons maintenant ce que contient la méthode createBody :
createBody : function(type, world, x, y, dimensions, fixed, userData) { // Par défaut, l'objet est statique if (typeof(fixed) == 'undefined') { fixed = true; } // Créer l'élément Fixture var fixDef = new b2FixtureDef(); fixDef.userData = userData; // attribuer les propriétés spécifiques de l'objet // Dessiner l'objet en fonction de son type : sa forme et ses dimensions switch (type) { case 'box': fixDef.shape = new b2PolygonShape(); fixDef.shape.SetAsBox(dimensions.width, dimensions.height); break; case 'ball': fixDef.shape = new b2CircleShape(dimensions.radius); break; } // Créer l'élément Body var bodyDef = new b2BodyDef(); // Affecter la position à l'élément Body bodyDef.position.x = x ; bodyDef.position.y = y; if (fixed) { // élément statique bodyDef.type = b2Body.b2_staticBody; } else { // élément dynamique bodyDef.type = b2Body.b2_dynamicBody; fixDef.density = 1.0; fixDef.restitution = 0.5; } // Assigner l'élément fixture à l'élément body et l'ajouter au monde 2dbox return world.CreateBody(bodyDef).CreateFixture(fixDef); }
Là, ça se complique un peu. Nous avons instancié deux classes : b2BodyDef et b2FixtureDef afin de créer notre objet.
Le Body est l’objet qui sera ajouté au monde 2dbox. On lui attribue les propriétés suivantes :
- position : en lui spécifiant des coordonnées x et y,
- type : état statique ou dynamique (en fonction du paramètre « fixed » passé).
Le body en l’état ne possède pas plus de caractéristiques. Il ne sait pas comment s’afficher et encore moins comment réagir aux lois physiques de notre monde 2dBox. C’est le rôle des Fixtures de définir les propriétés physiques d’un objet. On attribue à la fixture les propriétés suivantes :
- shape : la forme de l’objet, sphère ou cube en fonction du « type », et en spécifiant les dimensions,
- density : la densité de l’objet. Elle permettra au moteur physique de calculer la masse de l’objet en fonction de sa taille, et ainsi de déterminer son comportement dans le monde 2D,
- restitution : le coefficient de restitution de l’objet. Sans entrer dans les détails, cela permet de gérer les rebonds et les collisions. Je vous laisse vous référer à Wikipédia si vous aimez les formules mathématiques. Sachez tout de même que cette valeur doit être comprise entre 0 et 1,
- userData : permet de stocker un ensemble d’autres propriétés propres à notre application ou jeu. On peut y stocker n’importe quoi ! Très utile lorsque l’on souhaite affecter un comportement particulier à un objet ou modifier son état en réaction à ce qui se passe à l’écran.
Pour finir, nous assignons donc la Fixture au Body et ajoutons le Body à l’univers 2D.
Et voilà, nous avons réalisé notre classe utilitaire pour manipuler tous les objets box2d qui vont nous servir dans ce tuto. Il est temps maintenant d’exploiter tout ça !
Le fichier gip.js
Le fichier gip.js
est le coeur de l’application (on ne peut pas encore parler de jeu). C’est ici que nous allons créer l’environnement 2D, y ajouter des objets et lancer la simulation. Le code à insérer dans ce fichier est le suivant :
var box2dUtils; // classe utilitaire var world; // "monde" 2dbox var canvas; // notre canvas var canvasWidth; // largeur du canvas var canvasHeight; // hauteur du canvas var context; // contexte 2d // Initialisation $(document).ready(function() { init(); }); // Lancer à l'initialisation de la page this.init = function() { box2dUtils = new Box2dUtils(); // instancier la classe utilitaire // Récupérer la canvas, ses propriétés et le contexte 2d canvas = $('#gipCanvas').get(0); canvasWidth = parseInt(canvas.width); canvasHeight = parseInt(canvas.height); context = canvas.getContext('2d'); world = box2dUtils.createWorld(context); // box2DWorld // Créer le "sol" de notre environnement physique ground= box2dUtils.createBox(world, canvasWidth / 2, canvasHeight - 10, canvasWidth / 2, 10, true, 'ground'); // Créer 2 box statiques staticBox = box2dUtils.createBox(world, 600, 450, 50, 50, true, 'staticBox'); staticBox2 = box2dUtils.createBox(world, 200, 250, 80, 30, true, 'staticBox2'); // Créer 2 ball statiques staticBall = box2dUtils.createBall(world, 50, 400, 50, true, 'staticBall'); staticBall2 = box2dUtils.createBall(world, 500, 150, 60, true, 'staticBall2'); // Créer 30 éléments ball dynamiques de différentes tailles for (var i=0; i<30; i++) { var radius = 45; if (i < 10) { radius = 15; } else if (i < 20) { radius = 30; } // Placer aléatoirement les objets dans le canvas box2dUtils.createBall(world, Math.random() * canvasWidth, Math.random() * canvasHeight - 400, radius, false, 'ball'+i); } // Créer 30 éléments box dynamiques de différentes tailles for (var i=0; i<30; i++) { var length = 45; if (i < 10) { length = 15; } else if (i < 20) { length = 30; } // Placer aléatoirement les objets dans le canvas box2dUtils.createBox(world, Math.random() * canvasWidth, Math.random() * canvasHeight - 400, length, length, false, 'ball'+i); } // Exécuter le rendu de l'environnement 2d window.setInterval(update, 1000 / 60); } // Mettre à jour le rendu de l'environnement 2d this.update = function() { // effectuer les simulations physiques et mettre à jour le canvas world.Step(1 / 60, 10, 10); world.DrawDebugData(); world.ClearForces(); }
Rien de vraiment compliqué ici, puisque l’on se contente de faire appel aux méthodes de notre classe utilitaire Box2dUtils pour créer l’environnement et les objets 2D (de tailles variables, avec un positionnement aléatoire).
Revenons quand même sur quelques bouts de code :
canvas = $('#gipCanvas').get(0); context = canvas.getContext('2d'); world = box2dUtils.createWorld(context); // box2DWorld
Dans ce premier bout de code, nous récupérons l’élément canvas et le contexte 2D. Ce qu’il faut retenir ici c’est qu’un élément canvas dispose d’un contexte, c’est-à-dire une zone dans laquelle il est possible de dessiner et d’afficher des éléments graphiques. A savoir qu’il existe également un contexte 3D appelé « webgl » sur lequel je ne m’attarderai pas. Ici, nous récupérons le contexte 2D afin de spécifier à l’objet b2DebugDraw dans quel espace il doit dessiner (voir le contenu de la fonction createWorld).
Ensuite, attardons-nous sur ceci :
window.setInterval(update, 1000 / 60); /** ... **/ this.update = function() { // effectuer les simulations physiques et mettre à jour le canvas world.Step(1 / 60, 10, 10); world.DrawDebugData(); world.ClearForces(); }
C’est ici que la magie opère. La fonction setInterval de l’objet window permet de lancer un traitement répété à intervalle régulier. Ici nous appelons la méthode update qui va permettre de simuler les équations physiques de notre environnement. L’intervalle est défini en millisecondes.
Les calculs compliqués sont à la charge de la bibliothèque box2D, nous n’avons pas à nous en préoccuper. Configurons simplement l’appel au moteur de rendu via la méthode Step. Les paramètres passés sont les suivants :
- timeStep : le taux de rafraîchissement, fixé ici à 60 images par seconde, ce qui correspond à l’intervalle d’appel à la fonction update. La vie est bien faite !
- velocityIterations : compteur d’itérations pour le calcul de la vitesse,
- positionInterations : compteur d’itérations pour le calcul de la position.
Les calculs physiques sont effectués en deux phases : le calcul de la vitesse et le calcul de la position. Dans la première phase, le moteur recalcule les forces et contraintes physiques pour faire correctement bouger les objets dans l’environnement. Dans la seconde phase, le moteur ajuste le positionnement des objets dans l’espace.
Les valeurs 10 et 10 semblent communément utilisées. Evidemment, plus le nombre d’itérations est élevé, plus le nombre de calculs à effectuer est important. Donc, réaliser moins d’itérations améliore les performances au détriment de la justesse de la simulation alors que, à l’inverse, augmenter le nombre d’itérations augmente la qualité de la simulation au détriment des performances.
Il ne reste plus qu’à effectuer les deux manipulations suivantes à chaque step :
- DrawDebugData : mettre à jour l’affichage du mode debug,
- ClearForces : mettre à jour les forces physiques calculées afin de ne pas les recalculer à chaque Step.
Tests et résolution des problèmes
Nous sommes maintenant prêts à tester. Ouvrons notre navigateur favori et observons le résultat. Si vous avez correctement suivi ce tuto, vous devriez obtenir quelque chose comme ça.
Hum, cela n’est qu’en partie satisfaisant. Nous avons bien un monde 2D avec des plate-formes fixes et des objets qui tombent et rebondissent dans tous les sens… mais qu’est-ce que c’est lent ! Tout se passe au ralenti !
Essayons de comprendre d’où peut venir ce problème et de trouver une solution. En fait, la cause de tous nos soucis n’est pas très difficile à trouver. Il suffit de consulter la FAQ de box2d. Comme nous l’avons vu plus haut dans cet article, box2d n’est pas une bibliothèque graphique et ne doit pas être utilisée pour afficher des choses à l’écran. Son job est de calculer les positions, rotations et interactions physiques des éléments à l’écran, et pour se faire, box2d utilise comme unité de mesure le mètre et non le pixel. La FAQ nous prévient également que les objets mobiles ne devraient pas excéder une taille de 10 mètres.
Nous allons donc revoir un peu notre code afin de répondre à ces spécifications en effectuant une mise à l’échelle de tous nos objets. Il existe une règle plus ou moins établie selon laquelle 1 m = 30 pixels. Appliquons donc cela à notre environnement.
Dans un premier temps, nous ajoutons l’échelle comme variable de classe à Box2dUtils:
Box2dUtils = function() { this.SCALE = 30; // Définir l'échelle }
Ensuite, nous appliquons cette échelle à tous nos objets. Reprenons un peu le corps de la fonction createBody. Les modifications s’appliquent à la définition de la taille et du positionnement de nos objets :
fixDef.shape.SetAsBox(dimensions.width / this.SCALE, dimensions.height / this.SCALE); fixDef.shape = new b2CircleShape(dimensions.radius / this.SCALE); bodyDef.position.x = x / this.SCALE; bodyDef.position.y = y / this.SCALE;
Enfin, et pour que le rendu du mode debug reste inchangé, appliquons cette même échelle à l’objet b2DebugDraw :
debugDraw.SetDrawScale(30.0); // échelle
Et voilà !
Game over
Success
Ce premier tuto est maintenant terminé. Si tout s’est déroulé comme prévu vous devriez obtenir ce résultat. Et je vous rappelle également que les sources sont à votre disposition.
Next level
Tout cela est bien joli, mais ce n’est pas un jeu ! Effectivement, ça manque encore un peu d’interactivité. Pour cela, il faudrait ajouter la gestion des évènements utilisateurs et des collisions et le contrôle de nos objets.
Et ça tombe bien, car c’est exactement ce qui est au menu du prochain tuto !
Pingback: Box2d Web – Tuto 2 : move your body ! | Game in Progress
Merci pour tes tutos qui m’ont aidé, j’attends la suite. Par contre tu as fait une petite erreur dans celui-ci. Tu attribue l’userData à fixDef alors qu’il faut le faire à bodyDef.
Bonne continuation.
Bonjour Vernier !
Tout d’abord merci pour ton retour.
Concernant ta remarque : et bien oui et non.
Les classes Body et Fixture disposent toutes les deux d’un attribut userData. On peut donc attribuer une valeur userData au bodyDef ou au fixDef (rien n’empêche d’ailleurs de valoriser les deux pour un même objet 2D).
D’un point de vue purement technique on ne peut donc pas parler d’une erreur.
Par ailleurs, dans le prochain tuto nous verrons qu’il est possible d’assigner plusieurs fixtures à un même body. Dans ce cas précis il peut être utile de récupérer le userData d’une des fixtures plutôt que celui du body…
Cependant ta remarque est pertinente et effectivement on aurait plutôt tendance à appliquer plus volontairement les userData au body. Simplement car c’est lui qui représente notre objet dans l’environnement 2D.
Donc, les deux options sont possibles et c’est à chacun d’adapter ses choix en fonction de la situation rencontrée et de ses besoins.
A bientôt 🙂
Merci pour tes précisions. Je débute la prog javascript donc je pensais bien que quelque chose avait du m’échapper. ^^
J’ai un problème d’ailleurs dans l’organisation avec EaselJS et Box2D j’espère que tu pourra m’aider. Je crée un menu avec EaselJS , et lorsque je clic sur le bouton « jouer » , une partie de mon jeu se lance et donc mon world se crée ainsi que tous les bodies que j’ai créés. Jusque là aucun problème donc. Mais à la fin de ma partie je veux afficher le tableau des scores et proposer au joueur soit de recommencer la partie ou alors de retourner au menu.
C’est là que ça se complique. Pour le tableau récapitulatif j’ai intégré un DOMElement à mon canvas grâce à EaselJS (j’ai choisi cette méthode parce que j’avais besoin de placer des inputs text dans ce tableau) mais lorsque je clic sur le bouton « Retour menu » ou « Rejouer » de ce DOMElement ( qui font chacun respectivement appel aux même fonctions que j’ai utilisées pour créer le menu et la nouvelle partie) absolument rien ne s’affiche. Je pense que je ne saisi pas comment « supprimer » le world du canvas(je précise que j’ai préalablement supprimé tous les body de la bodyList). Il semble s’afficher par dessus tout autre element.
Bref c’est un peu confu j’aimerai vraiment que tu m’aide là dessus. Je veux pas pourrir ta rubrique commentaires donc tu peux me répondre sur mon mail si tu y a accès.
Merci d’avance.
Pingback: Box2d Web – Tuto 4 : jumping puzzle | Game in Progress
sa marche pas aprés modif de :
fixDef.shape.SetAsBox(dimensions.width / this.SCALE, dimensions.height / this.SCALE);
fixDef.shape = new b2CircleShape(dimensions.radius / this.SCALE);
bodyDef.position.x = x / this.SCALE;
bodyDef.position.y = y / this.SCALE;
et puis
debugDraw.SetDrawScale(30.0);
le lien vers le src ne marche plus non plus… sinon très bon tuto…
Bonjour,
As-tu bien pensé à déclarer la « constante » SCALE » ?
Box2dUtils = function() {
this.SCALE = 30;
}
Sinon, le second lien vers les sources est réparé !
Merci beaucoup pour ce tuto, d’une clarté sans égale !
Pingback: Box2d Web – Tuto 5 : liaisons dangereuses – Part 1 | Game in Progress
Pingback: EaselJS – Tuto 1 : dessine-moi un menu | Game in Progress
Merci beacoup.
Je trouve ton tuto super claire.
Continue comme ça ^^
Pingback: Box2d Web & EaselJS – Tuto 1 : bouncing pigs | Game in Progress