I. Avant-propos▲
Ce tutoriel est extrait de mon blog Game in Progress, destiné à partager mon expérience dans la réalisation d'un jeu en HTML 5 - JavaScript.
II. Menu principal▲
Dans ce premier tutoriel nous allons nous intéresser à la bibliothèque Box2dWeb.
Cette bibliothèque 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 :
Bref, une petite mise en bouche qui devrait, si tout va bien, nous mener à ce résultat :
II-A. Résultat final▲
II-B. 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.
III. Paramétrage▲
III-A. Bibliothèques▲
Avant de commencer à développer, voici la liste des bibliothèques qui vont être utilisées dans ce projet et les liens pour les récupérer :
III-B. Structure du projet▲
Une fois les bibliothèques 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 :
IV. Play !▲
IV-A. 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.
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.
IV-B. Initialiser les fichiers JavaScript▲
Tout d'abord, nous allons initialiser les fichiers JavaScript. Ouvrons donc les deux 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.
IV-C. 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 tutoriel, 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 :
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
debug
var
debugDraw =
new
b2DebugDraw
();
//
Définir
les
propriétés
d'affichage
du
debug
debugDraw.
SetSprite
(context);
//
contexte
debugDraw.
SetFillAlpha
(0
.
3
);
//
transparence
debugDraw.
SetLineThickness
(1
.
0
);
//
épaisseur
du
trait
//
Affecter
la
méthode
d'affichage
du
debug
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 :
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 ;
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 ;
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 tutoriel. Il est temps maintenant d'exploiter tout ça !
IV-D. Le fichier gip.js▲
Le fichier gip.js est le cœur 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. À 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 !
- 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.
La valeur 10 semble communément utilisée pour ces deux paramètres. Évidemment, 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 :
IV-E. 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 tutoriel, vous devriez obtenir quelque chose comme ça. Hum, cela n'est qu'en partie satisfaisant. Nous avons bien un monde 2D avec des plates-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 dix 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à !
V. Game over▲
Ce tutoriel 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.
Je vous rappelle que ce tutoriel est extrait de mon blog, sur lequel je publierai régulièrement de nouveaux articles et qui, je l'espère, seront également sujets à une publication sur Developpez.com.
VI. Remerciements▲
Je souhaite remercier LittleWhite qui m'a donné l'opportunité de publier ce tutoriel sur le site Developpez.com et qui m'a encadré dans ce périlleux exercice.
Je remercie également ClaudeLELOUP pour la relecture et les corrections apportées à cet article.