Création d'un jeu en 3D avec Delphi et FireMonkey

Grégory Bersegeay

Ce tutoriel va nous mener jusqu'à la réalisation d'un petit jeu de type Pong en 3D avec Delphi. Pour cela, nous utiliserons le framework FireMonkey. Sans toucher au code mais simplement en recompilant, vous pourrez faire tourner l'application sous Windows et MAC OS X. Si vous disposez du plugin Mobile ou d'une version Enterprise de Delphi, vous pourrez le compiler aussi pour iOS et Android.

Commentez

- Titre : Création d'un jeu en 3D avec Delphi et FireMonkey

- Auteur : Grégory Bersegeay

- Parution : 26 octobre 2016

- Public visé :

- 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 ® 2016 Grégory Bersegeay. 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. Prérequis

- II. Présentation du projet

- III. Le développement

- III-A. Initialisation du projet

- III-B. La 3D

- III-C. Positionnement des objets 3D

- III-D. Déclaration des paramètres globaux

- III-E. Développement du cœur du programme

- IV. Améliorations

- IV-A. Applications de textures

- IV-B. Application d'un style

- IV-C. Affichage d'un écran d'aide

- V. Remerciements

I - Prérequis

Ce tutoriel a été fait avec Delphi 10 Seattle Professionnel. Vous pourrez compiler le projet pour Windows, Mac OS X, Android et iOS. Je n'ai pas fait de test avec iOS, car je n'ai pas de périphérique adéquat. J'ai testé sous Windows 10, El Capitan et Android 5 et ça fonctionne sans avoir à modifier le code. Seule la compilation diffère. J'ai juste remarqué qu'à réglage de la vitesse identique, le palet se déplace plus vite sous Windows et MAC OS que sous Android.

Il s'agit de ma première approche du développement avec le framework multiplate-forme de Delphi. Je ne suis pas un spécialiste de FireMonkey, aussi toutes les remarques sont bonnes à prendre.

II - Présentation du projet

Pour mon premier tutoriel et ma première application avec FireMonkey, quoi de mieux que de faire une reprise d'un des premiers jeux vidéo : Pong ? Ce n'est pas d'une grande originalité, mais cela va nous permettre de voir des rudiments de 3D et d'intelligence artificielle, le tout dans un projet simple d'environ 420 lignes de code (commentaires inclus).

L'interface, très simple, est constituée d'une vue 3D et d'une barre en bas abritant le bouton permettant de démarrer une partie, d'un trackbar permettant de régler la difficulté, de l'affichage du score et d'un bouton permettant d'afficher l'aide.

Le but du jeu est très simple : un palet se déplace sur la table de jeu et vous devez contrôler la raquette et tenter de marquer des buts à votre adversaire. Le premier qui atteint le score de 7 points remporte la partie.

III - Le développement

Allez ! Assez parlé et passons aux choses sérieuses ! Je ne détaillerai pas forcément toutes les lignes de code : je m'attarderai sur les parties les plus importantes. Le code source du projet étant livré avec le tutoriel (ou téléchargeable à l'adresse :https://gbegreg.developpez.com/tutoriels/delphi/firemonkey/Pong3D/src/FMXPong_src.zip ), vous pourrez étudier ou modifier le code à loisir.

III-A - Initialisation du projet

Une fois Delphi démarré, démarrez un nouveau projet via le menu Fichier/Nouveau/Application multi-périphérique. Un assistant s'ouvre alors présentant des modèles d'application. Pour ce tutoriel, nous allons partir d'une page blanche : sélectionnez le modèle « Application vide ».

Nous allons placer les principaux éléments de l'interface :

- un panel (nommé pnlAction) que nous alignerons en bas de l'interface ;

- un viewport3D (nommé affichage3D) que nous alignerons sur le reste de l'interface.

Même si dans notre exemple cela représente peu d'intérêt, j'ai l'habitude d'utiliser les actions. Aussi, nous allons déposer sur la fiche un TactionList puis y créer deux actions :

- une action qui permettra de démarrer une nouvelle partie (actJouer) ;

- une action qui permettra d'afficher les règles du jeu (actAide).

Nous allons terminer de placer les éléments graphiques sur le panel, avec :

- un bouton associé à l'action actJouer : le clic sur le bouton déclenchera l'événement OnExecute de l'action ;

- un trackbar qui permettra de modifier la difficulté du jeu ;

- un label qui permettra d'afficher le score.

III-B - La 3D

Pour celles et ceux qui ont déjà manipulé GLScene, on retrouve sous FireMonkey un éditeur graphique de la scène 3D assez similaire.

Pour notre projet, la scène est simple (la figure 1 est une capture d'écran de l'IDE Delphi en mode conception afin de visualiser la scène 3D) :

- La table de jeu est constituée d'une base (un TCube large et aplati) et de deux bordures (deux TCube là encore).

- Le palet est un TCylinder.

- Les raquettes sont des TRoundCube.

- Un TText3D nommé txtGagne servira à afficher le message « Gagné » ou « Perdu » à la fin d'une partie (premier joueur ayant marqué 7 buts).

- Un TStrokeCube qui ne sera pas visible, mais qui va permettre de déterminer l'espace dans lequel le palet va pouvoir se déplacer. On taillera donc ce composant pour faire correspondre ses grands côtés aux bords de la table et les petits côtés derrière les buts. Ainsi, lors du déplacement du palet, on contrôlera la position de celui-ci par rapport à ce TStrokeCube. Si le palet atteint les grands côtés, alors on provoquera un rebond, si le palet atteint les petits côtés, alors un but sera marqué.

- Une lumière d'ambiance (Tlight) de type directionnelle permettra d'éclairer la scène.

- Enfin, deux Tdummy sont utilisés. Ces composants non visibles permettent de « regrouper » les autres objets 3D. Ainsi, un TDummy (dmyPrincipal) contiendra tous les objets 3D de la scène et le second (dmyPalet) contiendra le palet et deux TFloatAnimation nommés apparition et disparition. Ces deux animations seront de type Opacity : elles permettront de faire un fondu pour faire disparaître ou apparaître le palet en cas de but de remise en jeu.

Figure 1 : Conception de la scène 3D depuis le designer de Delphi

D'autres objets seront utilisés plus loin dans le tutoriel pour améliorer le rendu (effets d'animation et de couleur) et si vous utilisez les sources fournis, vous verrez également des objets TLightMaterialSource pour les couleurs et la texture (un bitmap trouvé sur internet) et TStyleBook pour appliquer un style « sombre » au panel (car j'aime bien).

III-C - Positionnement des objets 3D

Pour positionner les objets 3D, l'éditeur d'interface graphique de Delphi permet de faire cela à la souris et d'avoir le rendu en temps réel. C'est très pratique quand on tâtonne comme moi. Pour parvenir au résultat final du projet, j'ai cherché les meilleures positions et orientations selon mes goûts.

Nous allons commencer par placer un Tdummy (nommé dmyPrincipal) dans le viewport3D.

Nous sommes en 3D donc pour le positionner, nous avons trois coordonnées à renseigner : les abscisses X (droite/gauche), les ordonnées Y (haut/bas) et la profondeur Z (plus ou moins proche). Nous allons positionner le dummy en X=0, Y=0 et Z=6.

En plus de la position, il faut également renseigner l'orientation de l'objet : la propriété RotationAngle permet de définir cette orientation en précisant l'angle (en degré) à donner sur chacun des trois axes. Dans notre exemple, nous allons renseigner l'angle sur l'axe X à 290 et à 0 sur les axes Y et Z.

Enfin, nous allons régler l'échelle de l'objet. On peut là aussi jouer sur les trois axes. J'ai réglé l'échelle à 0,5 sur les trois axes.

Nous allons placer ensuite tous les autres objets 3D dans ce dummy. Les positions, orientations et échelles de ces objets seront ainsi par rapport à celles du dummy. C'est pratique si on souhaite appliquer un mouvement (rotation, zoom…) à l'ensemble de la scène 3D : on applique le mouvement au dummy et tous les objets qu'il contient subiront ce mouvement.

Figure 2 : Détails de la vue « structure » détaillantla hiérarchie des objets de la scène

Créons la table de jeu :

- Plaçons un TCube (nommé base) dans le dmyPrincipal. Ça sera la base du plateau de jeu. On le configure à la position X=0, Y=0 et Z=2. On conserve l'orientation par défaut (X=0, Y=0 et Z=0) ainsi que l'échelle (X=1, Y=1 et Z=1). Configurons la taille du plateau grâce aux propriétés Width à 18 (la largeur), Height à 40 (la longueur) et Depth à 1 (l'épaisseur).

- Ajoutons un second TCube qui sera la bordure de droite (nommé bordDroit). On lui donne une largeur de 1, une longueur de 40 et une épaisseur de 4. Il faut le positionner au bord droit du plateau : c'est-à-dire 18 (la largeur du plateau) - 1 (la largeur du bord) = 17. L'origine du repère étant le milieu du plateau, on divise 17/2 ce qui donne 8,5. Positionnons donc le bordDroit à X=8,5. Les autres paramètres (orientation et échelle) restent par défaut.

- Ajoutons un troisième TCube qui sera la bordure de gauche (nommé bordGauche). On lui donne les mêmes caractéristiques que bordDroit à part la position X qui sera à -8,5.

- Ajoutons maintenant un TStrokeCube (nommé cubeBande) qui ne sera pas visible, mais qui représentera l'espace dans lequel peut évoluer le palet. Lorsque le palet touche les grands côtés, il rebondira, et lorsqu'il touche les petits côtés, il y aura but.
On le laisse avec ses position et orientation par défaut (X=0, Y=0, Z=0), et on modifie sa largeur à 16, sa longueur à 42 (afin qu'il dépasse de la table de jeu en longueur pour les zones de buts) et une épaisseur de 1.

- Ajoutons un TRoundCube qui sera la raquette du joueur (nommé raquetteJoueur). On le place à la position X=0, Y= 19 et Z = 0,5. On le configure à une largeur de 3,5, une longueur et une épaisseur de 1.
La raquette gérée par l'ordinateur n'est pas créée lors de la conception de l'application. On fera un clone de la raquette du joueur dynamiquement. Si on change les propriétés de la raquette du joueur, elles seront ainsi automatiquement reportées sur la raquette gérée par l'ordinateur.

- Ajoutons la lumière d'ambiance pour éclairer la scène. Pour ce faire, on place un TLight (nommé lumiere) sur le viewport3D. On choisit le type de lumière « Directional » (on choisit une couleur et une direction et toute la scène est éclairée de la même manière. Il existe d'autres types de lumières « sport » et « point » qui permettent d'éclairer différemment la scène : l'intensité de la lumière n'est pas la même sur toute la scène par exemple).
Le positionnement de la lumière peut se faire par l'inspecteur d'objet via les propriétés Position et RotationAngle, mais il est plus pratique d'utiliser l'éditeur de l'interface pour orienter la lumière comme on le souhaite.

- Pour terminer la scène 3D, on place un second TDummy (nommé dmyPalet) qui permettra de gérer le palet. On conserve les Position et RotationAngle aux valeurs par défaut.

- Ajoutons un TCylinder (nommé palet) au dmyPalet. Nous le laissons à la position X=0, Y=0 et Z=0, par contre, nous mettons le RotationAngle.X à 90. Nous lui affectons une largeur de 2, une longueur de 0,5 et une épaisseur de 2.

Les bases sont posées, passons maintenant au code.

III-D - Déclaration des paramètres globaux

Dans la classe de la form, cinq attributs publics :

- vitesse de type single correspond à la vitesse de déplacement du palet.

- vistesseInitiale de type single et qui indique la vitesse initiale du palet en début de partie.

- tempsReactionIA de type single qui indiquera le délai de réaction avant de déplacer la raquette gérée par l'ordinateur.

- scoreJoueur et scoreCPU de type integer qui permettent de stocker le nombre de points de chaque joueur.

- jeu de type integer qui va permettre de déterminer quelle scène on souhaite afficher. Notre jeu contiendra cinq scènes différentes :

- Lorsque jeu = 1, on affiche la scène d'introduction. Celle-ci affiche la table et les deux raquettes qui zooment et qui tournent sans cesse.

- Lorsque jeu = 2, c'est la scène de jeu proprement dite qui est affichée. Le joueur a le contrôle de sa raquette, l'ordinateur gère la sienne et le palet se déplace.

- Lorsque jeu = 3, on affiche le message que le joueur a perdu.

- Lorsque jeu = 4, on affiche le message que le joueur a gagné.

- Lorsque jeu = 5, on affiche l'aide, c'est-à-dire un mémo avec un petit texte détaillant les règles du jeu.

III-E - Développement du cœur du programme

On va commencer par générer les événements liés à la raquette du joueur. Le joueur devra cliquer sur sa raquette, maintenir le bouton gauche de la souris enfoncé et déplacer la souris pour diriger sa raquette.

Dans l'événement OnMouseDown, on récupère la position actuelle de la raquette.

procedure TfPrincipale.raquetteJoueurMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single; RayPos, RayDir: TVector3D);
begin
// L'utilisateur clique sur sa raquette, on récupère la position de la raquette
if ssLeft in Shift then
begin
with TControl3D(sender).Position do
begin
DefaultValue := Point - (RayDir * RayPos.Length) * Point3D(1,0,0);
end;
end;
end;

Dans l'événement OnMouseMove, on gère le déplacement de la raquette. On limite le déplacement de la raquette à la largeur intérieure de la table afin que la raquette ne passe pas à travers les bordures de droite et gauche.

procedure TfPrincipale.raquetteJoueurMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Single; RayPos, RayDir: TVector3D);
var
limiteTableGauche, limiteTableDroite : single;
begin
affichage3D.BeginUpdate;
// L'utilisateur maintient le bouton gauche de la souris enfoncé et déplace la souris, alors on déplace sa raquette
if ssLeft in Shift then
begin
with TControl3D(sender).Position do
begin
// Nouvelle position de la raquette du joueur
Point := DefaultValue + (RayDir *RayPos.length) * Point3D(1,0,0);
// Si la nouvelle position de la raquette en X sort des limites de jeu (largeur intérieure de la table
limiteTableGauche := -(cubebande.width-raquetteJoueur.width)/2;
limiteTableDroite := (cubebande.width-raquetteJoueur.width)/2;
if Point.X < limiteTableGauche then Point := Point3D(limiteTableGauche,point.y,point.z);
if Point.X > limiteTableDroite then Point := Point3D(limiteTableDroite,point.y,point.z);
end;
end;

affichage3D.EndUpdate;
end;

Dans l'événement OnMouseUp, lorsque l'utilisateur relâche le bouton gauche de la souris, on fait revenir la raquette automatiquement au milieu du plateau de jeu. Pour ce faire, on utilise la méthode AnimateFloat de l'objet, et on crée une animation de type déplacement sur l'axe X :

procedure TfPrincipale.raquetteJoueurMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single; RayPos, RayDir: TVector3D);
begin
TControl3D(sender).Tag := 0;
if TControl3D(sender) = raquetteJoueur then
begin
// Si l'utilisateur relâche le bouton de la souris, on provoque le retour de la raquette au centre (position X=0)
raquetteJoueur.AnimateFloat('Position.X',0);
end;
end;

Enfin, dernier événement que l'on va implémenter sur la raquette du joueur, le OnRender. Cela nous permet de gérer les collisions entre la raquette et le palet et ainsi faire rebondir le palet.

procedure TfPrincipale.raquetteJoueurRender(Sender: TObject; Context: TContext3D);
var
P, Z, R, M, D : TPoint3D;
begin
// Récupération de la position de la raquette
with TControl3D(Sender) do
begin
P:=AbsoluteToLocal3D(dmyPalet.AbsolutePosition);
Z:=Point3D( 1 /width, 1 /height, 1 / depth);
R:=Point3d(width, height, depth);
D := Position.Point;
end;

// Récupération de la position du palet
with dmyPalet do M:= ( R + Point3D(width, height, depth)) * 0.5;

// Renvoi du palet si nécessaire
if (( abs(P.X) < M.X) and (abs(P.Y) < M.Y)) then
begin
// Renvoi du palet en fonction de point de contact sur la raquette (plus on est vers le bord de la raquette, plus il y aura d'angle)
D:=(dmyPalet.Position.point - D).Normalize;
D:=D * Point3D(45,90,0);
dmyPalet.Position.DefaultValue:=D;
end;
end;

Cet événement OnRender servira également à la raquette gérée par l'ordinateur. D'ailleurs, passons maintenant à l'initialisation de la form en renseignant l'événement FormCreate :

procedure TfPrincipale.FormCreate(Sender: TObject);
begin
cubeBande.visible := false;
dmyPalet.Visible := false;
palet.Visible := false;
initialiserPlateau;
tempsReactionIA := 0.30;
jeu := 1;
end;

On crée la procédure initialiserPlateau qui permet d'initialiser la table de jeu en créant dynamiquement la raquette gérée par l'ordinateur par clonage de celle du joueur :

// initialisation du plateau de jeu avec création dynamique de la raquette gérée par l'ordi
procedure TfPrincipale.initialiserPlateau;
begin
vitesse := 1;

// Création de la raquette gérée par l'IA par clonage de la raquette du joueur
with TRoundCube(raquetteJoueur.Clone(nil)) do
begin
parent := dmyPrincipal;
// On positionne cette raquette de l'autre côté de la table
Position.Y := -18;
OnRender := raquetteJoueurRender;
HitTest := false;
AddObject(CPUAI);
CPUAI.PropertyName:='Position.X';
end;

// Permet de capturer automatiquement la souris si l'utilisateur clique sur l'objet
raquetteJoueur.AutoCapture := true;
end;

On constate que l'on affecte à l'événement OnRender la même procédure que pour la raquette du joueur.

Il y a également l'objet CPUAI dont on n'a pas encore parlé. Il s'agit d'un TFloatAnimation qui servira à déplacer la raquette gérée par l'utilisateur. On précise que le type d'animation est une translation sur l'axe X.

Tant que nous y sommes, passons à la procédure qui va justement gérer le déplacement de la raquette gérée par l'ordinateur. L'intelligence artificielle (très simple) sera gérée de la manière suivante :

Lorsque le palet se dirige du joueur vers l'ordinateur, on calcule l'endroit où va arriver le palet sur la ligne du but. On va se servir de l'animation CPUAI pour déplacer la raquette vers ce point en lui précisant la position où le palet est censé entrer dans le but et la durée de l'animation. Lorsqu'on active l'animation, la raquette se déplacera depuis sa position actuelle jusqu'à celle calculée. La durée de l'animation correspond au temps que mettra la raquette pour rejoindre le point d'arrivée. Le trackbar servant à régler la difficulté du jeu va du coup jouer sur la vitesse du palet et aussi sur la durée de cette animation.

La position est recalculée si le palet rebondit contre un grand côté. Par conséquent, vous aurez plus de chance de marquer un but si vous parvenez à faire rebondir le palet sur un grand côté près du but adverse…

procedure TfPrincipale.CPU;
var
P, K, D, H : TPoint3D;
B, S : Single;
begin
// Si l'ordi arrive à 7 points, alors il gagne
if scoreCPU = 7 then
begin
jeu := 3;
exit;
end;

// Si joueur arrive à 7 points alors il gagne
if scoreJoueur = 7 then
begin
jeu := 4;
exit;
end;

P:=dmyPalet.Position.Point;
K:=Point3D(0, raquetteJoueur.Position.Y, 0);
K.X:=cubeBande.Width *0.5;
D:=dmyPalet.Position.DefaultValue.Normalize;

// Utilisation du TFloatAnimation CPUAI pour faire déplacer la raquette gérée par l'ordi
with CPUAI do
begin
if not Running and (dmyPalet.Position.DefaultValue.Y < 0) then
begin
// Calcul du point d'impact prévisionnel du palet et du but
B:=Abs((P.Y - (-K.Y)) / D.Y);
H:= P + D * B;
S:=raquetteJoueur.Width * 0.5;
P:=TControl3D(parent).Position.Point;
if H.X - S < -K.X then H.X := -K.X +S;
if H.X + S > K.X then H.X := K.X -S;
// Calcul de la durée de l'animation de déplacement de la raquette
duration:= tempsReactionIA + random * 0.25;
// Paramétrage du début et de la fin de l'animation (qui est un déplacement sur l'axe X : c'est défini dans initialiserPlateau)
StartValue := P.X;
StopValue := H.X;
// On lance l'animation
Start;
end;
end;
end;

Voyons maintenant la procédure qui va gérer le mouvement du palet sur la table de jeu et la gestion des collisions entre le palet et le TStrokeCube. Comme je l'ai déjà indiqué, le TStrokeCube cubeBande représente l'espace dans lequel le palet peut se déplacer. S'il se heurte aux grands côtés, le palet doit rebondir, s'il heurte les petits côtés, un but est marqué.

Voici donc la procédure qui gère ces mouvements.

// Déplacement du palet
procedure TfPrincipale.DeplacementPalet;
var P, D, M : TPoint3d;
w : single;
begin
// Calcul de la vitesse
vitesse := TPointF.Create(base.Width, base.Height).Length / (vitesseInitiale * tbDifficulte.Value);
// Détection de collision du palet avec cubeBande
P:=dmyPrincipal.AbsoluteToLocal3D(dmyPalet.AbsolutePosition);
D:=dmyPalet.Position.DefaultValue.Normalize;
M:=Point3d(cubeBande.width, cubeBande.height, cubeBande.depth);
M:=(M - Point3d(dmyPalet.width, dmyPalet.height, dmyPalet.depth)) * 0.5;

P:=P + D * vitesse;
// Si contact du palet avec les grands côtés du cubeBande, on calcule la nouvelle direction du palet
if ((P.X > M.X) and (D.X > 0)) or ((P.X < -M.X) and (D.X < 0)) then
begin
if abs(d.x / d.Y) > 2 then d:=Point3D(Random, D.X, 0)
else d.X := -D.X;

end;

// Si contact du palet avec les petits côtés du cubeBande, c'est qu'il y a but
if ((P.Y > M.Y) and (D.Y > 0)) or ((P.Y < -M.Y) and (D.Y < 0)) then
begin
// calcul du score en fonction du but dans lequel se situe le pavé
if P.Y > M.Y then scoreCPU := scoreCPU +1;
if P.Y < -M.Y then scoreJoueur := scoreJoueur +1;
// Arrêt de la boucle principale de jeu
aniBouclePrincipale.StopAtCurrent;
exit;
end;

Passons maintenant à la procédure qui permettra de faire un engagement ou un service, comme vous voulez. Nous ajoutons une règle : chaque joueur sert deux fois consécutivement puis le service change de camp. Voici le code de la procédure Service :

procedure TfPrincipale.Service;
var
serviceAQui: Integer;
const nbService: Integer = 2;
begin
// affichage du score
lblScore.text := Format(' %d : %d', [scoreCPU, scoreJoueur]);
// chaque joueur sert deux fois de suite
serviceAQui := 1 - (2 * (( (scoreJoueur+scoreCPU) div nbService) mod 2));
// Positionnement du palet pour service
dmyPalet.Position.DefaultValue:=Point3d(1,1,0) * serviceAQui;
With base do
begin
dmyPalet.Position.Point:= Point3d(width - 5, height -5 ,0) * -0.5 * serviceAQui;
end;
// Effet de fondu pour faire apparaitre le palet
apparition.Start;
end;

On profite de cette méthode Service pour mettre à jour l'affichage du score.

Ça y est ! Nous avons toutes nos briques et il ne manque plus que la boucle principale de notre jeu. Pour ce faire, nous allons positionner un nouveau TfloatAnimation que nous appellerons aniBouclePrincipale. Nous allons la configurer avec un delay et une duration à 0,1. La propriété Loop est cochée afin que cette animation boucle sans arrêt.

Nous allons implémenter son événement OnProgress afin de faire afficher les scènes en fonction de la valeur de la variable jeu.

procedure TfPrincipale.aniBouclePrincipaleProcess(Sender: TObject);
begin
case jeu of
1: // Intro
begin
txtGagne.Visible := false;
txtGagne.RotationAngle.x := 0;
palet.Visible := false;
if dmyPrincipal.Scale.X < 0.5 then
begin
// Zoom sur la scène 3D en plus de la rotation
dmyPrincipal.Scale.X := dmyPrincipal.Scale.X +0.01;
dmyPrincipal.Scale.y := dmyPrincipal.Scale.y +0.01;
dmyPrincipal.Scale.z := dmyPrincipal.Scale.z +0.01;
end;
// Rotation du dmyPrincipal pour faire tourner toute la scène 3D
dmyPrincipal.RotationAngle.Z := dmyPrincipal.RotationAngle.Z +2;
end;
2: // Jeu
begin
txtGagne.Visible := false;
txtGagne.RotationAngle.x := 0;
dmyPrincipal.RotationAngle.Z := 0;
dmyPalet.Visible := true;
palet.Visible := true;
// On enclenche le jeu
DeplacementPalet;
// Et l'intelligence artificielle
CPU;
end;
3: // Ordi gagne
begin
palet.Visible := false;
txtGagne.Visible := true;
txtGagne.text := 'Perdu :(';
txtGagne.RotationAngle.x := txtGagne.RotationAngle.x +3;
end;
4: // Joueur gagne
begin
palet.Visible := false;
txtGagne.Visible := true;
txtGagne.text := 'Gagné :)';
txtGagne.RotationAngle.x := txtGagne.RotationAngle.x +3;
end;
end;
end;

Pour les cas où l'ordinateur a gagné (la variable jeu = 3), on masque le palet et on affiche le message « Perdu ». En bonus, on applique à l'objet txtGagne une rotation autour de l'axe X.

On fait de même lorsque le joueur humain gagne. La seule différence étant le libellé affiché bien sûr.

Le plus dur est fait. Il reste quelques petites choses pour assembler et rendre fonctionnel le tout.

Tout d'abord, nous allons renseigner l'événement OnExecute de l'action actJouer. Cette action va servir à démarrer une partie.

procedure TfPrincipale.actJouerExecute(Sender: TObject);
begin
txtGagne.Visible := false;
txtGagne.RotationAngle.x := 0;
scoreJoueur := 0;
scoreCPU := 0;
vitesseInitiale := 50;
jeu := 2;
Service;
end;

Rien de bien compliqué, on initialise les scores, la vitesse initiale du palet, la phase de jeu qui sera à afficher et on fait appel à la procédure Service vue précédemment.

Ensuite, on crée une procédure nommée RetourMenu qui permettra de revenir à l'écran d'accueil lorsque la partie est terminée (que le joueur humain ait gagné ou non).

procedure TfPrincipale.RetourMenu;
begin
if (jeu = 3) or (jeu = 4) then jeu := 1;
end;

Nous appellerons cette procédure lorsque l'utilisateur clique sur la table de jeu. Pour ce faire, on implémente les événements OnClick des objets base, bordDroit et bordGauche pour y appeler RetourMenu.

Nous avons également placé un TTrackBar dans le panel. Celui-ci permet de régler la difficulté du jeu. La difficulté va consister à réduire ou augmenter le temps de réaction du palet géré par l'ordinateur et la vitesse du palet. On implémente l'événement OnTracking :

procedure TfPrincipale.tbDifficulteTracking(Sender: TObject);
begin
tempsReactionIA := tbDifficulte.Value/10;
end;

Enfin, implémentons les animations apparition et disparition. Sur l'événement OnFinish de l'animation apparition, on démarre la boucle principale du jeu.

procedure TfPrincipale.apparitionFinish(Sender: TObject);
begin
aniBouclePrincipale.Start;  // Démarre la boucle principale du jeu
end;

L'événement OnFinish de l'animation disparition va servir à relancer un service.

procedure TfPrincipale.disparitionFinish(Sender: TObject);
begin
Service;
end;

Pour terminer, on ajoute le déclenchement de l'animation disparition lorsque la boucle principale se termine (événement OnFinish de l'animation aniBouclePrincipale).

procedure TfPrincipale.aniBouclePrincipaleFinish(Sender: TObject);
begin
disparition.Start;
end;

Ça y est notre petit jeu est opérationnel. Évidemment de multiples améliorations sont possibles. Les sources fournis avec ce tutoriel sont un peu plus complets et disposent des améliorations décrites ci-après.

IV - Améliorations

Le code source fourni avec ce tutoriel est plus complet et propose quelques améliorations au code présenté précédemment.

IV-A - Applications de textures

Trois objets TlightMaterialSource sont utilisés pour gérer les couleurs des raquettes, du palet et de la table de jeu. La table dispose également d'une texture Bitmap pour un effet visuel plus joli.

IV-B - Application d'un style

La partie IHM en 2 dimensions (le panel présent en bas de l'écran) utilise un objet TstyleBook pour appliquer un style à la fenêtre de l'application.

IV-C - Affichage d'un écran d'aide

Je ne détaillerai pas cette partie, car elle est très simple. Elle consiste à afficher un memo qui présente la règle du jeu.

V - Remerciements

Je me suis servi souvent du site developpez.com pour résoudre des problèmes que je rencontrais, quel que soit le langage, alors j'ai voulu en retour partager un bout de code. J'espère que ça pourra être utile à certaines/certains ou, pourquoi pas, donner envie de se mettre à Delphi.

Je remercie l'équipe de Developpez.com pour leur travail ainsi que les relecteurs qui ont participé à ce tutoriel.