Programmer un jeu avec Java Micro Edition
Date de publication : 7 Mai 2009 , Date de mise à jour : 4 Juin 2009
Par
Alain Defrance
JME est une plateforme permettant de programmer en Java sur divers périphériques dans une version allégée de Java. Même si ce n'est pas la totalité des champs d'application, JME est très souvent utilisé pour le développement de jeux sur téléphone mobile. Ici nous verrons comment développer un jeu de ce type.
I. Introduction
II. Les contraintes de Java Micro Edition
III. L'application principale
III-A. Le Midlet
III-B. Le Contexte de l'application
IV. Création du menu
IV-A. Formulaire principal
IV-B. Le listener de cette fenêtre
V. Gestion des images
VI. Nos beans
VI-A. Les monstres
VI-B. L'explosion
VI-C. Les balles
VI-D. Le vaisseau spatial
VI. Les timers
VI-A. Apparition des monstres
VI-B. Déplacement des éléments (monstres et balles)
VIII. Assemblage
VIII-A. Dessiner sur l'écran : le Canvas
VIII-B. Interaction avec le joueur : la pression des touches
IX. Javadoc, sources, et captures d'écrans
X. Conclusion
XI. Remerciements
I. Introduction
Nous allons programmer une variante d'un space invader. Les objectifs sont les suivants :
-
3 vies initiales
-
Plusieurs niveaux de difficulté qui se caractérisent par un nombre variable d'ennemis
-
Avoir un vaisseau que l'on doit pouvoir déplacer de droite à gauche
-
Avoir différents ennemis qui ne doivent pas atteindre le bas de l'écran sous peine de perdre une vie
-
Avoir la possibilité de tirer sur les ennemis pour les détruie
-
Gestion du score
Nous allons développer ce petit jeu avec Java Micro Edition (alias JME). Il a la particularité d'être
présent sur beaucoup de mobiles, l'application sera donc déployable facilement.
JME possède certaines particularités que nous allons voir, ce qui nous obligera à adapter notre
code.
Pour finir, nous utiliserons l'éditeur NetBeans pour notre développement
II. Les contraintes de Java Micro Edition
JME est souvent utilisé sur des périphériques mobiles, comme des téléphones portables, mais aussi des
imprimantes et divers périphériques.
Nous allons développer une application pour téléphone portable en utilisant la configuration CLDC
et le profile MIDP. Notre machine virtuelle sera donc une KVM (Kilobyte Virtual Machine).
Notre machine virtuelle n'est donc pas aussi complète qu'une JVM (java Virtual Machine), et nous
aurons à nous passer de certaines fonctionnalités (comme les generics).
Tout ceci n'est pas grave et nous passerons facilement outre ces contraintes.
Un autre point important est que la puissance, ainsi que la mémoire, d'un téléphone portable est très
largement inférieure à celle de notre machine utilisée pour le développement. Pour effectuer nos
tests, nous utiliserons un émulateur, et nous disposerons donc de toute la puissance de notre machine.
Mais une fois déployée, l'application devra être suffisamment légère pour être supportée par la configuration
matérielle du téléphone portable.
III. L'application principale
III-A. Le Midlet
Dans une application créée avec Java Micro Edition, chaque application est une sous-classe de la classe
MIDlet. Cette classe possède 3 méthodes abstraites : startApp(), pauseApp(), destroyApp(boolean).
Nous allons utiliser cette classe pour récupérer le Display (l'écran), et le stocker. Nous allons
par ailleurs en profiter pour coder une méthode qui nous permettra de changer l'élément Displayable
(composant graphique pouvant être contenu par un Display).
| La classe principale : SpaceInvader |
package com.developpez.java.spaceinvader.app;
import com.developpez.java.spaceinvader.hight.MainForm;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.midlet.MIDlet;
@author
public class SpaceInvader extends MIDlet {
private Display display;
protected void startApp() {
show(new MainForm(this));
}
protected void pauseApp() {
}
@param
public void destroyApp(boolean unconditional) {
}
@param
public void show(Displayable displayable) {
if(display == null) {
display = Display.getDisplay(this);
}
display.setCurrent(displayable);
}
}
|
III-B. Le Contexte de l'application
Qu'est-ce que le contexte de l'application ?
Dans beaucoup d'applications nous avons une notion de contexte, c'est-à-dire l'état de l'application.
Ce contexte sera une classe singleton car posséder plusieurs contextes n'a pas de sens dans notre
application.
Nous l'utiliserons pour stocker le nombre de vies, et le score. Par ailleurs il devra être capable
de générer des entiers aléatoirement entre 0 et x (apparition aléatoire des ennemis, type d'ennemis).
Le code reste relativement simple :
| Le contexte : GameContext |
package com.developpez.java.spaceinvader.app;
import java.util.Random;
@author
public class GameContext {
public int score = 0;
public int nbLife = 3;
private Random rd = new Random();
public static GameContext instance;
private GameContext() { }
@return
public static GameContext getInstance() {
if (instance == null) {
instance = new GameContext();
}
return instance;
}
public int getNbLife() {
return nbLife;
}
public int getScore() {
return score;
}
public Random getRd() {
return rd;
}
@param
public void incScore(int score) {
this.score += score;
}
public void kill() {
this.nbLife -= 1;
}
}
|
IV. Création du menu
Nous allons commencer par créer notre menu principal. Ce dernier sera lancé directement au chargement
de l'application et permettra de choisir un niveau de difficulté.
IV-A. Formulaire principal
Nous avons donc à gérer deux actions différentes : quitter le jeu et démarrer la partie, ce qui est réalisable par l'utilisation des Command. Ces commandes devront être identifiées afin de pouvoir différencier leurs événements. Nous emploierons des constantes afin de bénéficier de la lisibilité du switch.
Ainsi, nous allons écrire une classe qui étend la classe Form :
| Notre menu principal : MainForm |
package com.developpez.java.spaceinvader.hight;
import com.developpez.java.spaceinvader.app.SpaceInvader;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Gauge;
import javax.microedition.lcdui.TextField;
@author
public class MainForm extends Form {
private SpaceInvader app;
private Gauge level = new Gauge(
"Difficulté",
true,
5,
3);
private Command cmdExit = new Command(
String.valueOf(CMD_EXIT),
"Quit",
Command.EXIT,
1);
private Command cmdStart = new Command(
String.valueOf(CMD_START),
"Start",
Command.OK,
1);
public static final int CMD_EXIT = 0;
public static final int CMD_START = 1;
{
append(level);
addCommand(cmdExit);
addCommand(cmdStart);
}
@param
public MainForm(SpaceInvader app) {
super("My Space Invader");
this.app = app;
setCommandListener(new MainListener(this));
}
@return
public int getLevel() {
return level.getValue();
}
@return
public SpaceInvader getApp() {
return app;
}
}
|
IV-B. Le listener de cette fenêtre
Un listener déclenché par des commandes doit implémenter l'interface CommandListener qui possède
la méthode commandAction(Command c, Displayable d). L'argument c est la commande qui a déclenché
l'appel de la méthode, alors que d est L'élément graphique contenant la commande c.
Voici donc l'écriture du Listener :
| Listener du formulaire principal : MainListener |
package com.developpez.java.spaceinvader.hight;
import com.developpez.java.spaceinvader.low.MainCanvas;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
@author
public class MainListener implements CommandListener {
private MainForm form;
@param
public MainListener(MainForm form) {
this.form = form;
}
@param
@param
public void commandAction(Command c, Displayable d) {
switch(Integer.parseInt(c.getLabel())) {
case MainForm.CMD_START:
form.getApp().show(new MainCanvas(form.getLevel()+1));
break;
case MainForm.CMD_EXIT:
form.getApp().destroyApp(true);
form.getApp().notifyDestroyed();
break;
}
}
}
|
 |
Nous avons à présent complètement terminé le menu principal. Nous allons donc pouvoir nous concentrer
sur le développement de l'application proprement dite.
|
V. Gestion des images
Comme dans tous jeux, nous allons devoir charger et afficher des images afin de rendre notre jeu plus
esthétique. Pour ce faire, nous allons utiliser une classe ayant la responsabilité de charger en mémoire
les images lorsqu'elle en aura besoin (lazy loading), et le cas échéant, en créer une par défaut.
Cette classe n'aura jamais besoin d'être instanciée, et l'ensemble de son interface sera publique.
Afin de créer une image nous allons écrire une méthode noImg(int[] rgb, int size) qui construira
une image par défaut (un carré d'une certaine couleur). Cette méthode appellera la méthode createColor(int[]
rgb, int size) qui construira un tableau contenant les couleurs de chaque pixel de l'image créée
(dans notre cas, nous créerons un carré unicolore).
En ce qui concerne le chargement des images, ceci se fera selon l'algorithme suivant :
| Fonctionnement du chargement des images |
Si l'image n'est pas encore chargée
Alors tenter de charger l'image
Si on ne la trouve pas, alors on crée une image par défaut
|
Nous allons avoir besoin des images suivantes :
-
Image de find
-
Monstre rouge
-
Monstre bleu
-
Balle
-
Vaisseau spatial
-
Explosion
Nous allons donc appliquer notre logique à toutes ces images afin de pouvoir les appeler de n'importe
où dans le code sans se soucier de leur chargement.
Notre code est donc celui-ci :
| Chargeur de ressources : ApplicationResource |
package com.developpez.java.spaceinvader.app;
import java.io.IOException;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Image;
@author
public class ApplicationResource {
private static Image imgBg;
private static Image imgRedMonster;
private static Image imgBlueMonster;
private static Image imgBullet;
private static Image imgShip;
private static Image imgBoom;
private ApplicationResource() {}
@param
@return
public static Image getImgBg(Canvas canvas) {
if(imgBg == null) {
try {
imgBg = Image.createImage(canvas.getClass()
.getResourceAsStream("/res/img/bg.jpg"));
} catch (IOException ex) {
imgBg = noImg(new int[] {0, 0, 0}, 295);
}
}
return imgBg;
}
@param
@return
public static Image getImgBoom(Canvas canvas) {
if(imgBoom == null) {
try {
imgBoom = Image.createImage(canvas.getClass()
.getResourceAsStream("/res/img/boom.gif"));
} catch (IOException ex) {
imgBoom = noImg(new int[] {255, 255, 255}, 20);
}
}
return imgBoom;
}
@param
@return
public static Image getImgBullet(Canvas canvas) {
if(imgBullet == null) {
try {
imgBullet = Image.createImage(canvas.getClass()
.getResourceAsStream("/res/img/bullet.gif"));
} catch (IOException ex) {
imgBullet = noImg(new int[] {255, 255, 51}, 5);
}
}
return imgBullet;
}
@param
@return
public static Image getImgRedMonster(Canvas canvas) {
if(imgRedMonster == null) {
try {
imgRedMonster = Image.createImage(canvas.getClass()
.getResourceAsStream("/res/img/redMonster.gif"));
} catch (IOException ex) {
imgRedMonster = noImg(new int[] {255, 0, 0}, 10);
}
}
return imgRedMonster;
}
@param
@return
public static Image getImgBlueMonster(Canvas canvas) {
if(imgBlueMonster == null) {
try {
imgBlueMonster = Image.createImage(canvas.getClass()
.getResourceAsStream("/res/img/blueMonster.gif"));
} catch (IOException ex) {
imgBlueMonster = noImg(new int[] {0, 0, 255}, 10);
}
}
return imgBlueMonster;
}
@param
@return
public static Image getImgShip(Canvas canvas) {
if(imgShip == null) {
try {
imgShip = Image.createImage(canvas.getClass()
.getResourceAsStream("/res/img/shipa.gif"));
} catch (IOException ex) {
imgShip = noImg(new int[] {0, 255, 0}, 10);
}
}
return imgShip;
}
@param
@param
@return
private static Image noImg(int[] rgb, int size) {
return Image.createRGBImage(createColor(rgb, size), size, size, false);
}
@param
@param
@return
private static int[] createColor(int[] rgb, int size) {
int[] r = new int[size*size];
int c;
c = 1 << 24;
c += rgb[0] << 16;
c += rgb[1] << 8;
c += rgb[2];
for(int i = 0; i < r.length; ++i) {
r[i] = c;
}
return r;
}
}
|
VI. Nos beans
Afin de pouvoir manipuler nos éléments plus facilement, nous allons utiliser des beans. Chaque bean représente
un objet graphique sur notre futur canvas. Les beans que nous devons écrire sont donc les suivants
:
-
Monstre bleu
-
Monstre rouge
-
Explosion
-
Balle
-
Vaisseau spatial
 |
Afin de pouvoir adopter un comportement plus générique, nous utiliseront une classe abstraite Monster
qui permettra de traiter tous les types de monstre de la même façon.
|
VI-A. Les monstres
Nous allons écrire nos beans représentant les monstres. Afin de ne pas perdre de temps inutilement dans
les explications nous allons utiliser une variété réduite de monstres (uniquement deux), mais nous
pourrions en créer beaucoup plus.
Comme expliqué précédemment, nous allons écrire une classe abstraite Monster qui proposera une signature
et certaines implémentations génériques pour tous nos monstres :
| Un monstre générique : Monster |
package com.developpez.java.spaceinvader.bean;
import com.developpez.java.spaceinvader.app.GameContext;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Image;
@author
public abstract class Monster {
private int x;
private int y;
private Canvas canvas;
@param
public Monster(Canvas canvas) {
this.canvas = canvas;
x = GameContext.getInstance().getRd().nextInt(210);
y = 0;
}
public void move() {
y += getSpeed();
}
@return
public int getX() {
return x;
}
@return
public int getY() {
return y;
}
@return
public Canvas getCanvas() {
return canvas;
}
@return
public abstract int getSpeed();
@return
public abstract Image getImage();
@return
public abstract int getScore();
}
|
Il ne nous reste plus qu'à créer une classe par monstre (bleu et rouge) afin de préciser les caractéristiques
spécifiques (vitesse, image, et score). Ces classes étendent la classe abstraite Monster.
Voici donc à quoi ressemble le code des monstres :
| Un monstre bleu : BlueMonster |
package com.developpez.java.spaceinvader.bean;
import com.developpez.java.spaceinvader.app.ApplicationResource;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Image;
@author
public class BlueMonster extends Monster {
@param
public BlueMonster(Canvas canvas) {
super(canvas);
}
@return
public int getSpeed() {
return 8;
}
@return
public Image getImage() {
return ApplicationResource.getImgBlueMonster(getCanvas());
}
@return
public int getScore() {
return 50;
}
}
|
 |
On ne détaillera pas l'implémentation du monstre rouge, elle est très similaire à celle du monstre bleu
et ne mérite pas d'attention particulière. Dans ma source j'ai appliqué les caractéristiques suivantes
à mon monstre rouge : Vitesse 5, Score 10.
|
VI-B. L'explosion
Lorsqu'une balle atteint un monstre, nous allons le faire exploser. Et cette explosion est un simple
bean, avec sa propre position et sa propre image.
Le code à écrire est tout ce qui est de plus simple :
| Une explosion : Boom |
package com.developpez.java.spaceinvader.bean;
import com.developpez.java.spaceinvader.app.ApplicationResource;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Image;
@author
public class Boom {
private int x;
private int y;
private Canvas canvas;
public Boom(Canvas canvas, int x, int y) {
this.canvas = canvas;
this.x = x;
this.y = y;
}
@return
public int getX() {
return x;
}
@return
public int getY() {
return y;
}
@return
public Image getImage() {
return ApplicationResource.getImgBoom(canvas);
}
}
|
VI-C. Les balles
Comme tous vaisseaux spatiaux de combat, notre vaisseau devra pouvoir tirer des balles (ou plutôt des
rockets, mais peu importe). Il va donc bien falloir représenter ces balles à l'écran. Notre bean
Bullet possèdera donc une position, une image, et devra pouvoir se déplacer.
Une particularité de ce bean est qu'il devra savoir s'il entre en contact avec un monstre ou pas.
Ceci peut paraître difficile à première vue, mais il n'en est rien.
Afin de prendre du recul, analysons un peu mieux ce que nous devrons gérer. Notre application
va afficher un certain nombre de monstres à l'écran, notre vaisseau, et un certain nombre de balles.
Si nous souhaitons savoir si une balle entre en contact avec un monstre, cette dernière devra connaître
l'ensemble des montres présents à l'écran. Nous allons donc créer une méthode isExplosed retournant
un boolean et recevant les monstres à l'écran. Nous parcourrons ces monstres, et si la balle est
en contact avec au moins un monstre, alors on renverra true.
Voici le code :
| Une balle : Bullet |
package com.developpez.java.spaceinvader.bean;
import com.developpez.java.spaceinvader.app.ApplicationResource;
import com.developpez.java.spaceinvader.app.GameContext;
import java.util.Enumeration;
import java.util.Vector;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Image;
@author
public class Bullet {
private int x;
private int y;
private Canvas canvas;
public Bullet(Canvas canvas, int x, int y) {
this.canvas = canvas;
this.x = x;
this.y = y;
}
@return
public int getX() {
return x;
}
@return
public int getY() {
return y;
}
public void move() {
y -= 10;
}
@param
@return
public boolean isExplosed(Vector monsters) {
Enumeration e = monsters.elements();
while(e.hasMoreElements()) {
Object o = e.nextElement();
if(o instanceof Monster) {
Monster monster = (Monster) o;
if(
getX() > monster.getX()-11
&&
getX() < monster.getX()+11
&&
getY() < monster.getY()+20
) {
monsters.removeElement(monster);
GameContext.getInstance().incScore(monster.getScore());
return true;
} else {
return false;
}
}
}
return false;
}
@return
public Image getImage() {
return ApplicationResource.getImgBullet(canvas);
}
}
|
 |
Ici nous utilisons des Vector, mais pourquoi ?
Il est vrai que les Vector ne sont quasiment plus utilisés, et il aurait été plus judicieux
d'utiliser des List<Monster>. Cependant, JME ne fournit pas de generics, nous sommes donc contraints
d'utiliser des Vector pour nos collections.
|
VI-D. Le vaisseau spatial
Il ne nous reste plus qu'à écrire le bean représentant notre vaisseau spatial. Ce dernier ne possède
rien de particulier, il possède une position et peut se déplacer vers la gauche et vers la droite.
Voici le code assez classique :
| Une balle : Bullet |
package com.developpez.java.spaceinvader.bean;
import com.developpez.java.spaceinvader.app.ApplicationResource;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Image;
@author
public class Ship {
private int x;
private int y;
private Canvas canvas;
public Ship(Canvas canvas, int x, int y) {
this.canvas = canvas;
this.x = x;
this.y = y;
}
@return
public int getX() {
return x;
}
@return
public int getY() {
return y;
}
@param
public void moveLeft(int step) {
this.x -= step;
}
@param
public void moveRight(int step) {
this.x += step;
}
@return
public Image getImage() {
return ApplicationResource.getImgShip(canvas);
}
}
|
 |
Nos beans sont enfin terminés, il nous reste le plus intéressant à faire : l'assemblage et les déplacements.
Maintenant que nos structures de données sont faites, il sera très simple de boucler l'application.
|
VI. Les timers
Afin de donner du dynamisme au jeu, nous allons utiliser deux TimerTask :
-
Un premier gérant l'apparition des monstres
-
Un second gérant les déplacements des éléments comme les monstres et les balles
VI-A. Apparition des monstres
Rien de compliqué ici, nous allons simplement ajouter un monstre de type aléatoire (rouge ou bleu) dans
un Vector passé en paramètre.
Ceci donne :
| Apparition des monstres : MonsterAppearTask |
package com.developpez.java.spaceinvader.app;
import com.developpez.java.spaceinvader.bean.BlueMonster;
import com.developpez.java.spaceinvader.bean.RedMonster;
import java.util.TimerTask;
import java.util.Vector;
import javax.microedition.lcdui.Canvas;
@author
public class MonsterAppearTask extends TimerTask {
private Vector monsters;
private Canvas canvas;
@param
@param
public MonsterAppearTask(Vector monsters, Canvas canvas) {
this.canvas = canvas;
this.monsters = monsters;
}
public void run() {
switch(GameContext.getInstance().getRd().nextInt(2)) {
case 0:
monsters.addElement(new BlueMonster(canvas));
break;
case 1:
monsters.addElement(new RedMonster(canvas));
break;
}
}
}
|
VI-B. Déplacement des éléments (monstres et balles)
Bonne nouvelle, ce TimerTask n'est pas plus compliqué que le premier. Nous allons simplement récupérer
deux Vector, un contenant les balles, et un autre les monstres. Nous allons simplement parcourir
ces deux Vector au travers d'Enumeration, et appeler leurs méthodes de déplacement.
Simple particularité pour les balles, il faudra vérifier qu'elles n'entrent pas en contact avec
un monstre.
Le code est le suivant :
| Gestion des déplacements : MoveTask |
package com.developpez.java.spaceinvader.app;
import com.developpez.java.spaceinvader.bean.Boom;
import com.developpez.java.spaceinvader.bean.Bullet;
import com.developpez.java.spaceinvader.bean.Monster;
import com.developpez.java.spaceinvader.low.MainCanvas;
import java.util.Enumeration;
import java.util.TimerTask;
import java.util.Vector;
@author
public class MoveTask extends TimerTask {
private Vector monsters;
private Vector bullets;
private Vector booms;
private MainCanvas canvas;
public MoveTask(MainCanvas canvas, Vector monsters, Vector bullets, Vector booms) {
this.canvas = canvas;
this.monsters = monsters;
this.bullets = bullets;
this.booms = booms;
}
public void run() {
Enumeration eBullets = bullets.elements();
while(eBullets.hasMoreElements()) {
Bullet bullet = (Bullet) eBullets.nextElement();
bullet.move();
if(bullet.getY() < 0) {
bullets.removeElement(bullet);
} else if(bullet.isExplosed(monsters)) {
bullets.removeElement(bullet);
booms.addElement(new Boom(canvas, bullet.getX(), bullet.getY()));
}
}
Enumeration eMonsters = monsters.elements();
while(eMonsters.hasMoreElements()) {
Monster monster = (Monster) eMonsters.nextElement();
monster.move();
if(monster.getY() > 290) {
GameContext.getInstance().kill();
monsters.removeElement(monster);
}
}
System.gc();
canvas.repaint();
}
}
|
VIII. Assemblage
Nous voilà sur la dernière ligne droite : l'assemblage. Il ne reste plus qu'a afficher les éléments, et
lier la pression de certaines touches aux actions de tir et de déplacement.
VIII-A. Dessiner sur l'écran : le Canvas
Nous allons créer un Canvas qui nous permettra d'afficher les élements graphiquement. Il va nous falloir
à présent parcourir toutes nos données pour afficher les différentes images à leur position respective sur
l'écran.
Les éléments à traiter sont les suivants :
-
L'image de fond
-
Notre vaisseau
-
Nos balles
-
Les monstres
-
Une bande en haut pour afficher le score et le nombre de vies
-
Le score
-
Le nombre de vies
-
Les explosions
Rappelons brièvement le fonctionnement d'un Canvas :
Un Canvas possède une méthode paint(Graphics) qui est appelée à chaque fois que l'on appelle la méthode
repaint(). Elle permet de décrire ce qu'on doit dessiner à l'écran. C'est donc dans cette méthode
que nous allons pouvoir dessiner à l'écran.
Pour ce faire, nous allons utiliser l'instance de Graphics passée en argument de la méthode paint(Graphics).
Graphics possède un certain nombre de méthodes permettant de dessiner toutes sortes de choses à l'écran
(cercles, rectangles, image, texte). Nous allons donc, pour chaque élément à afficher, récupérer
son Image, et la dessiner à l'écran à la position correspondante.
Il ne faudra pas non plus oublier de lancer les Timer sur les deux TimersTask (déplacements et
apparitions de monstres).
Ca se complique donc un tout petit peu, mais rien d'insurmontable :
| Affichage des éléments : MainCanvas |
package com.developpez.java.spaceinvader.low;
import com.developpez.java.spaceinvader.app.ApplicationResource;
import com.developpez.java.spaceinvader.app.GameContext;
import com.developpez.java.spaceinvader.app.MonsterAppearTask;
import com.developpez.java.spaceinvader.app.MoveTask;
import com.developpez.java.spaceinvader.bean.Boom;
import com.developpez.java.spaceinvader.bean.Bullet;
import com.developpez.java.spaceinvader.bean.Monster;
import com.developpez.java.spaceinvader.bean.Ship;
import java.util.Enumeration;
import java.util.Timer;
import java.util.Vector;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
@author
public class MainCanvas extends Canvas {
private Ship ship;
private Vector monsters = new Vector();
private Vector bullets = new Vector();
private Vector booms = new Vector();
private Enumeration e;
private Timer timerMove = new Timer();
private Timer timerAppear = new Timer();
private int level;
private boolean controlsEnabled = true;
@param
public MainCanvas(int level) {
this.level = level;
timerMove.schedule(new MoveTask(this, monsters, bullets, booms), 0, 100);
timerAppear.schedule(new MonsterAppearTask(monsters, this), 0, 3000-level*450);
}
protected void paint(Graphics g) {
checkInit(g);
g.drawImage(ApplicationResource.getImgBg(this), 0, 0, Graphics.LEFT | Graphics.TOP);
g.setColor(255, 255, 255);
g.drawImage(ship.getImage(), ship.getX(), ship.getY(), Graphics.HCENTER | Graphics.TOP);
e = bullets.elements();
while(e.hasMoreElements()) {
Bullet bullet = (Bullet) e.nextElement();
g.drawImage(bullet.getImage(), bullet.getX(), bullet.getY(), Graphics.HCENTER | Graphics.BOTTOM);
}
e = monsters.elements();
while(e.hasMoreElements()) {
Monster monster = (Monster) e.nextElement();
g.drawImage(monster.getImage(), monster.getX(), monster.getY(), Graphics.HCENTER | Graphics.VCENTER);
}
g.setColor(0, 0, 0);
g.fillRect(0, 0, g.getClipWidth(), 20);
g.setColor(200, 200, 200);
g.drawString("Score : " + GameContext.getInstance().getScore(), 0, 0, Graphics.TOP | Graphics.LEFT);
if(GameContext.getInstance().getNbLife() < 0) {
controlsEnabled = false;
timerAppear.cancel();
timerMove.cancel();
g.drawString("Game Over", g.getClipWidth(), 0, Graphics.TOP | Graphics.RIGHT);
} else {
g.drawString("Lives : " + GameContext.getInstance().getNbLife(), g.getClipWidth(), 0, Graphics.TOP | Graphics.RIGHT);
}
e = booms.elements();
while(e.hasMoreElements()) {
Boom boom = (Boom) e.nextElement();
g.drawImage(boom.getImage(), boom.getX(), boom.getY(), Graphics.HCENTER | Graphics.VCENTER);
booms.removeElement(boom);
}
}
private void checkInit(Graphics g) {
if(ship == null) {
ship = new Ship(this, g.getClipWidth()/2, g.getClipHeight()-30);
}
}
}
|
VIII-B. Interaction avec le joueur : la pression des touches
La gestion des événements sur un Canvas est un peu particulière puisque, contrairement aux commandes, elle
n'utilise pas de listener. Il nous suffit de surcharger certaines méthodes afin définir du code à
exécuter sur tel ou tel événement.
Nous utiliserons deux événements : keyPressed (appui sur une touche), et keyRepeted (maintien d'une
touche appuyée). Afin d'éviter d'implémenter deux fois la même chose, nous délégueront l'appel à
keyPressed dans keyRepeted.
Voici le code manquant à MainCanvas :
| Affichage des éléments : MainCanvas |
package com.developpez.java.spaceinvader.low;
import com.developpez.java.spaceinvader.app.ApplicationResource;
import com.developpez.java.spaceinvader.app.GameContext;
import com.developpez.java.spaceinvader.app.MonsterAppearTask;
import com.developpez.java.spaceinvader.app.MoveTask;
import com.developpez.java.spaceinvader.bean.Boom;
import com.developpez.java.spaceinvader.bean.Bullet;
import com.developpez.java.spaceinvader.bean.Monster;
import com.developpez.java.spaceinvader.bean.Ship;
import java.util.Enumeration;
import java.util.Timer;
import java.util.Vector;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
@author
public class MainCanvas extends Canvas {
protected void keyPressed(int keyCode) {
if(controlsEnabled) {
switch(keyCode) {
case KEY_NUM4:
ship.moveLeft(10);
break;
case KEY_NUM6:
ship.moveRight(10);
break;
case KEY_NUM5:
bullets.addElement(new Bullet(this, ship.getX(), ship.getY()));
break;
}
repaint();
}
}
protected void keyRepeated(int keyCode) {
keyPressed(keyCode);
}
}
|
Félicitations, le jeu est terminé ! Vous pouvez imaginer toutes sortes d'améliorations afin de le rendre plus
complet et plus intéressant :)
IX. Javadoc, sources, et captures d'écrans
X. Conclusion
S'il n'était pas forcément évident de développer ce petit jeu au début, il s'est avéré qu'avec une organisation
suffisamment efficace du code, il fut plutôt facile de réaliser cette application proprement.
Nous aurions tout-à-fait pu améliorer et rendre plus complet le programme, mais il ne s'agit que
d'un exemple et le but n'était pas d'écrire un jeu diffusable, même s'il faudrait très peu
de temps pour rendre ce jeu suffisamment complet.
Comme dans tout autre projet, le plus dur n'est pas le développement, mais la conception. On ne peut
pas dire que la conception de ce projet soit un véritable casse-tête compte tenu du peu de choses
à prendre en compte, mais il suffisait simplement de le prendre par le bon bout.
XI. Remerciements
Merci à
ced, pour sa relecture.


Copyright © 2009 Alain Defrance. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts.
Cette page est déposée.