Exercice pratique : le DOODLE JUMP
Bonjour,
Aujourd'hui on attaque le DOODLE JUMP avec des classes. Ce jeu existe dans de nombreuses version et sous forme de non moins nombreux tutoriels, je vais donc me permettre de ne pas respecter les conventions d'usage et prendre des libertés dans la conception de l'exercice, la plupart des notions ont déjà été abordées dans les autres exercices et je ne doute pas un instant que vous soyez capable de créer un Doodle Jump à partir de ce que vous avez déjà lu, on va donc explorer des voies incertaines pour celui-ci. Avant tout il me faut donc vous avertir que ce n'est pas forcément la meilleure solution qui est évoquée ci-dessous, le jeu sert juste de support à des tests
Tout d'abord le résultat :
Les sources sont disponibles en fin d'exercice
Les pré-requis
Pour ce programme vous devez connaître :
Fonctions et paramètres : http://help.adobe.com/fr_FR/FlashPlatform/reference/actionscript/3/statements.html#function
Manipulation de tableaux : http://help.adobe.com/fr_FR/FlashPlatform/reference/actionscript/3/Array.html
Ecouteurs d'événements : http://help.adobe.com/fr_FR/FlashPlatform/reference/actionscript/3/flash/events/package-detail.html
Programmation Orienté Objet : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/programmation/poo_bases/classesqqc
Exercices pratiques précédents de la série : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux
Etude préliminaire
Tout d'abord le DOODLE JUMP c'est quoi ? (merci Wikipedia)
Doodle Jump est un jeu de type casual game pour appareils mobiles, créé par la société Lima Sky. Il est disponible sur les smartphones Android, BlackBerry, iOS Symbian, Windows Phone ainsi que les téléphones classiques compatibles Java.
Doodle Jump s'inspire du jeu japonais Papijump de Sunflat Games.
Le but est d'amener un personnage à quatre jambes, le gribouilleur (the Doodler, en anglais), le plus haut possible, en le faisant sauter de plateforme en plateforme, tout en évitant les obstacles qui se présentent au cours de son ascension.
Chaque plateforme réagit différemment lorsque le personnage saute dessus. La couleur de la plateforme peut varier selon les différents thèmes (la couleur indiquée ci-dessous est celle du mode par défaut de Doodle Jump).
Vert : Au début du jeu, cela semble être le type le plus commun de plateforme. Toutefois, plus le jeu progresse et le score est grand, moins les plates-formes vertes sont fréquentes.
Bleu clair : Ces plateformes se déplacent latéralement à des vitesses variables jusqu'à 50 000, puis la vitesse reste la même.
Bleu-gris : Ces plateformes se déplacent verticalement à une vitesse constante.
Bleu marine : Ces plateformes peuvent être déplacées par le joueur. Elles se transforment en plate-forme vertes ensuite.
Marron : Ces plateformes se cassent si le personnage saute dessus.
Jaune : Après quelques secondes, ces plateformes deviennent rouge, puis explosent et disparaissent.
Blanc : Ces plateformes disparaissent après que le personnage ait sauté dessus.
Monstre : Les différents monstres, statiques ou mobiles, peuvent être évités ou tués en tirant dessus.
OVNIs : Le personnage est aspiré par la soucoupe volante.
Trous noirs : Le personnage est aspiré par le trou noir s'il saute devant.
Comme nous sommes dans le cadre d'un exercice on va se limiter à quelques fonctionnalités de base pour les plateformes et éliminer complètement les ennemis, il ne restera donc que le principe de base du jeu, à savoir générer les plateformes, faire sauter le personnage dessus, faire réagir le personnage en fonction des plateformes, faire défiler les plateformes et le fond, mourir et recommencer.
Préparation
On va commencer par préparer le travail, ouvrez un nouveau projet Flash et créez :
Un MovieClip “Plateforme” comportant une plateforme différente pour chaque frame.
Un MovieClip “Joueur” comportant sur trois frames les trois positions du joueur : gauche, face, droite.
Un Bitmap “Fond” composé uniquement d'une texture (il suffit d'exporter une image pour AS depuis la bibliothèque).
Tous les objets sont exportés pour AS et les clips “Plateforme” et “Joueur” ont pour classe de base “Plateforme” et “Joueur”, nous devrons écrire ces classes plus tard.
Pour la préparation c'est tout, passons au code.
Programme principal
Sur la première frame de votre projet tapez le code suivant. Pour ceux qui n'utilisent pas du tout l'IDE faites une classe de document “Main” avec le même code converti en classe et les imports nécessaires.
init(); function init():void{ InfosJeu.hauteur = stage.stageHeight; InfosJeu.largeur = stage.stageWidth; addChild(new Rendu(new Fond())); for (var i:int=0; i<22; i++) addChild(new Plateforme()); addChild(new Joueur()); }
Comment, quoi ? C'est tout ?
Hé bien oui, c'est effectivement tout, mais rien que là ça demande quelques explications. Tout d'abord comme vous pouvez le constater, il n'y a pas grand chose à part une fonction “init()” qu'on lance tout de suite, à vrai dire elle n'est pas très utile, sauf si vous décidez de faire un jeu plus complet, on aurait très bien pu se passer de la fonction et conserver juste les instructions.
InfosJeu.hauteur = stage.stageHeight; InfosJeu.largeur = stage.stageWidth;
Ok, ça commence à se compliquer dès les deux premières lignes, en fait ici ce que vous ne voyez pas c'est la classe “InfosJeu” qui est importée automatiquement dans l'IDE puisqu'elle se trouve à la racine du projet (au même endroit que le FLA), Flash l'importe donc de lui même sans qu'on lui demande quoi que ce soit, ce qui explique que vous ne me voyez pas importer cette classe.
Cette classe va me permettre de centraliser toutes les informations pratiques du jeu, il y a bien d'autres méthodes pour faire ça, mais comme je l'ai dit plus haut, je vais m'autoriser à jouer un peu moi aussi. Je crée donc une classe qui centralise toutes les informations pratiques, chaque partie du programme qui agit sur un des paramètres global du jeu devra aller renseigner cette classe et modifier le paramètre en question. L'avantage c'est que je n'ai plus besoin de fouiller dans mes objets pour récupérer des valeurs que cette classe me garde bien au chaud. On pourrait passer par des variables globales, ou tout centraliser dans la classe “Main” si vous en avez fait une, etc…. mais je trouve cette idée d'utiliser une classe unique pour stocker à un seul endroit toutes les infos utiles assez intéressante. On détaillera cette classe juste après, mais avant voyons ce que la fonction “init()” fait.
On commence donc par renseigner deux paramètres généraux, la hauteur et la largeur du jeu.
addChild(new Rendu(new Fond()));
Puis on crée le fond, mention spéciale ici car ce n'est qu'un artifice inutile pour le coeur du jeu, on étudiera donc ça en fin d'exercice, notez simplement la construction, j'ajoute à la liste d'affichage un nouveau rendu à partir de la classe “Rendu” (pas encore écrite), et je lui passe une nouvelle texture “Fond” (le bitmap que vous avez exporté depuis la bibliothèque). Je n'aurai jamais besoin d'aller chercher des infos dans le rendu, ni de l'utiliser pour autre chose que du décor, je n'ai donc pas besoin d'en faire un objet lié à une variable, je peux directement créer un objet générique, il en va de même pour le bitmap utilisé comme texture.
for (var i:int=0; i<22; i++) addChild(new Plateforme());
Ensuite je crée une boucle sur 22 itérations, pour chacune je pose une nouvelle plateforme, là aussi je ne passe pas par une variable intermédiaire, je créer directement un objet “Plateforme” générique, vous comprendrez pourquoi quand on attaquera la classe “Plateforme”.
addChild(new Joueur());
Et enfin j'ajoute un joueur, même combat que pour les ajouts précédents, pas besoin de passer par une variable intermédiaire, on crée directement un objet générique.
Grossièrement, le point d'entrée du programme ne se charge que de créer un certain nombre d'objets génériques, pour le reste c'est aux objets de se débrouiller entre eux.
La base étant prête, on s'attaque aux classes…
InfosJeu
Créez un nouveau fichier AS nommé “InfosJeu” et écrivez :
package { public class InfosJeu { public static var largeur:int = 640; public static var hauteur:int = 711; public static var joueurX:int = 0; public static var joueurY:int = 0; public static var vitesse:int = 0; // Constructeur public function InfosJeu ():void {}; } }
Cette classe à un seul objectif, centraliser les paramètres du jeu utilisés par les objets, il s'agit entre autre de la largeur et la hauteur du jeu, de la position du joueur et de sa vitesse. Elle n'est donc constituée que de variables, publiques pour être accessibles en dehors de la classe et statiques car elles sont uniques quelque soit le nombre d'instances de la classe.
Chaque partie du programme qui modifie un de ces paramètres devra renseigner cette classe afin que les autres objets puissent utiliser cette info.
Comme les variables sont publiques et statiques, et que cette classe est dédiée au stockage de variables, pas besoin de getter/setter, pour modifier ou interroger un paramètre il suffit à un objet de donner le nom de la classe et d'accéder à la variable grâce à une syntaxe pointée.
Plateforme
Créez un nouveau fichier AS nommé “Plateforme” et écrivez :
package { import flash.display.MovieClip; import flash.events.Event; public class Plateforme extends MovieClip { public var type:int; public var sens:int; public static var stock:Array = []; public static var nbPlateformes:int; private var H:int; private var W:int; // Constructeur public function Plateforme ():void { H = InfosJeu.hauteur; W = InfosJeu.largeur; stock.push(this); addEventListener(Event.ADDED_TO_STAGE, init); }; private function init(e:Event):void { removeEventListener(Event.ADDED_TO_STAGE, init); addEventListener(Event.ENTER_FRAME, update); } private function placePlateforme():void { nbPlateformes++ sens = 1; x = int(Math.random()*10)*32+158; y = nbPlateformes * 32; type = Math.random() * 4+1; if (nbPlateformes == 22) { x = 304; y = 694; type = 1; } gotoAndStop(type); } private function update(e:Event):void { if (nbPlateformes < 22) placePlateforme();// relance un niveau if (InfosJeu.joueurY <= H*.5) y -= InfosJeu.vitesse;// si le joueur monte les plateformes descendent // plateformes mobiles if(type==3) { x += 4*sens; if(x>=W-158-32 || x<158) sens *= -1; }; // replace la plateforme qui sort if (y >= H) { x = int(Math.random()*10)*32+158; y -= H; type = Math.random() * 4 + 1; this.visible = true; gotoAndStop(type); } } } }
Voici le programme de chaque plateforme, il est assez simple mais on va le détailler quand même.
La classe est la classe de base des plateformes, elle étend donc MovieClip, on commence par les variables :
public var type:int; public var sens:int; public static var stock:Array = []; public static var nbPlateformes:int; private var H:int; private var W:int;
Deux variables publiques, une contient le type de la plateforme, l'autre le sens du déplacement si la plateforme est mobile. Les deux variables suivantes sont publiques et statiques, il s'agit du stock des plateformes et de leur nombre. Vous noterez ici que le stock est un tableau qui conserve en mémoire toutes les plateformes créées, je peux accéder à ce tableau facilement en interrogeant la classe “Plateforme” et il est unique quel que soit le nombre de plateformes. C'est assez pratique car c'est la classe d'un objet qui stocke l'ensemble des occurrences qu'elle génère, ainsi pour atteindre une occurrence précise il me suffit d'interroger le stock de la classe. En gros la classe se débrouille toute seule pour générer les objets, les stocker et en compter le nombre exact.
// Constructeur public function Plateforme ():void { H = InfosJeu.hauteur; W = InfosJeu.largeur; stock.push(this); addEventListener(Event.ADDED_TO_STAGE, init); };
A chaque création d'une nouvelle plateforme, on récupère la largeur et la hauteur du jeu, on ajoute l'occurrence au stock de la classe et on ajoute un écouteur pour savoir quand la plateforme est ajoutée à la liste d'affichage.
private function init(e:Event):void { removeEventListener(Event.ADDED_TO_STAGE, init); addEventListener(Event.ENTER_FRAME, update); }
Quand la plateforme est ajoutée à la liste d'affichage on ajoute un écouteur “update” et on retire l'écouteur “init”.
private function placePlateforme():void { nbPlateformes++ sens = 1; x = int(Math.random()*10)*32+158; y = nbPlateformes * 32; type = Math.random() * 4+1; if (nbPlateformes == 22) { x = 304; y = 694; type = 1; } gotoAndStop(type); }
Cette fonction sert à placer les platefomes, chaque nouvelle plateforme placée augmente le nombre de plateformes total, son sens est par défaut de 1, sa position sur X est aléatoire entre deux bornes, sa position sur Y dépend du nombre de plateformes actuelles multiplié par deux fois la hauteur d'une plateforme et son type est aléatoire entre 1 et 4.
La plateforme portant le numéro 22 est particulière, c'est la dernière à être placée et elle doit se trouver exactement sous les pieds du joueur afin de l'empêcher de tomber, j'ai saisi ces valeurs à la main, mais vous pouvez les placer dans la classe InfosJeu pour peu que vous l'ayez renseignée avant.
private function update(e:Event):void { if (nbPlateformes < 22) placePlateforme();// relance un niveau if (InfosJeu.joueurY <= H*.5) y -= InfosJeu.vitesse;// si le joueur monte les plateformes descendent // plateformes mobiles if(type==3) { x += 4*sens; if(x>=W-158-32 || x<158) sens *= -1; }; // replace la plateforme qui sort if (y >= H) { x = int(Math.random()*10)*32+158; y -= H; type = Math.random() * 4 + 1; this.visible = true; gotoAndStop(type); } }
La fonction “update” met tout simplement à jour la plateforme, mais se charge aussi de les replacer toutes le cas échéant.
Si le nombre de plateformes total est inférieur à 22 (nombre max de plateformes), c'est que le joueur à perdu, ce nombre est alors fixé à 0 par le joueur, on replace donc toutes les plateformes en boucle jusqu'à ce que le nombre de plateformes total soit égal à 22.
Si le joueur monte et atteint ou dépasse le centre de l'écran, les plateformes descendent de la vitesse du joueur.
Les plateformes mobiles se déplacent d'elles même, lorsqu'elles arrivent à une extrémité elles changent de sens.
Si une plateforme sort du jeu par le bas elle est replacée aléatoirement sur X, à sa position moins la hauteur du jeu sur Y, sont type est changé et elle repasse visible pour le cas où elle serait passée invisible à un moment donné (on verra ça dans le joueur).
C'est tout pour les plateformes, on passe au joueur.
Joueur
Créez un nouveau fichier AS nommé “Joueur” et écrivez :
package { import flash.display.MovieClip; import flash.events.KeyboardEvent; import flash.events.Event; import flash.geom.Point; public class Joueur extends MovieClip { private var haut:int; private var vY:int; private var vX:int; private var gauche:int; private var droite:int; private var contact:Point; private var H:int; private var W:int; // Constructeur public function Joueur ():void { H = InfosJeu.hauteur; W = InfosJeu.largeur; x = W*.5-width*.5; y = H-64; vX = 8; vY = 0; contact = new Point(x-width*.5, y+height); addEventListener(Event.ADDED_TO_STAGE, init); }; private function init(e:Event):void { removeEventListener(Event.ADDED_TO_STAGE, init); stage.addEventListener(KeyboardEvent.KEY_DOWN,appuye); stage.addEventListener(KeyboardEvent.KEY_UP, relache); addEventListener(Event.ENTER_FRAME, update); } // Appuyer sur une touche private function appuye (e:KeyboardEvent):void { if (e.keyCode == 37) gauche = 1; if (e.keyCode == 39) droite = 1; if (e.keyCode == 38) haut = 1; } // Relâcher une touche private function relache (e:KeyboardEvent):void{ if (e.keyCode == 37) gauche = 0; if (e.keyCode == 39) droite = 0; if (e.keyCode == 38) haut = 0; } // mise à jour de l'objet private function update(e:Event):void { x += vX*(droite-gauche); y += vY++; if (y<H*.5) y = H*.5; if (y > H) { trace("perdu"); x = W*.5-width*.5; y = H - 64; Plateforme.nbPlateformes = 0; } contact.x = x+width*.5; contact.y = y+height; gotoAndStop(int(gauche)-int(droite)+2) // collisions avec les plateformes for each(var i in Plateforme.stock) { if (collisions(contact, i) && i.visible) { if (vY>=0) { y = i.y-height; vY = -haut*20; if (i.type == 2) vY = -64; if (i.type == 3) x = i.x; if (i.type == 4 && haut) i.visible = false; } } } updateInfos() } // mise à jour les infos générales pour cet objet private function updateInfos():void { InfosJeu.joueurX = x; InfosJeu.joueurY = y; InfosJeu.vitesse = vY; } // test de collision private function collisions(A:Point, B:Object):Boolean { if (A.x>=B.x && A.x<B.x+B.width && A.y>=B.y && A.y-Math.abs(vY)<B.y+B.height) return true; return false; } } }
C'est le gros morceau de l'exercice, on va aller très vite car je considère que vous avez fait les exercices précédents (si ce n'est pas le cas faites-le) et qu'on y retrouve les mêmes choses.
private var haut:int; private var vY:int; private var vX:int; private var gauche:int; private var droite:int; private var contact:Point; private var H:int; private var W:int;
Toutes les variables sont privée.
// Constructeur public function Joueur ():void { H = InfosJeu.hauteur; W = InfosJeu.largeur; x = W*.5-width*.5; y = H-64; vX = 8; vY = 0; contact = new Point(x-width*.5, y+height); addEventListener(Event.ADDED_TO_STAGE, init); };
Lorsqu'on crée le joueur on récupère la hauteur et la largeur du jeu, on le place, on lui donne une vitesse nulle sur chaque axe, on crée le point de contact qui servira aux collisions (le centre du joueur à ses pieds) et on regarde quand il est ajouté à la liste d'affichage.
private function init(e:Event):void { removeEventListener(Event.ADDED_TO_STAGE, init); stage.addEventListener(KeyboardEvent.KEY_DOWN,appuye); stage.addEventListener(KeyboardEvent.KEY_UP, relache); addEventListener(Event.ENTER_FRAME, update); }
Lorsqu'il est ajouté à la liste d'affichage, on retire l'écouteur qui sert pour l'ajout, on ajoute ceux du clavier et l'update.
Je passe sur les événements clavier, ça a été dit et redit depuis le début de ces exercices et on attaque directement la mise à jour du joueur.
// mise à jour de l'objet private function update(e:Event):void { x += vX*(droite-gauche); y += vY++; if (y<H*.5) y = H*.5; if (y > H) { trace("perdu"); x = W*.5-width*.5; y = H - 64; Plateforme.nbPlateformes = 0; } contact.x = x+width*.5; contact.y = y+height; gotoAndStop(int(gauche)-int(droite)+2) // collisions avec les plateformes for each(var i in Plateforme.stock) { if (collisions(contact, i) && i.visible) { if (vY>=0) { y = i.y-height; vY = -haut*20; if (i.type == 2) vY = -64; if (i.type == 3) x = i.x; if (i.type == 4 && haut) i.visible = false; } } } updateInfos() }
A chaque frame, on déplace le joueur sur X et sur Y en fonction des touches enfoncées, si la position du joueur dépasse la moitié du jeu en hauteur on fixe la position du joueur à la moitié de la hauteur du jeu, c'est à ce moment là que les plateformes vont commencer à monter.
Si le joueur dépasse le bas du jeu, on estime qu'il est tombé et que la partie est perdu, d'habitude je vous fait un joli panneau pour dire que la partie est terminée et pour relancer le jeu, là on relance le jeu direct en replaçant le joueur à sa position de départ et en modifiant le nombre total de plateformes, du coup si vous vous rappelez la classe précédente, toutes les plateformes vont se repositionner.
On met à jour le point de contact en fonction de la position du joueur et on affiche la pose qu'il doit prendre en fonction des touches.
Il faut à présent tester la collision entre le joueur et les plateformes, là c'est assez simple, on boucle sur toutes les plaformes stockées dans le stock de la classe “Plateforme”, on vérifie la collision avec un simple test de collision entre une AABB et un Point (on verra la formule ensuite) et on regarde si la plateforme est visible. Si il y a collision et que la plateforme est visible, si le joueur est en train de descendre ou est à l'arrêt, on replace le joueur sur la plateforme, on lui laisse la possibilité de sauter, on regarde le type de la plateforme et on agit en conséquence. Notez que c'est à ce moment qu'une plateforme sensée se briser devient invisible dès que le joueur à sauté depuis cette plateforme.
Et pour finir on met à jour les infos générales.
// mise à jour les infos générales pour cet objet private function updateInfos():void { InfosJeu.joueurX = x; InfosJeu.joueurY = y; InfosJeu.vitesse = vY; }
Les paramètres du joueur ont changé, il faut donc renseigner la classe “InfosJeu”.
// test de collision private function collisions(A:Point, B:Object):Boolean { if (A.x>=B.x && A.x<B.x+B.width && A.y>=B.y && A.y-Math.abs(vY)<B.y+B.height) return true; return false; }
Pour les tests de collisions voir ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/fiche_collisions
Rendu
Techniquement la base du moteur du jeu est terminée, mais tant qu'a faire on va ajouter un petit artifice qui permet de faire défiler le fond.
Créez un nouveau fichier AS nommé “Rendu” et écrivez :
package { import flash.display.Bitmap; import flash.display.Sprite; import flash.geom.Rectangle; import flash.geom.Point; import flash.display.BitmapData; import flash.events.Event; import flash.display.Graphics; public class Rendu extends Sprite { private var ecran:BitmapData; private var texture:BitmapData; private var surface:Rectangle; private var origine:Point; private var H:int; private var W:int; // Constructeur public function Rendu (fond:BitmapData):void { H = InfosJeu.hauteur; W = InfosJeu.largeur; ecran = new BitmapData(W,H,false,0); surface = new Rectangle(0,0,W,237); origine = new Point(0, 0); texture = fond; addEventListener(Event.ADDED_TO_STAGE, init); }; private function init(e:Event):void { removeEventListener(Event.ADDED_TO_STAGE, init); addEventListener(Event.ENTER_FRAME, update); } private function update(e:Event):void { graphics.clear(); ecran.lock(); if (InfosJeu.joueurY <= H*.5) surface.y += InfosJeu.vitesse; if(surface.y>237) surface.y = 0; if(surface.y<0) surface.y = 237; origine.y = 0; ecran.copyPixels(texture, surface, origine); origine.y = 237; ecran.copyPixels(texture, surface, origine); origine.y = 474; ecran.copyPixels(texture, surface, origine); ecran.unlock(); graphics.beginBitmapFill(ecran,null,true,false); graphics.drawRect(0,0,W,H); } } }
Cette classe va être votre exercice à vous, je vais me contenter de vous expliquer ce qu'elle fait sommairement. Pour faire défiler le fond j'utilise une texture dont la largeur fait la taille de l'écran et la hauteur fait deux fois un tiers (donc deux tiers) de la hauteur de la zone de jeu. Je dessine un bitmap faisant toute la zone de jeu et détermine une surface faisant un tiers de la zone de jeu. A chaque frame, comme pour les plateformes, si le joueur monte et atteint ou dépasse le centre de l'écran, le décor doit descendre. Je déplace donc la position de la surface à dessiner de la vitesse du joueur, si la position Y de la surface dépasse sa propre hauteur, elle est replacée à zéro, on obtiend donc une boucle. Je trace ensuite trois zones dans mon bitmap, chacune faisant la taille de ma surface mais étant positionnée juste en dessous de la précédente, j'ai donc rempli ma zone de jeu avec trois fois la même surface, le dessin à tracé étant positionné à une hauteur différente à chaque fois. L'astuce réside dans la taille de ma texture qui fait deux fois la taille de ma surface et dans le fait que ma surface boucle sur la position Y entre le 0 et sa propre hauteur (237).
Conclusion
Ce n'est certes pas une oeuvre d'art, d'autant que j'ai fait fit des conventions, mais ce petit exercice aborde tout un tas de petite techniques bien utiles lorsqu'on fait un jeu. Vous pouvez le faire plus simplement via l'IDE ou bien encore plus proprement via une structure POO propre, le moteur n'est pas bien compliqué c'est donc très malléable.
Les sources
