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).
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;
/**
* Classe principale
*
@author
Alain Defrance
*/
public
class
SpaceInvader extends
MIDlet {
/**
* Display représentant l'écran.
*/
private
Display display;
/**
* Méthode exécutée au lancement du Midlet
*/
protected
void
startApp
(
) {
// Nous allons coder un peu plus tard la classe MainForm
show
(
new
MainForm
(
this
));
}
/**
* Méthode exécutée à la mise en pause du Midlet
*/
protected
void
pauseApp
(
) {
// Nous ne faisons rien
}
/**
* Méthode exécutée à la destruction du Midlet
*
@param
unconditional
*/
public
void
destroyApp
(
boolean
unconditional) {
// Nous ne faisons rien
}
/**
* Méthode à appeler lorsque l'on veut mettre à jour le composant graphique
* à afficher
*
@param
displayable
Elément à afficher
*/
public
void
show
(
Displayable displayable) {
// On récupère le Display uniquement si on ne l'a pas déjà fait
if
(
display ==
null
) {
display =
Display.getDisplay
(
this
);
}
// Place le nouvel élément sur l'écran
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 :
package
com.developpez.java.spaceinvader.app;
import
java.util.Random;
/**
* Contexte de l'application
*
@author
Alain Defrance
*/
public
class
GameContext {
public
int
score =
0
;
public
int
nbLife =
3
;
/**
* Permet la génération d'entiers aléatoires.
*/
private
Random rd =
new
Random
(
);
/**
* Unique instance de la classe, typique du singleton
*/
public
static
GameContext instance;
/**
* Constructeur privé, typique du singleton
*/
private
GameContext
(
) {
}
/**
* Permet la récupération de l'instance unique, typique du singleton
*
@return
Le contexte
*/
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;
}
/**
* Incrémente le score
*
@param
score
*/
public
void
incScore
(
int
score) {
this
.score +=
score;
}
/**
* Appelé à perte d'une vie
*/
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 :
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;
/**
* Menu principal
*
@author
Alain Defrance
*/
public
class
MainForm extends
Form {
/**
* Référence vers l'instance du Midlet principal
*/
private
SpaceInvader app;
/**
* Selection du niveau
*/
private
Gauge level =
new
Gauge
(
"Difficulté"
,
true
,
5
,
3
);
/**
* Commandes pour sortir du programme
*/
private
Command cmdExit =
new
Command
(
String.valueOf
(
CMD_EXIT),
"Quit"
,
Command.EXIT,
1
);
/**
* Commande pour démarrer le jeu
*/
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);
}
/**
* Initialise le formulaire principal
*
@param
app
Midlet principal
*/
public
MainForm
(
SpaceInvader app) {
super
(
"My Space Invader"
);
this
.app =
app;
// Nous allons coder un peu plus tard la class MainListener
setCommandListener
(
new
MainListener
(
this
));
}
/**
* Retourne le niveau de difficulté, utilisé plus tard dans l'application
*
@return
*/
public
int
getLevel
(
) {
return
level.getValue
(
);
}
/**
* Accesseur retournant le Midlet principal, utilisé plus tard dans
* l'application
*
@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 :
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;
/**
* Listener du formulaire principal
*
@author
Alain Defrance
*/
public
class
MainListener implements
CommandListener {
/**
* Référence vers l'instance de mon formulaire principal
*/
private
MainForm form;
/**
* Initialisation de l'instance
*
@param
form
Formulaire principal
*/
public
MainListener
(
MainForm form) {
this
.form =
form;
}
/**
* Implémentation de la méthode appelée à un événement d'une commande
*
@param
c
Commande ayant déclenché l'appel
*
@param
d
Displayable contenant la commande qui a déclenché l'appel
*/
public
void
commandAction
(
Command c, Displayable d) {
// On traite toutes les commandes
switch
(
Integer.parseInt
(
c.getLabel
(
))) {
case
MainForm.CMD_START:
// On démarre une partie
form.getApp
(
).show
(
new
MainCanvas
(
form.getLevel
(
)+
1
));
break
;
case
MainForm.CMD_EXIT:
// On détruit le Midelet
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 :
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 :
package
com.developpez.java.spaceinvader.app;
import
java.io.IOException;
import
javax.microedition.lcdui.Canvas;
import
javax.microedition.lcdui.Image;
/**
* Possède la responsabilité du chargement des images
*
@author
Alain Defrance
*/
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;
/**
* Constructeur privé car nous ne souhaitons pas rendre cette classe
* instanciable
*/
private
ApplicationResource
(
) {}
/**
* Charge l'image de fond
*
@param
canvas
Canvas utilisé pour le chargement de l'image
*
@return
Image de fond
*/
public
static
Image getImgBg
(
Canvas canvas) {
if
(
imgBg ==
null
) {
try
{
imgBg =
Image.createImage
(
canvas.getClass
(
)
.getResourceAsStream
(
"/res/img/bg.jpg"
));
}
catch
(
IOException ex) {
// Par défaut un carré de 295*295 pixels de couleur noire
imgBg =
noImg
(
new
int
[] {
0
, 0
, 0
}
, 295
);
}
}
return
imgBg;
}
/**
* Charge l'image de l'explosion
*
@param
canvas
Canvas utilisé pour le chargement de l'image
*
@return
Image de l'explosion
*/
public
static
Image getImgBoom
(
Canvas canvas) {
if
(
imgBoom ==
null
) {
try
{
imgBoom =
Image.createImage
(
canvas.getClass
(
)
.getResourceAsStream
(
"/res/img/boom.gif"
));
}
catch
(
IOException ex) {
// Par défaut un carré de 20*20 pixels de couleur blanche
imgBoom =
noImg
(
new
int
[] {
255
, 255
, 255
}
, 20
);
}
}
return
imgBoom;
}
/**
* Charge l'image d'une balle
*
@param
canvas
Canvas utilisé pour le chargement de l'image
*
@return
Image d'une balle
*/
public
static
Image getImgBullet
(
Canvas canvas) {
if
(
imgBullet ==
null
) {
try
{
imgBullet =
Image.createImage
(
canvas.getClass
(
)
.getResourceAsStream
(
"/res/img/bullet.gif"
));
}
catch
(
IOException ex) {
// Par défaut un carré de 5*5 pixels de couleur jaune
imgBullet =
noImg
(
new
int
[] {
255
, 255
, 51
}
, 5
);
}
}
return
imgBullet;
}
/**
* Charge l'image d'un monstre rouge
*
@param
canvas
Canvas utilisé pour le chargement de l'image
*
@return
Image d'un monstre rouge
*/
public
static
Image getImgRedMonster
(
Canvas canvas) {
if
(
imgRedMonster ==
null
) {
try
{
imgRedMonster =
Image.createImage
(
canvas.getClass
(
)
.getResourceAsStream
(
"/res/img/redMonster.gif"
));
}
catch
(
IOException ex) {
// Par défaut un carré de 10*10 pixels de couleur rouge
imgRedMonster =
noImg
(
new
int
[] {
255
, 0
, 0
}
, 10
);
}
}
return
imgRedMonster;
}
/**
* Charge l'image d'un monstre bleu
*
@param
canvas
Canvas utilisé pour le chargement de l'image
*
@return
Image d'un monstre bleu
*/
public
static
Image getImgBlueMonster
(
Canvas canvas) {
if
(
imgBlueMonster ==
null
) {
try
{
imgBlueMonster =
Image.createImage
(
canvas.getClass
(
)
.getResourceAsStream
(
"/res/img/blueMonster.gif"
));
}
catch
(
IOException ex) {
// Par défaut un carré de 10*10 pixels de couleur bleu
imgBlueMonster =
noImg
(
new
int
[] {
0
, 0
, 255
}
, 10
);
}
}
return
imgBlueMonster;
}
/**
* Charge l'image du vaisseau du joueur
*
@param
canvas
Canvas utilisé pour le chargement de l'image
*
@return
Image du vaisseau du joueur
*/
public
static
Image getImgShip
(
Canvas canvas) {
if
(
imgShip ==
null
) {
try
{
imgShip =
Image.createImage
(
canvas.getClass
(
)
.getResourceAsStream
(
"/res/img/shipa.gif"
));
}
catch
(
IOException ex) {
// Par défaut un carré de 10*10 pixels de couleur verte
imgShip =
noImg
(
new
int
[] {
0
, 255
, 0
}
, 10
);
}
}
return
imgShip;
}
/**
* Permet de créer une image quand le chargement s'avère impossible
* (probablement dû à l'abscence de l'image)
*
@param
rgb
Tableau d'entiers de taille 3 contenant respectivement les taux
* des couleurs rouge, vert, et bleu compris entre 0 et 255
*
@param
size
Taille du carré
*
@return
Image créee
*/
private
static
Image noImg
(
int
[] rgb, int
size) {
return
Image.createRGBImage
(
createColor
(
rgb, size), size, size, false
);
}
/**
* Permet de générer le tableau contenant les taux des couleurs pour tous
* les pixels
*
@param
rgb
Tableau d'entiers de taille 3 contenant respectivement les taux
* des couleurs rouge, vert, et bleu compris entre 0 et 255
*
@param
size
Taille du carré
*
@return
Tableau des taux de couleurs pour chaques pixels
*/
private
static
int
[] createColor
(
int
[] rgb, int
size) {
int
[] r =
new
int
[size*
size];
int
c;
c =
1
<<
24
; // Alpha
c +=
rgb[0
] <<
16
; // Rouge
c +=
rgb[1
] <<
8
; // Vert
c +=
rgb[2
]; // Bleu
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 :
package
com.developpez.java.spaceinvader.bean;
import
com.developpez.java.spaceinvader.app.GameContext;
import
javax.microedition.lcdui.Canvas;
import
javax.microedition.lcdui.Image;
/**
* Signature d'un monstre
*
@author
Alain Defrance
*/
public
abstract
class
Monster {
/**
* Position en abscisse
*/
private
int
x;
/**
* Position en ordonnée
*/
private
int
y;
/**
* Canvas sur lequel le monstre sera affiché
*/
private
Canvas canvas;
/**
* Initialisation du monstre
*
@param
canvas
Canvas sur lequel le monstre sera affiché
*/
public
Monster
(
Canvas canvas) {
this
.canvas =
canvas;
// Initialise la position horizontale du monstre aléatoirement
x =
GameContext.getInstance
(
).getRd
(
).nextInt
(
210
);
y =
0
;
}
/**
* Déplace le monstre vers le bas
*/
public
void
move
(
) {
y +=
getSpeed
(
);
}
/**
*
@return
Position en abscisse
*/
public
int
getX
(
) {
return
x;
}
/**
*
@return
Position en ordonnée
*/
public
int
getY
(
) {
return
y;
}
/**
*
@return
Canvas sur lequel afficher le monstre
*/
public
Canvas getCanvas
(
) {
return
canvas;
}
/**
*
@return
Vitesse du monstre
*/
public
abstract
int
getSpeed
(
);
/**
*
@return
Image du monstre
*/
public
abstract
Image getImage
(
);
/**
*
@return
Score apporté par mort du monstre
*/
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 :
package
com.developpez.java.spaceinvader.bean;
import
com.developpez.java.spaceinvader.app.ApplicationResource;
import
javax.microedition.lcdui.Canvas;
import
javax.microedition.lcdui.Image;
/**
* Un monstre bleu
*
@author
Alain Defrance
*/
public
class
BlueMonster extends
Monster {
/**
* Initialisation du monstre
*
@param
canvas
Canvas sur lequel le monstre sera affiché
*/
public
BlueMonster
(
Canvas canvas) {
super
(
canvas);
}
/**
*
@return
Vitesse spécifique aux monstres bleus
*/
public
int
getSpeed
(
) {
return
8
;
}
/**
*
@return
Image spécifique aux monstres bleus
*/
public
Image getImage
(
) {
return
ApplicationResource.getImgBlueMonster
(
getCanvas
(
));
}
/**
*
@return
Score spécifique aux monstres bleus
*/
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 :
package
com.developpez.java.spaceinvader.bean;
import
com.developpez.java.spaceinvader.app.ApplicationResource;
import
javax.microedition.lcdui.Canvas;
import
javax.microedition.lcdui.Image;
/**
* Explosion
*
@author
Alain Defrance
*/
public
class
Boom {
/**
* Position en abscisse
*/
private
int
x;
/**
* Position en ordonnée
*/
private
int
y;
/**
* Canvas sur lequel l'explosion sera affiché
*/
private
Canvas canvas;
public
Boom
(
Canvas canvas, int
x, int
y) {
this
.canvas =
canvas;
this
.x =
x;
this
.y =
y;
}
/**
*
@return
Position en abscisse
*/
public
int
getX
(
) {
return
x;
}
/**
*
@return
Position en ordonnée
*/
public
int
getY
(
) {
return
y;
}
/**
*
@return
Image de l'explosion
*/
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 :
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;
/**
* Balle du vaisseau spatial
*
@author
Alain Defrance
*/
public
class
Bullet {
/**
* Position en abscice
*/
private
int
x;
/**
* Position en ordonnée
*/
private
int
y;
/**
* Canvas sur lequel la balle sera affichée
*/
private
Canvas canvas;
public
Bullet
(
Canvas canvas, int
x, int
y) {
this
.canvas =
canvas;
this
.x =
x;
this
.y =
y;
}
/**
*
@return
Position en abscisse
*/
public
int
getX
(
) {
return
x;
}
/**
*
@return
Position en ordonnée
*/
public
int
getY
(
) {
return
y;
}
/**
* Déplace la balle
*/
public
void
move
(
) {
y -=
10
;
}
/**
* Parcourt tous les monstres et détecte si la balle est en contact avec un
* monstre
*
@param
monsters
Monstres présents à l'écran
*
@return
True si la balle est en contact avec un monstre, sinon False
*/
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
) {
// Si la balle entre en contact avec un monstre, elle le
// supprime et augmente le score avec la valeur du monstre
monsters.removeElement
(
monster);
GameContext.getInstance
(
).incScore
(
monster.getScore
(
));
return
true
;
}
else
{
return
false
;
}
}
}
return
false
;
}
/**
*
@return
Image de la balle
*/
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 :
package
com.developpez.java.spaceinvader.bean;
import
com.developpez.java.spaceinvader.app.ApplicationResource;
import
javax.microedition.lcdui.Canvas;
import
javax.microedition.lcdui.Image;
/**
* Vaisseau spatial
*
@author
Alain Defrance
*/
public
class
Ship {
/**
* Position en abscisse
*/
private
int
x;
/**
* Position en ordonnées
*/
private
int
y;
/**
* Canvas sur lequel le vaisseau sera affichée
*/
private
Canvas canvas;
public
Ship
(
Canvas canvas, int
x, int
y) {
this
.canvas =
canvas;
this
.x =
x;
this
.y =
y;
}
/**
*
@return
Position en abscisse
*/
public
int
getX
(
) {
return
x;
}
/**
*
@return
Position en ordonnée
*/
public
int
getY
(
) {
return
y;
}
/**
* Déplacement du vaisseau vers la gauche
*
@param
step
Vitesse de déplacement
*/
public
void
moveLeft
(
int
step) {
this
.x -=
step;
}
/**
* Déplacement de vaisseau vers la droite
*
@param
step
Vitesse de déplacement
*/
public
void
moveRight
(
int
step) {
this
.x +=
step;
}
/**
*
@return
Image du vaisseau
*/
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.
VII. 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
VII-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 :
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;
/**
* Apparition des monstres
*
@author
Alain Defrance
*/
public
class
MonsterAppearTask extends
TimerTask {
private
Vector monsters;
private
Canvas canvas;
/**
* Initialisation du TimerTask
*
@param
monsters
Vector de monstres dans lequel on va empiler les
* monstres
*
@param
canvas
Canvas sur lequel on va afficher les monstres par la suite
*/
public
MonsterAppearTask
(
Vector monsters, Canvas canvas) {
this
.canvas =
canvas;
this
.monsters =
monsters;
}
public
void
run
(
) {
// Génération aléatoire entre deux types de monstres afin d'avoir
// plusieurs variétés de monstres
switch
(
GameContext.getInstance
(
).getRd
(
).nextInt
(
2
)) {
case
0
:
monsters.addElement
(
new
BlueMonster
(
canvas));
break
;
case
1
:
monsters.addElement
(
new
RedMonster
(
canvas));
break
;
}
}
}
VII-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 :
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;
/**
* Gestion des déplacements
*
@author
Alain Defrance
*/
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
(
) {
// On parcourt les balles
Enumeration eBullets =
bullets.elements
(
);
while
(
eBullets.hasMoreElements
(
)) {
Bullet bullet =
(
Bullet) eBullets.nextElement
(
);
// On les fait avancer
bullet.move
(
);
// Si elles sortent de l'écran alors on les supprime
if
(
bullet.getY
(
) <
0
) {
bullets.removeElement
(
bullet);
// Si elles entrent en contact avec un monstre, on crée une explosion
// et on les supprime
}
else
if
(
bullet.isExplosed
(
monsters)) {
bullets.removeElement
(
bullet);
booms.addElement
(
new
Boom
(
canvas, bullet.getX
(
), bullet.getY
(
)));
}
}
// On parcourt les monstres
Enumeration eMonsters =
monsters.elements
(
);
while
(
eMonsters.hasMoreElements
(
)) {
Monster monster =
(
Monster) eMonsters.nextElement
(
);
monster.move
(
);
// Si ils sortent de l'écran alors le joueur perd une vie, et on
// supprime le monstre
if
(
monster.getY
(
) >
290
) {
GameContext.getInstance
(
).kill
(
);
monsters.removeElement
(
monster);
}
}
/**
* On demande au garbage collector de passer pour liberer la mémoire
* prise par les balles et monstres détruits
*/
System.gc
(
);
/**
* On rafraîchit l'affichage
*/
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 :
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;
/**
* Gestion de l'affichage
*
@author
Alain Defrance
*/
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
;
/**
* A la construction on lance les timers
*
@param
level
*/
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) {
// Création du vaisseau spatial
checkInit
(
g);
// Background
g.drawImage
(
ApplicationResource.getImgBg
(
this
), 0
, 0
, Graphics.LEFT |
Graphics.TOP);
// Vaisseau spatial
g.setColor
(
255
, 255
, 255
);
g.drawImage
(
ship.getImage
(
), ship.getX
(
), ship.getY
(
), Graphics.HCENTER |
Graphics.TOP);
// Parcours des balles pour les afficher
e =
bullets.elements
(
);
while
(
e.hasMoreElements
(
)) {
Bullet bullet =
(
Bullet) e.nextElement
(
);
g.drawImage
(
bullet.getImage
(
), bullet.getX
(
), bullet.getY
(
), Graphics.HCENTER |
Graphics.BOTTOM);
}
// Parcours des monstres pour les afficher
e =
monsters.elements
(
);
while
(
e.hasMoreElements
(
)) {
Monster monster =
(
Monster) e.nextElement
(
);
g.drawImage
(
monster.getImage
(
), monster.getX
(
), monster.getY
(
), Graphics.HCENTER |
Graphics.VCENTER);
}
// Bande pour le score et les vies
g.setColor
(
0
, 0
, 0
);
g.fillRect
(
0
, 0
, g.getClipWidth
(
), 20
);
// Score
g.setColor
(
200
, 200
, 200
);
g.drawString
(
"Score : "
+
GameContext.getInstance
(
).getScore
(
), 0
, 0
, Graphics.TOP |
Graphics.LEFT);
// Lorsque l'on a plus de vies on stoppe les timers, et on désactive les controles
if
(
GameContext.getInstance
(
).getNbLife
(
) <
0
) {
controlsEnabled =
false
;
timerAppear.cancel
(
);
timerMove.cancel
(
);
g.drawString
(
"Game Over"
, g.getClipWidth
(
), 0
, Graphics.TOP |
Graphics.RIGHT);
}
else
{
// Sinon on affiche le nombre de vies
g.drawString
(
"Lives : "
+
GameContext.getInstance
(
).getNbLife
(
), g.getClipWidth
(
), 0
, Graphics.TOP |
Graphics.RIGHT);
}
// On parcourt les explosions pour les afficher, puis on les supprime
// afin qu'elles ne persistent pas dans le temps.
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
);
}
}
// ...
// Gestion des événements vus juste après
// ...
}
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 :
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;
/**
* Gestion de l'affichage
*
@author
Alain Defrance
*/
public
class
MainCanvas extends
Canvas {
//...
// Code précédemment décrit gérant l'affichage des différents éléments.
//...
protected
void
keyPressed
(
int
keyCode) {
if
(
controlsEnabled) {
switch
(
keyCode) {
// A la pression de la touche 4 on se déplace de 10 pixels vers
// la gauche
case
KEY_NUM4:
ship.moveLeft
(
10
);
break
;
// A la pression de la touche 6 on se déplace de 10 pixels vers
// la droite
case
KEY_NUM6:
ship.moveRight
(
10
);
break
;
// A la pression de la touche 5, on ajoute une balle dans le
// Vector à la position actuelle du vaisseau spatial
case
KEY_NUM5:
bullets.addElement
(
new
Bullet
(
this
, ship.getX
(
), ship.getY
(
)));
break
;
}
repaint
(
);
}
}
// Lorsque l'on maintien la touche appuyée, on fait la même chose
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▲
- La Javadoc
- Les sources (projet NetBeans)
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.