Introduction
IMPORTANT!! Cet article est basé et traduit à partir de l’article original d’Impaler. Dans cet article, nous allons étudier une méthode de développement d’un jeu vidéo de type plateforme 2D avec libGdx. Le jeu sera développé en se basant sur un modèle de développement MVC. Le but principal de l’article est d’apprendre quelques concepts de base incluant les suivants :- Comment créer un jeu 2D simple de type plateforme
- Comment développer un jeu en suivant une architecture MVC
- Comment utiliser les graphiques 2D d’OpenGL sans connaître OpenGL
- Comment déployer un jeu pour le bureau et sur la plateforme Android
- Comment ajouter du son
Étape pour créer un jeu
- Avoir une idée du jeu
- Faire des maquettes sur papier pour avoir une idée de quoi aura l’air le jeu.
- Analyser l’idée de façon itérative, i.e. revenir sur des idées de départ pour les améliorer tout en avançant pour terminer avec ce que contiendra la première version du jeu.
- Sélectionner une technologie et débuter le prototype.
- Débuter la programmation et la création des ressources pour le jeu.
- Développer itérativement, i.e. jouer, tester, améliorer et recommencer jusqu’à obtention du résultat désiré.
- Polir et distribuer!
Le concept du jeu
Étant donné que c’est un projet qui doit se faire dans un court laps de temps, on limite le temps alloué au processus de développement pour mettre plus de temps sur la technologie et la science pour faire des jeux. Le jeu développé pour cet article est basé sur Star Guard de Vacuum Flowers. Téléchargez le jeu et faites des tests, cela vous permettra d’avoir une idée globale du projet. Le concept du jeu est de guider le héros (Bob) à travers les différents niveaux en éliminant les ennemis et en évitant tout ce qui essaie de le tuer. Les contrôles seront relativement simples soient les flèches avec les touches Z et X qui seront respectivement utilisées pour se déplacer, sauter et tirer. Le plus longtemps que la touche « sauter » sera maintenue le plus haut le personnage sautera. On pourra changer la direction dans les airs ainsi que tirer. On verra plus tard comment transférer ces contrôles sur la plateforme Android.Démarrez Eclipse
Nous allons utiliser la plateforme libGdx sous Eclipse. Pourquoi libGdx et non XNA ou autres plateformes? Tout simplement par qu’il est « simple », gratuit et multiplateformes. Il permet aussi de nous abstraire plusieurs notions complexes qui nous entraveraient dans le développement.Générer les projets
La première étape sera de télécharger libGdx. Ensuite, il suffira de créer un nouveau projet libGdx avec l’utilitaire gdx-setup-ui.jar. Ce dernier se retrouve dans le dossier racine de libGdx. Une fois l’application gdx-setup-ui.jar lancée, la fenêtre ci-contre devrait s’afficher. Figure 1 : Page de démarrage et de configuration de projet Voici ce que chaque section signifie :-
Configuration : Zone de configuration granulaire du projet
- Name : Nom du projet qui sera utilisé dans Eclipse
- Package : Nom du package de déploiement. Il doit être unique pour chaque projet. C’est ce nom qui permet de définir uniquement une application dans Google Play. Cela permet entre autre d’envoyer des mises à jour du jeu dans Google Play. D’ordre général, le nom du package est de la forme d’un URL inversé. Dans mon cas, je vais utiliser « ca.collegeshawinigan.bobgame ».
- Game Class : Nom de la classe définissant le jeu. Toujours débuter le nom d’une classe avec une lettre majuscule. C’est une bonne pratique Java.
- Destination : Dossier dans lequel les projets seront générés. Je suggère de créer un dossier avec le nom du projet, car le setup génère un minimum de deux dossiers soient « nomProjet » et « nomProjet-android ». Ainsi, il sera plus facile à retrouver dans l’arborescence des dossiers.
- Android SDK : Dossier dans lequel se trouve le android Studio Development Kit, à télécharger vers le lien suivant.
- Sub Projects : Types de projet à générer. Sélectionnez les projets que vous désirez générer. Pour plus de faciliter de développement, je suggère au moins la version Desktop. Ainsi, il ne sera pas nécessaire de toujours déployer le projet sur la plateforme Android pour faire du débogage.
- Si vous sélectionnez la version HTML, assurez-vous d’avoir le Google Web Toolkit installé sur votre système.
- Si vous sélectionnez la version iOS… bon je ne suis pas un fan iOS donc je ne peux répondre pour cette partie! 😉
-
Extensions : On sélectionne les librairies tierce-partie dans cette section.
- Le bouton « show Third Party Extensions » permet d’afficher plus de librairies. Il suffit de les sélectionner puis d’appuyer sur « Save ».
- Dans notre cas, nous aurons besoins de la librairie « Tools » et « box2d ».
- Advanced Settings : il suffit d’appuyer sur « Advanced » pour y accéder
- Maven Mirror Url : TO DO.
- IDEA : génère les fichier pour un projet sous Intellij IDEA.
- Eclipse : génère les fichiers pour un projet sous Eclipse. Nous choisirons cette option pour BobGame.
- Offline Mode : permet de pas forcer le téléchargement des dépendances pour les librairies.
- Pour sauvegarder les options, il faut appuyer sur le bouton « Save ».
- Generation : pour lancer la génération, il
il ne suffit que de cliquer sur « Generate »!
Importer dans Eclipse
Après avoir généré les projets, il faudra les importer dans Eclipse.- File à Import…
- General… à Existing Projects into Workspace
- Select root directory ß Votre projet
- Les projets générés devrait se retrouver dans la section « Projects »
- Terminer
- Les dossiers des projets devraient se retrouver dans le panneau d’explorateur de package qui se situe à gauche
Développement
Le projet du jeu se retrouve dans le package « bobgame ». Les autres projets servent de projet de configuration pour les plateformes spécifiques. Par exemple, la dimension de l’écran pour le desktop ou encore la configuration des différents senseurs pour les plateformes Android.Les ressources
Il est important de comprendre que lorsque l’on utilise libGdx, les ressources sont partagés à partir du projet Android d’où l’obligation de générer le projet Android. Les ressources se retrouvent dans le dossier « Assets » à l’intérieur du projet Android. On y retrouvera entre autres les images, les sons et les textures.Créer le jeu
Il est important de comprendre le concept d’une application de type jeu vidéo. Le principe est très simple. Il s’agit d’une méthode d’initialisation, d’une boucle infinie et d’une méthode de terminaison. On peut retrouver plus d’information sur l’architecture des jeux ici et de la boucle infinie ici. LibGdx permet de simplifier le tout en générant le code de base telle une pièce de théatre. Dans notre cas, nous allons définir les scènes (niveaux), les acteurs, leurs rôles et comportements. Cependant la chorégraphie sera réalisée par le joueur. Donc pour configurer notre jeu, on doit réaliser les étapes suivantes :- Démarrer l’application.
- Charger les images et sons en mémoire.
- Générer les niveaux avec les acteurs et les comportements.
- Laisser le contrôle au joueur.
- Créer le moteur qui va gérer l’interaction entre le joueur et le jeu basée sur les actions reçues de la manette (ou clavier ou souris).
- Déterminer le moment où le jeu prend fin.
- Terminer le spectacle.
package ca.collegeshawinigan.bobgame; import com.badlogic.gdx.ApplicationListener; public class BobGame implements ApplicationListener { @Override public void create() { } @Override public void dispose() { } //Ne mettre cette méthode que si elle est implémentée ! //@Override //public void render() { //} @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
- La méthode Create permet de charger les différentes ressources nécessaires au jeu.
- La méthode Resize est exécutée à chaque fois que l’écran est redimensionné.
- La méthode Pause est exécutée lorsque le système sort de l’application pour effectuer d’autres tâches. Par exemple, recevoir un appel téléphonique sur les téléphone Android
- La méthode Resume est exécutée lorsque l’application a été mis en pause et revient comme application ayant le focus.
- La méthode Dispose est exécutée à la fin de l’exécution. Principalement utilisé pour détruire les objets qui ont été instanciés.
- La méthode Render est le coeur du jeu. C’est la méthode qui fait office de boucle principale du jeu. Ainsi toute la logique et le rendu du jeu se fait à l’intérieur de cette méthode.
Les acteurs
Commençons avec le jeu. La première étape sera d’avoir un monde où le personnage pourra se promener. Le monde est composé de niveaux et chaque niveau est composé d’un terrain. Le terrain n’est rien de plus que des blocs desquelles le personnage ne peut traverser au travers. Pour le moment, identifier les acteurs et les entités n’est pas une tâche complexe. Nous avons le personnage (Bob) et les blocs qui forment le monde. Si vous avez essayé Star Guard, on constate que Bob peut avoir quelques états. Lorsque l’on ne touche à rien Bob est inactif (idle). Il peut aussi se déplacer (move) dans les deux directions et il peut sauter (jump). De plus, lorsqu’il est mort (dead), il ne peut plus rien faire. Ainsi, on vient d’identifier quatre états.- Idle : Lorsqu’il ne bouge ou saute pas ET est envie.
- Moving : Déplacement de gauche ou droite à une vitesse constante.
- Jumping : Déplacement en hauteur de gauche ou droite.
- Dead : Invisible et regénération
package ca.collegeshawinigan.bobgame.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Bob { public enum State { IDLE, WALKING, JUMPING, DYING } static final float SPEED = 2f; // unité par seconde static final float JUMP_VELOCITY = 1f; static final float SIZE = 0.5f; // Demi unité Vector2 position = new Vector2(); Vector2 acceleration = new Vector2(); Vector2 velocity = new Vector2(); Rectangle bounds = new Rectangle(); State state = State.IDLE; boolean facingLeft = true; public Bob(Vector2 position) { this.position = position; this.bounds.height = SIZE; this.bounds.width = SIZE; this.bounds.x = this.position.x; this.bounds.y = this.position.y; } }
- Dans un jeu, l’énumération des états sert à simplifier la compréhension du code lors du développement.
- position est la position de Bob dans le monde. Ainsi les coordonnées seront exprimées en utilisant celle du monde.
- acceleration est l’accélération en XY lorsque Bob saute.
- velocity est la vitesse de déplacement de Bob qui est continuellement calculée.
- bounds représente les limites de Bob.
- state est l’état actuellement de Bob.
- facingLeft indique que Bob fait face à gauche.
- Quelques constantes sont définies dans le haut de la classe. Celles-ci seront utilisées pour calculer la mécanique de Bob.
package ca.collegeshawinigan.bobgame.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Block { static final float SIZE = 1f; Vector2 position = new Vector2(); Rectangle bounds = new Rectangle(); public Block(Vector2 pos) { this.position = pos; this.bounds.width = SIZE; this.bounds.height = SIZE; this.bounds.x = this.position.x; this.bounds.y = this.position.y; } }Les blocs ne sont rien de plus que des rectangles qui seront placés dans le monde. On utilisera les blocs pour générer le terrain. Il n’y aura qu’une seule règle. Rien ne peut les pénétrer. À propos des Vector2 dans libGdxRemarquez que l’on utilise le type Vector2. Cela nous permet de nous simplifier la vie pour effectuer des calculs de positionnement et de déplacement.
Le système de coordonnées et les unités
À l’instar du monde réel, le monde des jeux possède des dimensions. Pensez à une maison d’une étage. Il y a une largeur, une hauteur et une profondeur. Dans notre cas, nous n’avons besoins que de deux dimensions, ainsi nous ne nous préoccupons pas de la profondeur. Si une pièce a une dimension de 5 mètres en largeur et 3 mètres en hauteur, on peut la décrire dans le système métrique. Il est facile d’imaginer une table 1 mètre de large et 1 mètre de haut au milieu. Nous ne pouvons pas passer au travers la table, pour la traverser, nous aurions besoin de sauter au-dessus d’elle, marcher 1 mètre et sauter. Nous pouvons utiliser plusieurs tables pour créer une pyramide et créer des dessins bizarres dans la chambre. Dans notre monde, le monde représente la chambre, les blocs représente la table et les unités représente les mètres dans le monde réel. Si nous courrons 10 km/h, cela se traduit à 2.7778 m/s (10 * 1000 / 3600). Si on traduit dans le monde de Bob, cela sera l’équivalent de 2.78 unités/s. Observez le diagramme suivant qui représente les boîtes limitrophes et Bob dans le système de coordonnées du monde. Les carrés rouges sont les boîtes limitrophes des blocs. Le carré vert est la boîte limitrophe de Bob. Les cases vides représentent l’air. La grille est juste pour la référence. C’est le monde dans lequel nous allons créer nos simulations. L’origine du système de coordonnées est au fond à gauche. Ainsi, marcher à gauche à 10 km/h indique que la position de Bob devra diminuer à 2,7 u/s. Notez que l’accès aux membres dans les classes est celui définit par le packaging d’Eclipse. Il faudra définir les modèles dans un package différent que l’on nommera Model. On doit créer les méthodes accesseurs (getters et setters) pour accéder aux membres à partir du moteur.Exercices
- Créer les packages suivant pour votre projet :
- com.votreDomaine.nomJeu.controller
- com.votreDomaine.nomJeu.model
- com.votreDomaine.nomJeu.screens
- com.votreDomaine.nomJeu.view
- Créer les getters et setters pour les propriétés des classes Bob et Bloc
- Dans le menu contextuel du code dans Eclipse, il y a une méthode qui simplifie grandement le temps passé à coder. Il suffit de sélectionner Source –> Générer les Getter et Setters. Ensuite, on sélectionne ce que l’on désire générer.
Créer le monde
Dans un premier temps, nous allons « hard-coder » le monde avoir un aperçu rapide. Le monde sera de 10 x 7. On placera Bob et les blocs de la méthode ci-contre. La classe World.java va ressemble à ceci.package ca.collegeshawinigan.bobgame.model; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; public class World { /** Les blocs qui composent le monde **/ Array<Block> blocks = new Array<Block>(); /** Notre héro!! **/ Bob bob; // Getters ----------- public Array<Block> getBlocks() { return blocks; } public Bob getBob() { return bob; } // -------------------- public World() { createDemoWorld(); } private void createDemoWorld() { bob = new Bob(new Vector2(7, 2)); for (int col = 0; col < 10; col++) { blocks.add(new Block(new Vector2(col, 0))); blocks.add(new Block(new Vector2(col, 6))); if (col > 2) blocks.add(new Block(new Vector2(col, 1))); } blocks.add(new Block(new Vector2(9, 2))); blocks.add(new Block(new Vector2(9, 3))); blocks.add(new Block(new Vector2(9, 4))); blocks.add(new Block(new Vector2(9, 5))); blocks.add(new Block(new Vector2(6, 3))); blocks.add(new Block(new Vector2(6, 4))); blocks.add(new Block(new Vector2(6, 5))); } }Cette classe n’est qu’un conteneur pour les différentes entités du monde. Présentement, les entités sont Bob et les blocs. Dans le constructeur les blocs sont ajoutés dans un tableau et Bob est créé. Je sais que ce n’est pas une bonne méthode de programme, mais ce n’est que pour des fins de démonstration rapide. Il faut se souvenir que l’origine est représentée par le coin inférieur gauche.
Créer le jeu et afficher le monde
Pour dessiner le monde sur l’écran, nous avons besoin un écran pour lui et dire à ce dernier de dessiner le monde. Dans libGdx, il y a une classe appelé Game et nous allons redéfinir la classe principale du jeu pour hériter de la classe Game fournit par libGdx au lieu d’être un ApplicationListener. À propos des Écrans (Screens)Un jeu est constitué de plusieurs écrans. Même dans notre jeu, nous aurons 3 écrans. L’écran de départ (Start), l’écran de jeu (Play) et l’écran de la partie terminée (Game Over). Chaque écran ne s’occupe que de son monde et ne connaît pas le contenu des autres écrans. Par exemple, l’écran de départ contiendra le menu d’options Jouer et Quitter. Il possède deux éléments (boutons) et s’occupe du clic/touché de ces éléments. Il dessine en continue ces deux éléments, jusqu’à ce qu’un des deux boutons soit cliqué. Si Jouer est cliqué, l’écran avise le jeu qu’il doit charger l’écran de jeu et décharger l’écran de départ. L’écran de jeu s’occupe à calculer et à rendre le jeu en soi jusqu’à ce que la fin du jeu arrive. À ce moment, le jeu avise la boucle principale d’afficher le partie terminée et décharge l’écran de jeu. L’écran de Game Over ne sert qu’à afficher le score final et bouton de redémarrage de jeu.Pour l’instant, redéfinissons le code du jeu principale et créons l’écran principal. Nous allons ignorer l’écran de départ et de partie terminée. La classe GameScreen.javapackage ca.collegeshawiniga.bobgame.screens; import com.badlogic.gdx.Screen; public class GameScreen implements Screen { @Override public void render(float delta) { // TODO Auto-generated method stub } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void show() { // TODO Auto-generated method stub } @Override public void hide() { // TODO Auto-generated method stub } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { // TODO Auto-generated method stub } }Astuces!Dans Eclipse, on peut générer des classes automatiquement. Il suffit de cliquer avec le bouton de droite sur le projet et ajouter une classe. Dans la fenêtre de nouvelle classe, on peut faire hériter le code d’une classe interface en ajoutant la classe interface dans la zone Interface.Dans le cas du GameScreen, on fera hériter celle-ci de l’interface Screen.La classe du jeu principal dans mon cas BobGame devient très simple.
package ca.collegeshawinigan.bobgame; import ca.collegeshawinigan.bobgame.screens.GameScreen; import com.badlogic.gdx.Game; public class BobGame extends Game { @Override public void create(){ setScreen(new GameScreen()); } }
- GameScreen implémente l’interface Screen qui agit très similairement à ApplicationListener, mais possède deux méthodes de plus.
- show() : Cette méthode est appelée lorsque le jeu principal rend cet écran actif.
- hide() : Cette méthode est appelée lorsque le jeu principal affiche un autre écran.
private World world; private WorldRenderer renderer; /** Autre code **/ @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); renderer.render(); } /** Autre code **/
- La propriété world est l’instance du monde qui contient les blocs et Bob.
- La propriété renderer est la classe qui va rendre le monde sur l’écran. Elle sera décrite ci-bas.
package ca.collegeshawinigan.bobgame.view; import ca.collegeshawinigan.bobgame.model.Block; import ca.collegeshawinigan.bobgame.model.World; import ca.collegeshawinigan.bobgame.model.Bob; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private World world; private OrthographicCamera cam; /** Pour fin de débogage **/ ShapeRenderer debugRenderer = new ShapeRenderer(); public WorldRenderer(World world) { this.world = world; this.cam = new OrthographicCamera(10, 7); this.cam.position.set(5, 3.5f, 0); this.cam.update(); } public void render() { // Rendu des blocs debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Filled); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x; float y1 = block.getPosition().y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // Rendu de Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x; float y1 = bob.getPosition().y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } }WorldRenderer n’a qu’un seul but soit de vérifier l’état actuel du monde et de le dessiner. Il n’y a qu’une seule méthode public (render())qui est appelé par la boucle principale de GameScreen. Le renderer a besoin du monde pour extraire l’information de celui-ci. C’est la raison pour laquelle il est en paramètre dans le constructeur. Lors du rendu, la première étape consiste à dessiner les blocs et ensuite Bob. Dans le cas présent, les blocs et Bob sont représentés par des primitives de OpenGL. Pour dessiner ces primitives avec OpenGL, cela demande des connaissances de la librairie. Cependant libGdx permet de nous abstraire de ces notions avec la classe ShapeRenderer. Voici les points importants à connaître :
- On déclare le monde (world) en tant que propriété.
- On déclare un objet OrthographicCamera. Nous allons utiliser cette caméra pour visualiser le monde d’un point de vue orthographique. Actuellement le monde est très petit et a taille d’une écran, mais éventuellement les niveaux seront plus grands et Bob se déplacera à travers. Nous allons devoir déplacer la caméra avec Bob. On pourrait faire l’analogie avec une caméra dans la vie réelle. Si vous désirez en savoir plus sur la projection orthographique, vous pouvez cliquez ici.
- La classe ShapeRenderer sera utilisée pour dessiner des primitives. Cette classe permet de dessiner des primitives tel des cercles, rectangles, lignes, etc.
- Dans le constructeur, nous instancions la caméra pour une vue de 10 x 7 (le monde actuel). Cela signifie qu’en remplissant l’écran de 10 blocs en largeur et 7 blocs en hauteur, on prendra tout l’espace de la vue de la caméra. Important : Il ne faut pas perdre la notion que le jeu est de résolution indépendante. Ainsi si l’écran est de 480 x 320, cela signifie que 480 pixels (px) représentent 10 unités donc chaque bloc aura 48 px de largeur et 48.7 px de hauteur. Dans une écran 1080P, les blocs auraient 192 px x 154.3 px.
- La ligne #23 indique à la caméra que l’on désire fixer le centre de la caméra sur le point (5, 3.5) de notre monde. L’image qui suit montre le concept.
- À chaque fois que l’on doit modifier un paramètre de la caméra, on doit mettre à jour celle-ci. En gros, on modifie les matrices internes de la caméra. LibGdx fait un très bon travail pour nous abstraire de toutes ces notions de calculs matriciels que l’on retrouve dans OpenGL (Croyez-moi ce ne sont pas que des petits calculs!).
- Dans la méthode render(), on mets en accord les matrices de la caméra avec le renderer (dessinateur?). Ceci est nécessaire, car nous avons modifié les paramètres de la caméra.
- On dit au renderer que l’on désire dessiner des rectangles.
- Tel qu’indiqué précédemment, on dessine la série de blocs en premier lieu. Ainsi, on itère à travers le tableau de blocs. J’espère que vous n’avez pas oublié de créer vos getters et setters! 😉
- Les blocs seront dessinés en rouge.
- On indique au renderer de dessiner le rectangle.
- Le même principe s’applique pour Bob sauf que l’on dessine un rectangle vert.
package ca.collegeshawinigan.bobgame.screens; import ca.collegeshawinigan.bobgame.model.World; import ca.collegeshawinigan.bobgame.view.WorldRenderer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL20; public class GameScreen implements Screen { private World world; private WorldRenderer renderer; @Override public void show() { world = new World(); renderer = new WorldRenderer(world); } @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); renderer.render(); } /** ... restant du code omis ... **/ }
- La méthode render() possède 3 lignes soient la première pour indiquer la couleur du fond, la seconde pour effacer l’écran et la troisième pour lancer le rendu.
package ca.collegeshawinigan.bobgame; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; public class AndroidLauncher extends AndroidApplication { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); config.useAccelerometer = false; config.useCompass = false; config.useWakelock = true; config.useGL20 = true; initialize(new BobGame(), config); } }
- La propriété de configuration useWakeLock permet d’empêcher Android de tamiser l’écran s’il détecte une inactivité de la part de l’OS.
- La propriété useGL20 permet, dans notre cas, de charger des images qui n’ont pas des dimensions de puissance de 2. De plus, nous allons devoir tester l’application sur un périphérique Android, car l’émulateur n’a pas OpenGL ES 2.0.
- Il faudra effectuer la même modification dans le fichier Main.java du projet desktop.
Le patron MVC
Pour ceux qui suivent le cours avec moi, vous avez sûrement remarqué que l’on développait le jeu de manière différente. On ne fait plus des classes qui contiennent toute l’information, mais on subdivise les entités en tâche. Ce que l’on appelle le développement MVC qui tient pour Modèle-Vue-Contrôleur (Model-View-Controller). Cette méthode a des similarités avec le développement 3-tiers quoiqu’elles ne sont pas identique dans la façon de faire. Cette méthode de travail permet de faire du développement efficace au niveau de la maintenance et de la simplicité. Ce didacticiel explique le développement de jeux en utilisant le MVC.Ajouter les images
Pour l’instant, le rendu fonctionne, mais on aimerait ajouter des images. Nous allons modifier le renderer (vue) pour qu’il dessine des images au lieu de rectangles. Dans OpenGL, afficher une image est un processus complexe. Il faut charger les images, les convertir en textures et ensuite les « mapper » sur des surfaces définies par de la géométrie. LibGdx comme dans son habitude, nous soustrait de toute cette complexe en nous permettant de charger les images et de les convertir en une seule ligne. Les projets libGdx accèdent aux ressources à l’intérieur du projet Android sous le dossier assets. Ainsi les ressources sont partagées entre les différents projets d’où l’obligation d’avoir le projet Android. Ainsi, je suggère de classer les ressources par catégorie, par exemple, les textures, les sons et les images devraient être à l’intérieur de sous-dossiers respectifs. Dans un premier temps, créez un dossier images à l’intérieur du dossier assets. On y stockera pour l’instant les deux images qui seront utilisées dans cette partie. Les deux images sont block.png et bob_01.png. Ces images sont disponibles ici. Dans le fichier téléchargé, plusieurs images de Bob sont disponibles. Ceux-ci seront utilisés lors de l’animation du personnage. La prochain étape consiste à faire un peu de ménage dans la classe WorldRenderer. Nous allons séparer le traçage des rectangles dans une méthode séparée et utilisée cette dernière pour le débogage. De plus, nous allons charger les textures et les rendre dans l’écran./** package et importations omises **/ public class WorldRenderer { private static final float CAMERA_WIDTH = 10f; private static final float CAMERA_HEIGHT = 7f; private World world; private OrthographicCamera cam; /** * ShapeRenderer permet de dessiner facilement les * formes de base * Sera utilisé pour des fins de débogage * **/ ShapeRenderer debugRenderer = new ShapeRenderer(); /** Textures **/ private Texture bobTexture; private Texture blockTexture; private SpriteBatch spriteBatch; private boolean debug = false; private int width; private int height; private float ppuX; // pixels par unité pour X private float ppuY; public void setSize (int w, int h) { this.width = w; this.height = h; ppuX = (float)width / CAMERA_WIDTH; ppuY = (float)height / CAMERA_HEIGHT; } public WorldRenderer(World world, boolean debug) { this.world = world; this.cam = new OrthographicCamera(CAMERA_WIDTH, CAMERA_HEIGHT); this.cam.position.set(CAMERA_WIDTH / 2f, CAMERA_HEIGHT / 2f, 0); this.cam.update(); this.debug = debug; spriteBatch = new SpriteBatch(); loadTextures(); } private void loadTextures() { bobTexture = new Texture(Gdx.files.internal("images/bob_01.png")); blockTexture = new Texture(Gdx.files.internal("images/block.png")); } public void render() { spriteBatch.begin(); drawBlocks(); drawBob(); spriteBatch.end(); if (debug) drawDebug(); } private void drawBlocks() { for (Block block : world.getBlocks()) { spriteBatch.draw( blockTexture, block.getPosition().x * ppuX, block.getPosition().y * ppuY, Block.getSize() * ppuX, Block.getSize() * ppuY); } } private void drawBob() { Bob bob = world.getBob(); spriteBatch.draw( bobTexture, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.getSize() * ppuX, Bob.getSize() * ppuY); } private void drawDebug() { // Démarrage du renderer debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Line); // render blocks for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x ; float y1 = block.getPosition().y ; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // Rendre Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x ; float y1 = bob.getPosition().y ; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } }
- CAMERA_WIDTH et _HEIGHT : Constantes pour la dimension du viewport.
- bobTexture et blockTexture : Textures qui seront dessinés.
- spriteBatch : Système qui s’occupe du rendu des textures.
- debug : Utiliser dans le constructeur lorsque l’on désirera afficher les contours des objets.
- ppuX et ppuY sont les facteurs de multiplication pour ajuster les positions et dimensions des unités du jeu en pixel. Par exemple si l’écran a une dimension de 1920 x 1080, chaque bloc aura 192px x 153.4px soit (1920 / 10) x (1080 / 7).
- width et height gardent la dimension de l’écran en pixel.
- setSize est utilisé pour calculer le ratio de pixel par unité. Cette méthode est appelé à chaque fois qu’il y aura un redimensionnement de l’écran.
- loadTextures() : Méthode utilisée pour charger les textures.
- render() : On sépare la partie debug de la méthode de rendu principale.
- spriteBatch démarre la procédure de rendu, ajoute à la pile les éléments à dessiner et ensuite lance la procédure de rendu. Plus d’info ici.
- drawBlocks et drawBob sont similaires. Ils dessinent chacun de leurs éléments à la position et dimension indiqués multipliés par les facteurs de redimensionnement ppuX et ppuY.
/** ... omis ... **/ public void show() { world = new World(); renderer = new WorldRenderer(world, true); } public void resize(int width, int height) { renderer.setSize(width, height); } /** ... omis ... **/L’application devrait ressembler à ceci sans le débogage. Avec le débogage activé. Essayez-le sur une plateforme Android pour voir comment il s’affiche.
Traiter les entrées sur PC et Android
Pour l’instant, le jeu ne fait qu’afficher Bob et les blocs donc rien d’extravagant. Pour en faire un jeu, nous devons traiter les entrées pour créer des actions basées sur ceux-ci. Les touches sur le PC seront relativement simple. Nous allons utiliser les flèches avec les lettres Z et X pour respectivement faire déplacer, sauter et tirer Bob. Pour l’Android nous allons devoir définir des zones de touches où on simulera l’appuie de touches. Pour continuer dans la veine du MVC, nous séparer les contrôles des modèles et des vues. Si ce n’est déjà fait, créez un package controller. Pour débuter, nous allons contrôler Bob par les touches. Pour jouer, nous allons devoir considérer 4 touches soient gauche, droite, sauter et tirer. Parce que l’on utilise deux types d’entrées (clavier et tactile), les événements devront être fournis à un processeur qui lancera les actions. Chaque action est lancé par un événement. Le déplacement à gauche est lancé après avoir appuyé sur la flèche de gauche ou une partie de l’écran est touchée. Le saut est lancé lorsque Z sera appuyé et ainsi de suite. Créez un contrôleur très simple appelé WorldController.package ca.collegeshawinigan.bobgame.controller; import java.util.HashMap; import java.util.Map; import ca.collegeshawinigan.bobgame.model.Bob; import ca.collegeshawinigan.bobgame.model.Bob.State; import ca.collegeshawinigan.bobgame.model.World; public class WorldController { enum Keys { LEFT, RIGHT, JUMP, FIRE } private World world; private Bob bob; static Map<Keys, Boolean> keys = new HashMap<WorldController.Keys, Boolean>(); // Initialisation static du hashmap static { keys.put(Keys.LEFT, false); keys.put(Keys.RIGHT, false); keys.put(Keys.JUMP, false); keys.put(Keys.FIRE, false); }; public WorldController(World world) { this.world = world; this.bob = world.getBob(); } // ** Écran touchée ou touche appuyée *********** // public void leftPressed() { keys.get(keys.put(Keys.LEFT, true)); } public void rightPressed() { keys.get(keys.put(Keys.RIGHT, true)); } public void jumpPressed() { keys.get(keys.put(Keys.JUMP, true)); } public void firePressed() { keys.get(keys.put(Keys.FIRE, false)); } public void leftReleased() { keys.get(keys.put(Keys.LEFT, false)); } public void rightReleased() { keys.get(keys.put(Keys.RIGHT, false)); } public void jumpReleased() { keys.get(keys.put(Keys.JUMP, false)); } public void fireReleased() { keys.get(keys.put(Keys.FIRE, false)); } /** Méthode de mise à jour principale **/ public void update(float delta) { processInput(); bob.update(delta); } /** Modification des paramètres et de l'état de Bob selon les entrées **/ private void processInput() { if (keys.get(Keys.LEFT)) { // Flèche de gauche bob.setFacingLeft(true); bob.setState(State.WALKING); bob.getVelocity().x = -Bob.getSpeed(); } if (keys.get(Keys.RIGHT)) { // Flèche de droite bob.setFacingLeft(false); bob.setState(State.WALKING); bob.getVelocity().x = Bob.getSpeed(); } // On immobilise Bob si les deux touches sont appuyées. if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) || (!keys.get(Keys.LEFT) && !(keys.get(Keys.RIGHT)))) { bob.setState(State.IDLE); // acceleration is 0 on the x bob.getAcceleration().x = 0; // horizontal speed is 0 bob.getVelocity().x = 0; } } }
- On définit un énumération des actions de Bob.
- On définit un nouvel HashMap de clés et de booléen. Ce HashMap sera utilisé pour garder l’état des touches.
- On initialise statiquement chacune des touches à faux.
- Pour chacune des touches, une fois appuyée ou relâchée, on met à jour l’action.
- La méthode processInput permet de traiter les informations entrées par les touches. Dans le cas présent, nous modifions l’état et autres propriétés de Bob.
public static final float SPEED = 4f; // unité par seconde public void setState(State newState) { this.state = newState; } public void update(float delta) { position.mulAdd(velocity.cpy(),delta); }
- Nous n’avons que mis à jour la vitesse de déplacement de Bob.
- De plus, la méthode setState a été ajoutée, car elle a été oubliée plus tôt.
- La méthode update() permet de mettre à jour la position de Bob à l’aide du temps d’exécution du jeu. Cette méthode sera appelé par le WorldController. Nous utilisons velocity.tmp(), car elle permet de cloner l’objet (velocity) avec la même valeur et ensuite on multiplie ce vecteur avec le temps delta en seconde qui constitue généralement une fraction de seconde. On doit cloner l’objet, sinon on modifie l’objet velocity.
- Exemple
- position <– (2, 3)
- velocity <– (4, 0)
- delta <– 0.25
- position <– velocity * delta + position <– (4, 0) * 0.25 + (2, 3)
- position <– (5, 3)
/** IMPORTATIONS OMISES **/ // On fait hérité GameScreen d'InputProcessor public class GameScreen implements Screen, InputProcessor { private World world; private WorldRenderer renderer; private WorldController controller; private int width, height; @Override public void show() { world = new World(); renderer = new WorldRenderer(world, false); controller = new WorldController(world); Gdx.input.setInputProcessor(this); } @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); controller.update(delta); renderer.render(); } @Override public void resize(int width, int height) { renderer.setSize(width, height); this.width = width; this.height = height; } @Override public void hide() { Gdx.input.setInputProcessor(null); } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { Gdx.input.setInputProcessor(null); } // * Méthode InputProcessor ***************************// @Override public boolean keyDown(int keycode) { if (keycode == Keys.LEFT) controller.leftPressed(); if (keycode == Keys.RIGHT) controller.rightPressed(); if (keycode == Keys.Z) controller.jumpPressed(); if (keycode == Keys.X) controller.firePressed(); return true; } @Override public boolean keyUp(int keycode) { if (keycode == Keys.LEFT) controller.leftReleased(); if (keycode == Keys.RIGHT) controller.rightReleased(); if (keycode == Keys.Z) controller.jumpReleased(); if (keycode == Keys.X) controller.fireReleased(); return true; } @Override public boolean keyTyped(char character) { // TODO Auto-generated method stub return false; } @Override public boolean touchDown(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftPressed(); } if (x > width / 2 && y > height / 2) { controller.rightPressed(); } return true; } @Override public boolean touchUp(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftReleased(); } if (x > width / 2 && y > height / 2) { controller.rightReleased(); } return true; } @Override public boolean touchDragged(int x, int y, int pointer) { // TODO Auto-generated method stub return false; } @Override public boolean mouseMoved(int x, int y) { // TODO Auto-generated method stub return false; } @Override public boolean scrolled(int amount) { // TODO Auto-generated method stub return false; } }
- On fait hérité GameScreen de InputProcessor
- Les méthodes abstraites qui doivent être codées sont ajoutées.
- Pour chaque touche qui est appuyé ou relâché, on gère l’action qui doit être effectuée.
- Pour les méthodes touchUp et touchDown qui sont liées aux écrans tactiles, on sépare l’écran en 4. Le coin inférieur droit et gauche sont respectivement l’équivalent des touches droites et gauches.
if (!Gdx.app.getType().equals(ApplicationType.Android)) return false;
- Ce code ne fait que retourner faux si l’application n’est pas roulée sur Android.
Résumé
En résumé, nous avons pu affiché le décor et Bob. Ainsi que déplacer Bob de gauche à droite. Il reste beaucoup de chose à faire pour rendre le jeu plus intéressant. Peu à peu nous allons introduire différentes notions pour améliorer le jeu. Nous avons besoin d’effectuer les tâches suivantes :- Interaction avec le terrain collision et saut
- Animation
- Un niveau plus grand avec une caméra qui suit Bob
- Ennemies et fusils
- Sons
- Améliorer les contrôles
- Des écrans supplémentaires pour la fin de la partie et le début
- et plus avec LibGdx!
Laisser un commentaire