I. Introduction

Frame-Engine est un moteur permettant le développement d'applications et de jeux vidéos. Il a pour avantage de fournir une solution de développement unique avec l'ensemble des plates-formes existantes compatibles HTML5.

Dans ce tutoriel, je vais vous expliquer les différents éléments permettant de réaliser un petit jeu vidéo de A à Z en quelques minutes. Vous pouvez déjà avoir un aperçu du petit jeu que nous allons réaliser à cette adresse :

http://www.frame-engine.com/hamtaro/ ou directement ici.

Vous pouvez télécharger le code source ici, ou alors essayer l'application finale en l'installant sur vos mobiles via l'App Store, Google Play, ou simplement sur votre navigateur favori.

II. Préliminaires

Avant d'attaquer la phase de conception et de développement de notre jeu, il est important d'avoir quelques notions préliminaires. Celles-ci vous permettront de mieux appréhender la philosophie du moteur.

II-A. Mode de rendu

Frame-Engine est construit autour d'un système de rendu par image, il faut donc raisonner en terme de rafraîchissement et de boucle synchrone. Toutes les x FPS, votre application va être rafraîchie, une image va être calculée par le moteur puis affichée. C'est pourquoi il est important de connaître comment le moteur va afficher vos éléments afin de pouvoir jouer sur les effets de profondeur, de vitesse de déplacement et d'interaction entre éléments. À cela viennent s'ajouter des appels asynchrones qui sont les appels clavier, souris, réseau… Il faut prendre en compte que si vous modifiez une propriété interne à un élément lors d'un appel asynchrone vous n'avez aucun moyen de savoir comment votre modification va affecter le moteur et les éléments.

II-B. JavaScript

Comme le moteur fonctionne avec JavaScript, je vous recommande d'avoir un minimum de connaissance dans ce langage. De plus, nous utilisons la bibliothèque Base.js pour l'implémentation de l'héritage. Cette bibliothèque est simple à prendre en main mais comme pour JavaScript, je vous recommande de vous renseigner sur son utilisation.

II-C. Installation du moteur

Afin d'utiliser le moteur dans vos applications vous devez inclure la bibliothèque frame-engine.js dans votre page.

Pour l'utiliser vous devez copier le fichier dans le répertoire de votre application puis inclure la balise suivante dans le <head> de votre fichier HTML.

 
Sélectionnez
<script type="text/javascript" src="frame-engine.0.1.2b.js" ></script>

Une fois la bibliothèque ajoutée dans l'entête de votre fichier HTML, il ne reste plus qu'à créer une instance du moteur avec pour paramètre l'id de votre canvas.

 
Sélectionnez
<script type="text/javascript">
window.onload = function(){
new frameEngine("canvas");
}
</script>

Vous pouvez directement utiliser le paquetage prêt à l'utilisation disponible sur la page de téléchargement du site.

III. Concept de base

Voici un diagramme présentant les classes actuellement disponibles dans la bibliothèque. Certaines classes présentent un lien d'héritage. Les méthodes et variables membres ne sont pas présentes sur ce diagramme pour essayer de le garder aussi clair que possible. Il est important de prendre en compte que la plupart des méthodes sont statiques afin d'alléger les objets instanciés par le moteur JavaScript.

Image non disponible

On peut constater que le moteur (Engine) ne peut contenir qu'une et une seule Area à un instant t. Tandis qu'une Area peut contenir plusieurs Drawable. Tous les Drawables héritent de BaseElement qui apporte la logique d'affichage (calcul des positions, des limites d'affichage, des événements…).

Le paquetage Entity qui dérive directement du paquetage DrawableObjects apporte une notion supplémentaire d'entité, tel qu'un personnage ou un objet réagissant dans le jeu.

Le paquetage Containers vous apporte des classes facilitant la gestion et le stockage de vos données.

Le paquetage Useful fournit un ensemble de classes utiles dans le domaine du jeu vidéo. On y trouve des timers afin de réaliser des actions à un moment précis, un système de particule ou encore des méthodes pour jouer une musique de fond ou des effets sonores. Une classe de filtre d'image est aussi disponible, elle vous permet d'appliquer des effets et correction (gamma, saturation…) sur vos images ou l'écran de manière simplifiée. La classe Input quant à elle vous donne un accès au clavier et à la souris.

Un paquetage Network est fourni à l'état expérimental afin de faciliter les communications entre le client et le serveur mais aussi entre plusieurs clients via la classe Room.

Le paquetage UI est aussi fourni à titre expérimental afin de faciliter l'interaction avec le joueur dans la saisie de données et la validation d'action.

III-A. Engine

Engine est la classe de base du moteur. C'est elle qui contient tout le processus d'affichage et de gestion des événements. Le plus important pour vous est de savoir initialiser le moteur et de charger une zone (méthode init du moteur et setCurrentArea. Néanmoins le moteur offre d'autres fonctionnalités, comme l'affichage d'information pendant quelques secondes (méthode displayInfo), ou encore l'affichage de flash lumineux (méthode flash).

III-B. Area

Cette classe définit des zones à dessiner. Elle contient des éléments qui sont tous issus du même parent BaseElement.

BaseElement fournit un grand nombre de fonctions pour modifier vos éléments. La classe Area organise les éléments en pseudo couches, pour donner une impression de profondeur, cela ressemble fortement au z-index du CSS.

III-C. Drawable

Les drawables sont les éléments qui peuvent être affichés à l'écran, par exemple des images, des formes géométriques…

Il y a 7 drawables principaux :

III-D. Entity

Les entités sont les formes évoluées des drawables. Elles offrent des fonctionnalités supplémentaires telles que le déplacement par point de contrôle ou encore le déplacement simple de façon lissée. Nous offrons aussi une entité respectant des règles de physique simulées basiques. Vous pouvez en trouver un exemple sur le site http://www.frame-engine.com/fr/showcase.

Nous utilisons généralement ces entités pour des ennemis, le joueur ou encore des éléments interactifs.

III-E. Fonctionnement du Frame-Engine

Frame-Engine utilise une boucle de rendu comme on en trouve dans les jeux vidéos. Le moteur se synchronise avec le rafraîchissement de votre navigateur si cela est possible, ensuite à chaque rafraîchissement (de 30 à 180 fois par secondes) une image est calculée et affichée. Il est important de connaître l'ordre de rendu des éléments dans le moteur afin de comprendre comment va être construite l'image à afficher. Certaines fonctions sont automatiquement appelées par le moteur. Celles-ci vous permettent de réaliser des modifications s'appliquant aux éléments.

Voici un schéma expliquant la boucle de rendu du moteur :

Image non disponible

Les fonctions beforeRender et afterRender vous permettent de réaliser des tests sur l'ensemble de votre Area actuellement affichée par le moteur, tandis que les fonctions beforeProcess et afterProcess permettent de réaliser des modifications sur les éléments et entre éléments. Par exemple, il est recommandé de tester si votre ennemi touche un personnage dans une de ces deux fonctions.

Maintenant que vous avez les bases, nous allons passer à la création d'un jeu basique.

IV. Environnement de base

L'environnement de base d'un projet Frame-Engine est en règle générale constitué d'une page HTML avec un canvas, puis de fichiers JavaScript et médias.

Nous nommerons ici notre fichier de base index.html ce qui permet d'être compatible avec les conteneurs mobiles fournis sur le site.

Notre fichier index.html contient un canvas et les liens vers les fichiers JavaScript :

 
Sélectionnez
<!DOCTYPE html> 
<html> 
<head> 
<link href="../engine/frame.0.1.css" type="text/css" rel="stylesheet" media="screen" /> 
<script type="text/javascript" src="frame-engine.0.1.js ></script> 
<script type="text/javascript" src="entryPoint.js"></script> 
</head> 
<body> 
<canvas id="canvas"></canvas> 
</body> 
</html>

À cela nous ajoutons l'instanciation du moteur dans l'entête :

 
Sélectionnez
<script type="text/javascript"> 
window.onload = function(){ 
 new frameEngine("canvas"); 
} 
</script>

Et enfin, un point important est le redimensionnement matériel du jeu vidéo :

 
Sélectionnez
<style type="text/css"> 
 html, body, canvas{ 
  width:100%; 
  height:100%; 
  margin:0; 
  padding:0; 
 } 
</style>

Le fichier principal de notre application s'appelle entryPoint.js. Il contient les éléments d'initialisation et ici, comme l'application est simple, on l'utilisera pour l'ensemble du jeu.

L'initialisation la plus simple est la suivante :

 
Sélectionnez
function setup(){ 
 Engine.init(); 
 Engine.setAlwaysRefresh(true); 
 Engine.setCurrentArea(area); 
}

La fonction setup est une fonction réservée à l'initialisation du moteur.

À ce niveau, nous avons tout le nécessaire pour commencer le développement de notre jeu.

V. Le jeu

Le jeu développé ici est très simple mais permet de présenter les concepts principaux du moteur.

Le personnage principal sera représenté par Hamtaro qui aura pour objectif d'attraper toutes les graines tombant du ciel. Hamtaro sera animé par la technique des sprites (une image contenant plusieurs images formant une ou plusieurs animations).

V-A. Surface de jeu

Image non disponible

Tout d'abord nous allons limiter la surface de jeu du moteur à 1000 pixels de large par 722 pixels de haut, ce qui correspond à l'image de fond que nous allons utiliser.

Pour donner une dimension au moteur, on modifie l'appel à la méthode d'initialisation (init) :

 
Sélectionnez
Engine.init(false,1000,722);

Le premier paramètre est l'activation du mode debug, le second la largeur de la surface de rendu et le dernier la hauteur de la surface de rendu.

Tous les paramètres de la méthode init sont optionnels, les valeurs par défaut sont visibles dans la documentation.

Hors de toute fonction, on définit une Area qui sera la zone affichée par le moteur :

 
Sélectionnez
var area = new Area();

Puis dans le setup, on associe cette area comme élément de départ du jeu :

 
Sélectionnez
Engine.setCurrentArea(area);

Chaque area possède 3 méthodes que vous pouvez redéfinir : init, beforeRender, afterRender.

Nous allons utiliser la méthode init pour ajouter des éléments à dessiner. Nous ajoutons un Drawable au deuxième niveau de profondeur pour afficher une image de fond. Le constructeur prend en paramètre la position en x, en y et le chemin vers l'image.

 
Sélectionnez
area.init = function(){ 
 this.addDrawable(2, new Drawable(0,0,"images/background.jpg")); 
}

V-B. Le joueur

Image non disponible

Nous allons maintenant créer le personnage (Hamtaro dans l'exemple). Pour ce faire, nous étendons la classe Entity :

 
Sélectionnez
var Player = Entity.extend({ 
});

La classe Entity étend de la classe AnimatedDrawable, ce qui permet d'afficher des animations comme on en trouve dans les gifs animés.

Notre personnage peut se déplacer de façon latérale et sauter, je rajoute donc deux variables contenant les valeurs de déplacement et surcharge la fonction move.

Nous utilisons trois séquences d'animation, la première est l'état à l'arrêt, la seconde le mouvement vers la droite, et la dernière le mouvement vers la gauche.

Pour réaliser le saut du personnage, nous utilisons un système de timer, ainsi, un seul appui sur la touche espace réalise un saut complet. Pour éviter que le personnage ne saute plusieurs fois en même temps on utilise la variable state présent dans tout élément et on définit un état statique :

 
Sélectionnez
Player.JUMP = 1; 

//Création de la classe Player qui hérite de Entity 
var Player = Entity.extend({ 
 
 offsetX : 0, //Variable member statique 
 offsetY : 0, //Variable member statique 
//Surcharge de la fonction move de la clase Entity 
 move : function(){ 
  if (Input.keysDown[KEY_LEFT]){        //Si on appuie sur la touche flèche gauche 
   if (this.offsetX != -10)             
    AnimatedDrawable.playSequence(player, 2);    //On lance la 3eme animation  partir de 0) présente dans le sprite 
   this.offsetX = -10;                //On change la valeur de l'offset 
  } 
  if (Input.keysDown[KEY_RIGHT]){            //Si on appuie sur la touché flèche droite 
   if (this.offsetX != 10) 
    AnimatedDrawable.playSequence(player, 1);    //On lance la deuxième animation présente dans le sprite 
   this.offsetX = 10; 
  } 
//Si on appuie sur aucune touche ou que l'on atteint les bords de l'écran 
  if ((!Input.keysDown[KEY_RIGHT]  && !Input.keysDown[KEY_LEFT]) || ( this.rightBound > Engine.width && this.offsetX == 10) || ( this.x < 0 && this.offsetX == -10)){ 
   this.offsetX = 0;                    //On remet l'offset à 0 
   AnimatedDrawable.playSequence(player, 0);    //On lance la première séquence animée 
  } 
//Si on appuie sur la touche espace et que l'on est pas déjà en train de sauter 
  if (Input.keysDown[KEY_SPACE] && this.state != Player.JUMP){ 
   this.offsetY = -20;        // On change l'offset en y pour faire monter le personnage 
   this.state = Player.JUMP;    // On change l'état de l'instance 
   BaseElement.timeOut(this, 250, function(){     // On programme un appel ultérieur provoquant la descente du personnage 
    this.offsetY = 20; 
//On programme un appel ultérieur pour remettre les variables dans leur état initial 
    BaseElement.timeOut(this, 250, function(){     
     if (this.y != 550) this.setPosY(550);    // On utilise toujours setPosX/Y pour modifier les coordonnées 
     this.offsetY = 0; 
     this.state = 0; 
    }); 
   }); 
  } 
  this.base(this.offsetX,this.offsetY,20);    // On appelle la function move de la classe parente 
 } 
});

Pour connaître l'état des touches, on utilise le membre statique keysDown de la classe Input.

La méthode move est appelée via la méthode beforeProcess que nous surchargeons :

 
Sélectionnez
beforeProcess : function(){ 
   this.move(); 
 }

La mauvaise idée pour le déplacement aurait été d'utiliser les événements keyDown et keyUp. En effet, ces événements sont déclenchés lors de l'appui sur une touche. Or lorsque vous appuyez longtemps sur une touche du clavier l'événement est déclenché plusieurs fois, mais ce nombre de fois dépend de votre matériel. Par conséquent, le mouvement n'aurait pas été fluide et différent selon les machines.

Il ne reste plus qu'à instancier une version du joueur dans l'initialisation de l'area :

 
Sélectionnez
player = new Player(300,550,"images/player.png", 167,156,100);

On définit les séquences animées :

 
Sélectionnez
AnimatedDrawable.setSequences(player, [[0,0],[0,7],[8,15]]);

On définit la séquence à jouer à l'état initial :

 
Sélectionnez
AnimatedDrawable.playSequence(player, 0); 
//On ajoute l'instance à l'area 
this.addDrawable(1, player);

Nous ajoutons aussi les séquences de mouvements et définissons la séquence initiale.

V-C. Les graines

Image non disponible

Notre personnage va manger des graines qui tomberont et lui fera ainsi gagner des points. Nous créons donc une classe étendant Drawable dans laquelle la méthode beforeProcess fera les actions suivantes :

  • si le joueur attrape la graine le score augmente et la graine disparaît ;
  • si la graine touche le sol, le jeu est fini et on affiche une image de fin ;
  • la graine descend de haut en bas.

En bonus on joue un son lorsque le joueur attrape la graine, la méthode pour jouer un son ne prend pas d'extension dans le chemin du fichier. Le moteur va sélectionner le bon format de fichier selon le navigateur utilisé, c'est pourquoi il est nécessaire d'avoir les fichiers sons au format mp3 et ogg.

Dans l'initialisation de l'area nous ajoutons :

 
Sélectionnez
scoreText = new TextElement(20,20, "0", color(0,0,0));    //Création d'un élément texte pour afficher le score 
this.addDrawable(1, scoreText);    //Ajout de cet élément dans l'area 

//Création de la classe Seed héritant de la classe Drawable 
var Seed = Drawable.extend({ 
 //Surcharge de la method beforeProcess pour effectuer une action avant l'affichage par le moteur 
 beforeProcess : function(){ 
  if (this.hit(player)){    //On test la collision entre la graine et le joueur 
   this.remove();        //Si il y a bien une collision on supprime cette instance de graine 
   Sound.play("sounds/eat");    //On joue un son 
   score += 10;        //On incrémente le score 
   scoreText.setText(score.toString());     //On met a jour l'élément textuel 
  } 
  if (!end)    //Si le jeu n'est pas terminé 
   BaseElement.advanceBy(this,0,10,50);    //On fais descendre la graine de 10 pixels sur 50ms 
  if (this.bottomBound > 722){    //Si la graine touche le sol le jeu est fini 
   end = true; 
   area.addDrawable(0, new Drawable((Engine.width * 0.5) - (236 * 0.5), (Engine.height * 0.5) - (59 * 0.5), "images/loose.png"));    //On ajoute dynamiquement l'image indiquant la fin du jeu 
   this.remove();     
   AnimatedDrawable.playSequence(player,0); 
  } 
 } 

});

V-D. Finalisation

Il ne reste plus qu'à faire tomber les graines sur la surface de jeu toutes les 500ms. On crée un timer :

 
Sélectionnez
var timer = new Timer(500);

Puis dans la méthode beforeRender de l'Area on instancie des graines :

 
Sélectionnez
area.beforeRender = function(){ 
 if (!end && timer.itsTime()){    //Si le jeu n'est pas fini et que 500ms se sont écoulées 
  this.addDrawable(2, new Seed(random(0,900),5,"images/sunflower.png"));    //On ajoute une nouvelle graine 
 } 
}

Nous utilisons la variable end pour définir si le jeu est fini ou non.

V-E. Sauvegarde du meilleur score

Afin de sauvegarder localement le meilleur score on utilise le localStorage d'HTML5.

Pour accéder au localStorage, on utilise la variable storage qui a pour avantage d'être liée au système de stockage fourni dans le paquetage Android Frame-Engine. Ainsi, le système de sauvegarde fonctionne sans changement sous Android, ce qui ne serait pas le cas en utilisant directement le localStorage.

La sauvegarde est assez simple, à la fin de la partie, on ajoute une clé contenant la valeur du meilleur score.

Dans la classe Seed, on modifie dans la fonction beforeProcess, la partie terminant le jeu :

 
Sélectionnez
if (this.bottomBound > 722){ 
   end = true; 
   if (storage.getItem('best_score') < score)    //Si le score présent dans le localstorage est inférieur au score de la partie 
    storage.setItem('best_score',score);    //On met à jour le score dans le localstorage 
   area.addDrawable(0, new Drawable((Engine.width * 0.5) - (236 * 0.5), (Engine.height * 0.5) - (59 * 0.5), "images/loose.png")); 
   this.remove(); 
   AnimatedDrawable.playSequence(player,0); 
  }

Puis pour afficher le meilleur score, on ajoute à l'initialisation de notre Area l'instanciation d'un TextElement :

 
Sélectionnez
if (storage.getItem('best_score')){     //Si le score est présent dans le local storage alors on l'affiche 
  this.addDrawable(1,new TextElement(20,35, "best : " + storage.getItem('best_score'), color(0,0,0))); 
 }

VI. Conclusion

Comme vous pouvez le voir il n'y a rien de très compliqué dans ce tutoriel. L'idée est de montrer la logique de développement avec Frame-Engine et ainsi vous simplifier la vie.

N'hésitez pas à nous contacter pour obtenir plus d'information ou encore nous proposer vos idées.

Vous trouverez le mini jeu hamtaro à cette adresse :

http://www.frame-engine.com/hamtaro/.

L'archive du code source ici :

http://www.frame-engine.com/hamtaro/hamtaro.zip.

Vous pouvez retrouver la documentation complète de la bibliothèque ici :

http://www.frame-engine.com/docs.

VII. Remerciements

Merci à LittleWhite pour sa relecture technique et pour le temps qu'il a consacré à la mise en page du présent tutoriel. Merci aussi à FirePrawn et ClaudeLELOUP pour leurs relectures orthographiques.