Programmer un jeu avec MIDlet Pascal

darrylsite

Dans les deux premiers tutoriels sur MIDlet Pascal, nous avons appris à utiliser MIDlet Pascal pour développer des applications pour téléphones portables. Dans ce tutoriel, nous allons concevoir et programmer un jeu avec cet outil : un des classiques des casse-têtes connu sous le nom de Mario Sokoban. Nous travaillerons uniquement en mode graphique et n'utiliserons pas de formulaire pour cette application. Commentez

- Titre : Programmer un jeu avec MIDlet Pascal

- Auteur : darrylsite

- Parution : 11 mars 2010

- Mise à jour : 11 mars 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Kpizingui Darryl. 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.

Sommaire

- I. Introduction

- II. Prérequis et configuration du projet

- II-A. Prérequis

- II-B. Création et configuration du projet

- III. Le jeu

- III-A. Les données du programme

- III-B. La vue du programme

- III-C. Le moteur du jeu

- III-D. Autres unités et assemblage du programme

- III-D-1. Autre Unité

- III-D-2. Assemblage du programme

- IV. Les Menus

- IV-A. Le système de menu

- IV-B. Assemblage du programme

- V. Conclusion et remerciements

- V-A. Conclusion

- V-B. Remerciements

I - Introduction

MIDlet Pascal est un compilateur et un environnement de développement intégré utilisant le langage Pascal pour produire des applications Java pour téléphones portables. Si vous n'avez pas encore utilisé MIDlet Pascal, je vous recommande de consulter les tutoriels suivants pour trouver toute l'aide nécessaire pour débuter avec MIDlet Pascal :

- Introduction au MIDlet Pascal 2.02

- Programmation avancée avec MIDlet Pascal

Nous nous proposerons dans ce tutoriel de programmer un jeu avec MIDlet Pascal : Mario Sokoban. Mario Sokoban est un jeu de réflexion, et est assez simple à réaliser. Ce programme nous permettra de passer en revue les principales caractéristiques de cet outil, et nous verrons qu'on peut faire des programmes assez élaborés avec lui. Nous nous efforcerons de faire un programme assez complet et fonctionnel tout en essayant d'éviter de nous mélanger les pinceaux.

Je supposerai que vous avez déjà acquis les bases de la programmation en langage Pascal. Puisque MIDlet Pascal utilise le langage Pascal, nous insisterons seulement sur les nouveautés apportées par celui-ci.

Voici quelques images du projet que nous aurons à réaliser au cours de ce tutoriel.

II - Prérequis et configuration du projet

II-A - Prérequis

Avant de nous lancer dans des discussions et programmes interminables, nous allons nous attarder sur quelques notions nouvelles par rapport au Pascal standard, qui nous seront bien utiles pour comprendre les programmes que nous aurons à faire.

- Le mode de passage en paramètre des types primitifs (Integer, Real, Boolean) se fait obligatoirement par valeur. Le mot clé var n'est pas admis dans la version 2.02. Par contre, les types structurés (Record, Array, Image) se transmettent par adresse toujours sans le mot clé var. L'explication est que les types structurés sont représentés dans le code Java par des classes qui représentent en fait des pointeurs vers les objets qu'elles représentent.

- L'affectation des Record ou des Array est à utiliser en connaissance de cause. Quand on fait l'affectation a :=b, a et b représentent après l'affectation les même objets.

- L'utilisation des types ou variables déclarés dans la partie interface d'une unité dans une autre se fait en précédant son identificateur du nom de l'unité suivie d'un point. Cette notation peut aussi être utilisée avec les fonctions et les procédures, et c'est ce que nous allons utiliser dans tous les codes que nous allons écrire. Cette notation est particulièrement intéressante puisqu'elle permet de savoir à quel unité appartient le type ou la fonction que l'on utilise.

- Il existe aussi quelques limites imposées par MIDlet Pascal quant au support du langage Pascal telles que : type énuméré et structure case-of non pris en compte.

II-B - Création et configuration du projet

Nous utiliserons tout au long de ce tutoriel la version 2.02 de MIDlet Pascal.

Démarrez MIDlet Pascal et créez un nouveau projet nommé Sokoban. Dans le panneau propriété, sélectionnez MIDlet Type, et mettez ce champ à FullScreen pour nous permettre de pouvoir utiliser la totalité de l'écran du téléphone. Pour la configuration d'un émulateur, reporter vous au tutoriel : Introduction au MIDlet Pascal 2.02

Je vous invite maintenant à télécharger et importer les fichiers suivants dans le projet. Ces fichiers sont les images et les grilles de Mario Sokoban que nous allons utiliser dans le projet. Ces grilles contiennent la position des différents objets dans le labyrinthe.

Les images : images.zip
Les grilles : grilles.zip

Vous pouvez télécharger le fichier ci-après pour être sûr que nous disposions du même projet de départ : sokoban.zip

III - Le jeu

Le principe du jeu est bien simple, diriger Mario afin qu'il range des caisses à des emplacements bien précis dans un entrepôt. Nous utiliserons plus tard le terme emplacement cible pour décrire ces emplacements. Bien que simple comme principe, ce jeu peut vraiment donner des maux de tête pour qui veut résoudre des grilles difficiles.

Dans cette section, nous allons concevoir et programmer les méthodes qui nous permettront de voir le jeu sur l'écran de l'émulateur et de rendre le jeu fonctionnel. Vu que le jeu peut facilement être très dur à résoudre, nous fixerons un temps maximal pour la résolution d'une grille, au bout duquel la partie sera considérée comme perdue.

Nous allons utiliser une approche qui consiste à séparer le travail à faire en trois parties. Et chaque partie sera contenue dans une unité.

- La première contiendra les données du programme : déclaration globale de types, de variables ainsi que des constantes.

- La deuxième concerne tout ce qui touche à l'affichage du jeu sur l'écran.

- La dernière contiendra le moteur du jeu. MIDlet Pascal impose qu'on utilise d'autres unités que dans la partie Implementation d'une unité. Ces contraintes nous obligeront à décrire la boucle principale du jeu dans cette unité afin de pouvoir utiliser les méthodes que nous allons définir.

III-A - Les données du programme

Nous avons précédemment convenu que nous assimilerions l'entrepôt à un labyrinthe, que nous allons représenter par une matrice. Chaque élément de la matrice contiendra un nombre qui représente le type d'élément qui se trouve à cet emplacement dans le labyrinthe. Les différents éléments que pourra contenir le labyrinthe sont : Mario, une caisse, une cible, une caisse bien placée, un mur ou le vide. En Pascal standard, nous aurions pu utiliser un type énuméré pour représenter les différents éléments de la grille (la matrice). Nous allons utiliser une astuce qui consiste à garder ces différentes valeurs dans les champs d'un type Record. Nous utiliserons aussi cette même approche pour représenter les différents sens de déplacements possible de Mario dans la grille.

Pour des raisons de performance, nous conserverons la position de Mario dans une variable globale et non dans la grille.

Voici le code de cette unité :

Header.mpsrc

unit header;

Interface

const

TailleGrilleX = 18; // nombre de colonne de la grille
TailleGrilleY = 18;    // nombre de ligne de la grille

type

TGrille = array[1..TailleGrilleX, 1..TailleGrilleY] of integer; // le labyrinthe

TSens = record // sens de deplacement de mario
droite : integer;
gauche : integer;
haut   : integer;
bas    : integer;
end;

TEntite = record //les entites se trouvant dans le labyrinthe
vide     : integer;
mario    : integer;
mur      : integer;
cible    : integer;
caisse   : integer;
okCaisse : integer;
end;

TPosition = record
x : integer;
y : integer;
end;

var
sSense          : TSens;
sEntite         : TEntite;
grille          : TGrille;
marioPosition   : TPosition; // la position de mario dans le labyrinthe

procedure setMarioPosition(x, y : integer);

Implementation

procedure setMarioPosition(x, y : integer);
begin
marioPosition.x := x;
marioPosition.y := y;
end;

initialization

SSense.droite := 0;
SSense.gauche := 1;
SSense.haut   := 2;
SSense.bas    := 3;

SEntite.vide     := 0;
SEntite.mario    := 1;
SEntite.mur      := 2;
SEntite.cible    := 3;
SEntite.caisse   := 5;
SEntite.okCaisse := 6;

end.

III-B - La vue du programme

En séparant le programme en trois parties : données - vue - traitement, on peut se focaliser sur une partie sans connaître le détail des autres. Ainsi, la vue de notre jeu va uniquement concerner l'affichage du labyrinthe avec ses différents éléments : Mario, caisse, ... On affichera le Timer qui permet de limiter le temps de résolution.

Les données du labyrinthe se trouvent dans la variable globale header.Grille. Chaque élément de cette grille est de type Integer dont la valeur est identique à un champ de la variable header.sEntite. Souvenez-vous que la position de Mario est définie dans la variable header.MarioPos. Il sera donc affiché après l'affichage de la grille.

L'affichage du labyrinthe va se faire de la façon suivante : on parcourt la grille, pour un élément donné on détermine l'entité se trouvant à cette position puis on affiche l'image de cette entité.

if(header.grille[i, j]=sEntite.mur) then
drawImage(imgMur, x+debutX, y+debutY)
else if (header.grille[i, j]=sEntite.caisse) then
drawImage(imgCaisse,  x+debutX, y+debutY)
else if (header.grille[i, j]=sEntite.OKCaisse) then
drawImage(imgOKCaisse,  x+debutX, y+debutY)
else if (header.grille[i, j]=sEntite.cible) then
drawImage(imgCible,  x+debutX, y+debutY);

Pour le Timer, on affichera l'image centrée d'une horloge suivie du temps écoulé sous la forme mm :ss.

Nous avons vu dans les précédents tutoriels que l'affichage à l'écran n'est mis à jour qu'après un appel de la méthode Repaint.

Voici le code de l'unité :

affichage.mpsrc

unit affichage;

interface

procedure afficheGrille;
procedure afficheTimer;

var
imgMario, imgCible, imgCaisse, imgOKCaisse, imgMur : Image;

implementation

uses header, misc;

const
largZone = 14; // largeur de l'image d'une entité
debutX   = 5; //position du coin superieur gauche
debutY   = 30; // du debut du labyrinthe

type
TPosition = header.TPosition;

var
SSense    : header.TSens;
SEntite   : header.TEntite ;
imgRetour : Image;

procedure setBackGround;
begin
setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);
end;

procedure afficheGrille;
var
i, j, x, y : integer;
img        : Image;
begin
setBackGround;
for i:= 1 to header.TailleGrilleX do
for j:=1  to header.TailleGrilleY do
begin
x := (i-1)*largZone; // position du coin superieur droit
y := (j-1)*largZone; // de l'element sur l'ecran
//pas structure 'case of'
if(header.grille[i, j]=sEntite.mur) then
drawImage(imgMur, x+debutX, y+debutY)
else if (header.grille[i, j]=sEntite.caisse) then
drawImage(imgCaisse,  x+debutX, y+debutY)
else if (header.grille[i, j]=sEntite.OKCaisse) then
drawImage(imgOKCaisse,  x+debutX, y+debutY)
else if (header.grille[i, j]=sEntite.cible) then
drawImage(imgCible,  x+debutX, y+debutY);
end;
// affichage de Mario sur l'ecran
x := (header.marioPosition.x-1)*largZone;
y := (header.marioPosition.y-1)*largZone;
drawImage(imgMario,  x+debutX, y+debutY) ;
//retour
drawImage(imgRetour,  (getWidth div 2)-8, (GetHeight)-20) ;
afficheTimer;
//repaint;
end;

procedure centrerClock(ch : String; y : integer);
var x : integer;
begin
x:=(getWidth div 2) - (getStringWidth(ch) div 2);
setColor(0, 0, 55);
fillRect(x-5,y-5, getStringWidth(ch)+10, getStringHeight('T')+10);
setColor(0,255, 0);
drawImage(loadImage('/clock.png'), x-20, y);
drawText(ch, x, y);
end;

procedure afficheTimer;
var
m, s   : integer;
sm, ss : string;
begin
s := misc.tempsMax - misc.getTimeElapsed;
m := s div 60;
s := s-m*60;
sm := ''+m;
if (m<10) then
sm := '0'+sm;
ss :=''+s;
if (s<10) then
ss := '0'+ss;
centrerClock(''+sm+':'+ss, 5);
repaint;
end;

initialization

imgMario    := loadImage('/mario.png');
imgCible    := loadImage('/10118.png');
imgcaisse   := loadimage('/caisse.png');
imgOKCaisse := loadImage('/okcaisse.png');
imgMur      := loadImage('/mur.png');

sSense  := header.sSense;
sEntite := header.sEntite;

imgRetour := loadImage('/return.png');
end.

III-C - Le moteur du jeu

Nous allons maintenant rendre le jeu jouable et interactif en développant les règles du jeu, en gérant les collisions et en développant la boucle principale du jeu.

Penchons-nous tout d'abord sur les règles du jeu :

- Mario doit se déplacer dans le labyrinthe, et pousser des caisses

- Mario ne peut pousser une caisse que si l'espace après elle est vide ou est un emplacement cible

- Mario ne peut pas passer à travers un mur ou franchir les limites du labyrinthe

- Le jeu est gagné si toutes les caisses se trouvent sur les objectifs

- Nous ajoutons que la partie est perdue si le joueur décide d'abandonner la partie ou si le temps maximal de résolution est épuisé

Les règles ne sont pas très compliquées comme vous pouvez le voir. Mais nous allons être prudents pour ne pas oublier de détails.

- Avant de pousser une caisse, on doit considérer deux choses; si la caisse est déjà sur un emplacement cible (une caisse bien placée), nous devons indiquer dans la grille que l'emplacement est un emplacement cible après avoir poussé la caisse. Si la position que doit occuper la caisse après le déplacement est un emplacement cible, nous devons indiquer dans la grille que cet emplacement contient une caisse bien placée

- Avant de chercher à déplacer Mario, nous devons vérifier si le nouvel emplacement est vide ou est un emplacement cible. Un autre cas possible sera que cet emplacement contient une caisse que Mario peut pousser

La boucle principale du jeu se chargera de contrôler la pression des touches du clavier. Selon la touche appuyée, on décidera s'il faut chercher à déplacer Mario selon la direction voulue ou abandonner la partie. On vérifiera à chaque fois si le temps limite n'est pas épuisé ou si la partie n'a pas encore été gagnée.

Voici le code résumant tous ces discours :

jeu.mpsrc

unit jeu;

Interface

var
statutJeu : record //Variable representant l'issue du jeu
perdu   : integer;
abandon : integer;
erreur  : integer;
gagne   : integer;
end;

function jouer : integer;

Implementation

uses header, affichage, misc;

type
TPosition = header.TPosition;

{**
* indique si la translation du point de position 'pos' d'une unite dans le sens
* 'sens' amera ce point hors des limites du labyrinthe
* @param pos : position initiale
* @param sens : sens du deplacement
* @return true si le deplacement du point amenera celui-ci hors du labyrinthe, false sinon
**}

function depaceFrontiere(pos : TPosition; sens : integer) : boolean;
var
p : header.TPosition;
begin
if ((p.y=1) and (sens=header.sSense.haut)) or
((p.y=header.TailleGrilleY) and (sens=header.sSense.bas))
or ((p.x=header.TailleGrilleX) and (sens=header.sSense.droite))
or ((p.x=1) and (sens=header.sSense.gauche)) then
depaceFrontiere := true;
else
depaceFrontiere:= false
end;

{**
* determinela nouvelle position d'un point apres la translation de celui-ci d'une unite dans un
* sens donne
* @param pos : position initiale du point
* @param sens : sens du deplacement
* @return la nouvelle position
**}

function deplacer(pos : TPosition; sens : integer) : TPosition;
var
p : header.TPosition;
begin
//ne jamais faire l'affectation des records sans connaissance de cause
p.x := pos.x;
p.y := pos.y;
//la structure de controle 'case of' n'est pas disponible sous MIDlet Pascal
if (sens=header.sSense.haut) then
p.y := p.y-1
else if (sens=header.sSense.bas) then
begin
p.y := p.y+1;
end
else if (sens=header.sSense.droite) then
p.x := p.x+1
else
p.x := p.x-1;

deplacer := p;
end;

{**
* indique si une partie est complete en verifiant qu'il ne reste plus aucun
* emplacement vide ou devrait se trouver une caisse
* @return true si la partie est gagne, false sinon
**}
function estFini : boolean;
var
i, j   : integer;
trouve : boolean;

begin
trouve := false;
for i:=1 to header.TailleGrilleX do
for j:=1 to header.TailleGrilleY do
if(header.grille[i, j]= sEntite.cible) then
trouve := true;

estFini:= not trouve;
end;

{**
* deplace une caisse si possible
* @param pos : position initiale de la caisse
* @require : une caisse doit se situe a la position initaile
* @param sens : sens du deplacement
* @return true si la caisse a ete deplacee, false sinon
**}
function deplaceCaisse(pos : TPosition; sens : integer) : boolean;
var
deplace : boolean;
p : header.TPosition;
begin
deplace := false;
if (not depaceFrontiere(pos, sens)) then
begin
p := deplacer(pos, sens);

if (header.grille[p.x, p.y] = sEntite.vide) or (header.grille[p.x, p.y]=sEntite.cible) then
begin
if (header.grille[pos.x, pos.y] = sEntite.OKCaisse) then
header.grille[pos.x, pos.y] := sEntite.cible
else
header.grille[pos.x, pos.y] := sEntite.vide;

if (header.grille[p.x, p.y] = sEntite.cible) then
header.grille[p.x, p.y] := sEntite.OKCaisse
else
header.grille[p.x, p.y] := sEntite.caisse;

deplace := true;
end;
end;

deplaceCaisse := deplace;
end;

{**
* Deplace Marion d'une position autre autre si le deplacement est possible dans le cas
* ne fait rien. Le deplacement de Mario peut aussi entrainer le deplacement d'autre piece
* @param pos : position initiale de Mario
* @param sens : sens du deplacement
**}
procedure deplaceMario(pos : TPosition; sens : integer);
var
p : header.TPosition;
begin
if (not depaceFrontiere(pos, sens)) then
begin
p := deplacer(pos, sens);
if(header.grille[p.x, p.y]=sEntite.vide) or (header.grille[p.x, p.y]=sEntite.cible)then
begin
pos.x := p.x;
pos.y := p.y;
end
else if (header.grille[p.x, p.y]=sEntite.caisse) or (header.grille[p.x, p.y]=sEntite.OKCaisse) then
if (deplaceCaisse(p, sens)) then
begin
pos.x := p.x;
pos.y := p.y;
end;
end;
end;

{**
* Boucle principale du jeu. Cette procedure gere le cycle de vie du jeu depuis
* la creation d'une partie jusqu"à la fin du jeu. Elle gere aussi les interactions claviers
**}
function jouer : integer;
var
p            : TPosition;
continuer    : boolean;
touche, sens : integer;
etat         : integer;

begin
p.x := header.marioPosition.x;
p.y := header.marioPosition.y;

continuer := true;
etat := statutJeu.erreur;

misc.initTimer;
affichage.afficheGrille;
misc.viderBuffer;

while (continuer) do
begin
affichage.afficheTimer;
touche := GetKeyPressed;
if ( touche <> KE_NONE) then
begin
misc.viderBuffer;
touche := KeyToAction(touche);
if (touche=GA_UP) then
sens := sSense.haut
else if (touche=GA_DOWN) then
sens := sSense.bas
else if (touche=GA_LEFT) then
sens := sSense.gauche
else if (touche=GA_RIGHT) then
sens := sSense.droite
else if (touche=GA_FIRE) then
begin
continuer := false;
etat := statutJeu.abandon;
end;

deplaceMario(p, sens);
header.setMarioPosition( p.x, p.y);
affichage.afficheGrille;
if (estFini) then
begin
continuer := false;
etat      := statutJeu.gagne;
end;
end;

if(misc.getTimeElapsed>misc.tempsMax) then
begin
etat := statutJeu.perdu;
continuer := false;
end;
delay(10);
end;

jouer := etat;
end;

initialization

statutJeu.erreur  := -1;
statutJeu.abandon :=  0;
statutJeu.perdu   :=  1;
statutJeu.gagne   :=  2;

end.

III-D - Autres unités et assemblage du programme

III-D-1 - Autre Unité

Vous vous souvenez sans doute que je vous avais demandé demander de télécharger deux types de fichiers : les images et les grilles du Mario Sokoban. On a déjà utilisé une partie des images pour l'affichage du jeu. Mais je ne vous ai pas encore dit à quoi vont nous servir ces grilles et comment les utiliser.

Ces grilles contiennent en fait la position des différents objets ou entités que contient le labyrinthe. Dans les codes que je vous ai donnés, on a supposé que la grille a déjà été initialisée avec les différentes entités au départ. Nous allons maintenant voir comment initialiser la grille du jeu.

Nous utilisons un format spécial pour enregistrer les données du labyrinthe. Regardons un exemple :

0022222222
0020333332
0020555032
002020003222
002020105302
002020005002
002025552002
002000000002
002222222222
</end>

Chaque chiffre représente une entité (caisse, Mario, ...). Ce sont en fait ces mêmes chiffres que nous avions utilisés pour définir les constantes définissant ces entités dans les données du programme.

sEntite.vide         := 0;
sEntite.mario     := 1;
sEntite.mur        := 2;
sEntite.cible       := 3;
sEntite.caisse     := 5;
sEntite.okCaisse := 6;

La position des chiffres est la même que pour les éléments de la matrice. Nous allons donc créer une nouvelle unité où nous mettrons les traitements permettant de charger une grille ainsi d'autres procédures permettant la gestion du Timer et la libération du buffer du clavier.

misc.mpsc

unit misc;

interface

const tempsMax = 900 //60*15 s;

procedure chargerGrille(id : string);
procedure initTimer;
function getTimeElapsed : integer;
procedure viderBuffer;

implementation

uses header;

var
temps : integer;

procedure initTimer;
begin
temps :=GetRelativeTimeMs;
end;

{**
* donne le temps ecroule en seconde
***}
function getTimeElapsed : integer;
begin
getTimeElapsed := (GetRelativeTimeMs-temps) div 1000;
end;

{**
* charge une grille depuis un fichier
* @param id : le numero de la grille
**}
procedure chargerGrille(id : string);
var
res   : resource;
ligne : string;
compteur, i, g : integer;
begin
for i:=1 to header.TailleGrilleX do
for g:=1 to header.TailleGrilleY do
grille[i, g] := -1;

res := OpenResource('/'+id+'.sok');
compteur := 0;
repeat
compteur := compteur+1;
ligne := ReadLine(res);
if (length(ligne)&lt;&gt;0) and (ligne&lt;&gt;'&lt;/end&gt;') then
begin
for i:= 0 to (length(ligne)-1) do
begin
g:=StringToInteger(GetChar(ligne, i));
if (g=header.sEntite.mario) then
begin
header.marioPosition.x := i+1;
header.marioPosition.y := compteur;
header.grille[i+1, compteur]:= header.sEntite.vide;
end
else
header.grille[i+1, compteur]:=g;
end;
end;
until (ligne='&lt;/end&gt;');
CloseResource(res);
end;

{**
*vide le buffer du clavier
**}
procedure viderBuffer;
begin
while getKeyPressed &lt;&gt; KE_NONE do
begin
end;
end;

initialization

end.

III-D-2 - Assemblage du programme

Nous venons de terminer la partie la plus importante du projet. Il ne nous reste qu'à écrire le programme principal, compiler puis exécuter le projet.

sokoban.mpsrc

program sokoban;

uses jeu, header, affichage, misc;

var etat : integer;

begin
misc.chargerGrille('grille1');
etat:=jeu.jouer;
end.

Nous verrons dans la prochaine section comment ajouter des menus et un écran d'accueil à notre programme.

Vous pouvez télécharger la version actuelle du projet : sokoban.zip.

IV - Les Menus

Dans la section précédente, nous avons écrit le programme rendant le jeu jouable. Mais on est contraint de résoudre la même grille à chaque fois. Aussi, l'utilisateur n'a pas d'aide pour comprendre le but du jeu ni pour savoir comment y jouer.

Nous allons dans cette section ajouter un écran d'accueil à notre programme. Cet écran sera muni d'un menu principal donnant accès à plusieurs choix tels que : Nouveau jeu - Aide du jeu - A Propos - Quitter. Bien avant que le menu principal apparaisse, on demandera à l'utilisateur s'il veut activer ou désactiver la musique de fond. Le choix d'un menu fera éventuellement apparaître un autre menu ou effectuer une tâche particulière.

IV-A - Le système de menu

Nous allons utiliser un système de menu bien simple mais avec un peu de fantaisie.

Chaque menu a un titre général, et des options que nous conserverons dans un tableau. L'affichage du menu se fera par groupe de trois options. Selon qu'on clique sur les touches de direction basse et haute, les autres options deviennent visibles. On affichera une boule à coté du menu en surbrillance quelques secondes après l'affichage des options.

procedure afficherMenu(index : integer);
var
y, z,  d         : integer;
avant, apres     : integer;
begin

SetFont(FONT_FACE_SYSTEM, FONT_STYLE_PLAIN, FONT_SIZE_SMALL);
d := GetStringHeight('H');

setColor(0, 0, 55);
FillRect(0, menuPos.y-d*2, getWidth, (d+9)*5);

setColor(204, 207, 57);
centrerTexteX(TitreMenu, menuPos.y-d*2);

avant := index-1;
if (avant<1) then
avant := dimMenu;
apres := index+1;
if (apres > dimMenu) then
apres := 1;

setColor(116, 138, 113);
centrerTexteX(menu[avant], menuPos.y);

y := menuPos.y+d+8;
z:= y+(d div 2)-7;
setColor(100, 192, 91);
centrerTexteX(menu[index], y);

y := y+d+8;
setColor(116, 138, 113);
centrerTexteX(menu[apres], y);

repaint;
delay(400);
drawImage(check, ObtenirCentreX(menu[index])-20, z);
end;

Pour créer par exemple l'écran d'accueil, on définit les options du menu dans le tableau Menu, puis le nombre d'options dans le la variable dimMenu ainsi que le titre du menu. On demandera ensuite au joueur de choisir une option.

procedure AfficheMenuAccueil ;
var
x, etat : integer;
img : Image;
begin
img := loadImage('/sokoban.png');
//on cherhce a centrer l'image sur l'ecran
x := (getWidth div 2)-(getImageWidth(img) div 2);

repeat

setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);
menu[1] := 'Jouer';
menu[2] := 'Aide du jeu';
menu[3] := 'A propos';
menu[4] := 'Quitter';
dimMenu := 4;
TitreMenu := 'MEMU PRINCIPAL';

//l'entete
drawImage(img, x, 10);
afficherMenu(1);
Repaint;

etat :=choixMenu;
if (etat=1) then
AfficheMenuNiveau
else if (etat=2) then
annexe.aide;
else if (etat=3) then
annexe.apropos;
until etat= 4;

end;

La sélection d'une option entraîne l'exécution d'une action, que nous codons dans une procédure. Les actions à effectuer pour le choix des options sont écrites dans l'unité annexe.mprsc. Voici tout le code gerant la gestion des menus.

menu.mpsrc

unit menu;

interface

procedure AfficheMenuAccueil ;
procedure AfficheMenuSon ;

implementation

uses misc, jeu, header, annexe;

const
dimMax = 15; // Nombre d'option maximal que peut contenir un menu
NiveauMax = 10; //le niveau de jeu le plus eleve

var
logoPos, menuPos : record // position du coin superieur gauche du logo et du menu
x, y :integer;
end;

Menu               : array[1..dimMax] of string;  //les options du menu
TitreMenu          : String; // le titre du menu
dimMenu            : integer; // le nombre d'option que continet effectivement le menu courant

check, imgEtoile   : Image;

{**
* dessine un texte centrée sur l'ecran
* @param ch : le texte a affiche
* @param y : position verticale du texte
**}
procedure centrerTexteX(ch : String; y : integer);
var x : integer;
begin
x:=(getWidth div 2) - (getStringWidth(ch) div 2);
drawText(ch, x, y);
end;

{**
* obtient l'abscisse qui permet de centrer le texte a l'ecran
* @param ch : le texte a affiche
* @return : l'abscisse du point
**}
function ObtenircentreX(ch : String) : integer;
var x : integer;
begin
x:=(getWidth div 2) - (getStringWidth(ch) div 2);
ObtenirCentreX := x;
end;

{**
* affiche le menu courant a l'ecran
* @param ch : l'index de l'option choisie
**}
procedure afficherMenu(index : integer);
var
y, z,  d         : integer;
avant, apres     : integer;
begin

SetFont(FONT_FACE_SYSTEM, FONT_STYLE_PLAIN, FONT_SIZE_SMALL);
d := GetStringHeight('H');

setColor(0, 0, 55);
FillRect(0, menuPos.y-d*2, getWidth, (d+9)*5);

setColor(204, 207, 57);
centrerTexteX(TitreMenu, menuPos.y-d*2);

avant := index-1;
if (avant&lt;1) then
avant := dimMenu;
apres := index+1;
if (apres &gt; dimMenu) then
apres := 1;

setColor(116, 138, 113);
centrerTexteX(menu[avant], menuPos.y);

y := menuPos.y+d+8;
z:= y+(d div 2)-7;
setColor(100, 192, 91);
centrerTexteX(menu[index], y);

y := y+d+8;
setColor(116, 138, 113);
centrerTexteX(menu[apres], y);

repaint;
delay(400);
drawImage(check, ObtenirCentreX(menu[index])-20, z);
end;

{**
* permet a l'utilisateur de choisir une option et valider son choix
* @return : l'index de l'option choisie
**}
function choixMenu : integer;
var
fin    : boolean;
choix  : integer;
touche :integer;
begin
fin := false;
choix := 1;
repeat
touche := GetKeyPressed;
if (touche&lt;&gt;KE_NONE ) then
begin
if (touche=KE_KEY2) or (KeyToAction(touche)=GA_UP) then
begin
choix := choix-1;
if choix&lt;1 then
choix := dimMenu;
afficherMenu(choix);
Repaint;
end
else if (touche=KE_KEY8) or (KeyToAction(touche)=GA_DOWN) then
begin
choix:=choix+1;
if choix&gt;dimMenu then
choix := 1;
afficherMenu(choix);
Repaint;
end
else if (touche=KE_KEY5) or (KeyToAction(touche)=GA_FIRE) then
begin
fin := true;
choixMenu := choix;
end;

touche := KE_NONE;
end;
until fin;
end;

{**
* Affiche le menu permettant d'activer ou desactiver la musique de fond
**}
procedure AfficheMenuSon ;
var
etat : integer;
begin

menu[1] := 'OUI';
menu[2] := 'NON';
dimMenu := 2;
TitreMenu := 'ACTIVEZ LE SON ?';

setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);

afficherMenu(1);

Repaint;
etat := choixMenu;
if(etat&lt;=1) then
annexe.music;
end;

{**
* Affiche le niveau du jeu actuellement choisi par l'utilisateur
* @param ch : le niveau choisi
**}
procedure afficheNiveau(id : integer);
var
x, y : integer;
begin
setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);

//on cherche a centrer le texte sur l"ecran
x:= (getWidth div 2) - (getStringWidth('NIVEAU '+id) div 2)  -20;
y:= (getHeight div 2) - (getStringHeight('N') div 2);
setColor(100, 192, 91);
drawText('NIVEAU '+id, x+20, y);
drawImage(imgEtoile, x, y);
repaint;
delay(1500);
end;

{**
* Charge et lance une partie
* @param id : le niveau de la partie
**}
procedure ActionJouer(id : integer);
var
etat : integer;
quitter : boolean;
begin
quitter := false;
repeat
afficheNiveau(id);
misc.chargerGrille('grille'+id);
etat:=jeu.jouer;
if (etat= jeu.statutJeu.gagne) then
begin
quitter := false;
if(id&lt;NiveauMax) then
id :=id+1;
end
else
quitter := true;
until quitter;

end;

{**
* Affiche le menu permettant de choisir un niveau, puis lance une partie
**}
procedure AfficheMenuNiveau ;
var
i, etat : integer;
begin

for i := 1 to NiveauMax do
menu[i] := 'Niveau '+i;
menu[NiveauMax+1] := 'Anuler';
dimMenu := NiveauMax+1;
TitreMenu := 'CHOISISSEZ UN NIVEAU';

afficherMenu(1);

Repaint;
etat := choixMenu;
if(etat&lt;=NiveauMax) then
ActionJouer(etat);
end;

{**
* Le menu principal
**}
procedure AfficheMenuAccueil ;
var
x, etat : integer;
img : Image;
begin
img := loadImage('/sokoban.png');
//on cherhce a centrer l'image sur l'ecran
x := (getWidth div 2)-(getImageWidth(img) div 2);

repeat

setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);
menu[1] := 'Jouer';
menu[2] := 'Aide du jeu';
menu[3] := 'A propos';
menu[4] := 'Quitter';
dimMenu := 4;
TitreMenu := 'MEMU PRINCIPAL';

//l'entete
drawImage(img, x, 10);
afficherMenu(1);
Repaint;

etat :=choixMenu;
if (etat=1) then
AfficheMenuNiveau
else if (etat=2) then
annexe.aide;
else if (etat=3) then
annexe.apropos;
until etat= 4;

end;

initialization

menuPos.x := 50;
menuPos.y := 100;

check := loadImage('/10118.png');
imgEtoile := loadImage('/etoile.png');
end.

Et le code de l'unité comportant les actions :

annexe.mpsrc

unit annexe;

interface

procedure music;
procedure aPropos;
procedure aide;

implementation

uses header, affichage;

var imgRetour : Image;

{**
* lance la lecture en boucle de la musique du fond
**}
procedure music;
var
b : boolean;
begin
if  OpenPlayer('/bgsound.mid', 'audio/midi') then
if SetPlayerCount(-1) then
b:=StartPlayer ;
end;

procedure centrerTexte(ch : String; y : integer);
var
x : integer;
begin
x:=(getWidth div 2) - (getStringWidth(ch) div 2);
drawText(ch, x, y);
end;

procedure attente;
begin
while (GetKeyPressed<> KE_NONE) do
begin
end;

repeat
until KeyToAction(GetKeyPressed)=GA_FIRE;
end;

procedure afficheEntete;
var
img : Image;
x : integer;
begin
img := loadImage('/sokoban.png');
x := (getWidth div 2)-(getImageWidth(img) div 2);
drawImage(img, x, 10);
end;

(**
** affiche le texte a propos du programme
**
**)
procedure aPropos;
var
x, y, dy , clicked: integer;
begin

setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);
afficheEntete;
drawImage(imgRetour,  (getWidth div 2)-8, (GetHeight)-20) ;

setColor(116, 138, 113);
SetFont(FONT_FACE_MONOSPACE, FONT_STYLE_PLAIN, FONT_SIZE_SMALL);
y:=45;
dy:=getStringHeight('H');
dy:=dy+dy div 3;

y:=y+dy;
centrerTexte('Par Kpizingui Darryl', y);
y:=y+dy;
centrerTexte('nobi_8@hotmail.com', y);
y:=y+dy;
centrerTexte('http://darrylsite.tk/', y);
y:=y+dy;
centrerTexte('© Copyright 2010', y);
y:=y+dy;
drawImage(loadImage('/logo.png'), (getWidth div 2)-25, y);

Repaint;

attente;
end;

procedure aide;
var x, y, d : integer;
begin
setColor(0, 0, 55);
fillRect(0, 0, getWidth, getHeight);

afficheEntete;

setColor(116, 138, 113);
SetFont(FONT_FACE_MONOSPACE, FONT_STYLE_PLAIN, FONT_SIZE_SMALL);

x := 20;
y := 60;
d := 20;

drawImage(affichage.imgMario, x, y);
drawText('Mario', x+d, y);
y:=y+d+(d div 3);

drawImage(affichage.imgCible, x, y);
drawText('Cible', x+d, y);
y:=y+d+(d div 3);

drawImage(affichage.imgCaisse, x, y);
drawText('Caisse', x+d, y);
y:=y+d+(d div 3);

drawImage(affichage.imgOKCaisse, x, y);
drawText('Caisse bien placée', x+d, y);
y:=y+d+(d div 3);

drawImage(affichage.imgMur, x, y);
drawText('Mur', x+d, y);

drawImage(imgRetour,  (getWidth div 2)-8, (GetHeight)-20) ;

repaint;

attente;

setColor(0, 0, 55);
fillRect(0, 50, getWidth, getHeight);
setColor(116, 138, 113);
SetFont(FONT_FACE_MONOSPACE, FONT_STYLE_PLAIN, FONT_SIZE_SMALL);
y:=45;
d:=getStringHeight('H')+(d div 3);
y:=y+d;
centrerTexte('Utilisez les touches de', y);
y:=y+d;
centrerTexte('direction pour diriger', y);
y:=y+d;
centrerTexte('Mario afin qu''il deplace', y);
y:=y+d;
centrerTexte('les caisses sur les cibles.', y);
y:=y+d;
centrerTexte('La partie est gagnée s''il', y);
y:=y+d;
centrerTexte('ne reste plus aucune cible', y);

drawImage(imgRetour,  (getWidth div 2)-8, (GetHeight)-20) ;

repaint;
attente;
end;

initialization
imgRetour := loadImage('/return.png');
end.

IV-B - Assemblage du programme

Nous venons d'écrire le code permettant d'ajouter des menus à notre programme. Il nous reste donc à écrire le programme principal permettant de lancer le jeu depuis le menu principal.

sokoban.mpsrc

program sokoban;

uses jeu, header, affichage, misc, menu, annexe;

begin
menu.AfficheMenuSon;
menu.AfficheMenuAccueil;
end.

Pour savoir comment éxecuter le programme dans un émulateur ou sur un téléphone portable, vous pouvez consulter le tutoriel suivant : Introduction au MIDlet Pascal 2.02

V - Conclusion et remerciements

V-A - Conclusion

Nous sommes donc arrivés à écrire le programme qui nous permet de jouer au jeu Mario Sokoban en utilisant MIDlet Pascal. Bien que le jeu soit assez simple à programmer, il faut un peu d'organisation et d'analyse pour y arriver. J'ai choisi une des mille et une méthodes qui existent pour vous présenter un exemple de programme qu'on peut réaliser avec MIDlet Pascal. Des programmes plus complexes peuvent être réalisés avec cet outil, et ses possibilités peuvent être augmentées en utilisant des librairies Java.

Vous pouvez télécharger le projet complet : sokoban.zip

V-B - Remerciements

J'adresse tous mes remerciements à krachik, Alcatîz ainsi qu'à m-dlb pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.