Tutoriel Gdi+ : programmez un jeu de Pacman complet en VBA

Thierry GASPERMENT

Comment développer un jeu de Pacman complet en VBA avec Gdi+.

Commentez cet article : Commentez

- Titre : Tutoriel Gdi+ : programmez un jeu de Pacman complet en VBA

- Auteur : Thierry GASPERMENT

- Parution : 8 décembre 2008

- Mise à jour : 8 avril 2018

- 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 ® 2013 Thierry GASPERMENT. 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. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. Outils utilisés

- II-A. Graphismes

- II-B. Sons

- II-C. Commandes

- II-D. Minuterie

- III. Spécifications du jeu

- III-A. Déroulement du jeu

- III-B. L'écran de menu

- III-C. L'écran de jeu

- III-D. L'écran partie gagnée

- III-E. L'écran partie perdue

- III-F. L'écran des scores

- III-G. L'écran des crédits

- III-H. L'écran des options

- III-I. Schemas des écrans

- IV. Création du projet

- V. Création des images et sons

- V-A. Les images

- V-B. Les sons

- VI. Création du tableau de jeu

- VII. Préparation du projet VBA

- VII-A. Le graphisme

- VII-B. Les sons

- VII-C. Les commandes

- VII-D. La minuterie

- VII-E. Le formulaire principal

- VIII. Préparation des modules objets

- VIII-A. Schemas des modules objets

- VIII-B. Création des modules objets

- IX. Préparation du premier écran

- IX-A. L'interface d'écran clScreen

- IX-B. Préparation du premier écran : le menu clScreenMenu

- X. Création de la boucle principale dans l'objet Jeu

- XI. Lancement du jeu dans le formulaire

- XII. Création du menu : affichage et choix dans le menu

- XII-A. Minuterie

- XII-B. Affichage du menu

- XII-C. Encadrement de l'entrée de menu sélectionnée

- XII-D. Sélection d'une option de menu

- XIII. Gestion des options de jeu

- XIII-A. Stockage des options

- XIII-B. Les variables des options

- XIII-C. Chargement des options

- XIII-D. Création de l'écran des options de jeu

- XIII-E. Affichage des options de jeu

- XIII-F. Modification des options de jeu

- XIII-G. Validation ou annulation des options de jeu

- XIII-H. Navigation dans l'écran de menu avec le clavier ou le joystick

- XIV. Bilan à mi-parcours

- XV. Développement de l'écran de jeu

- XV-A. Création de l'écran de jeu

- XV-B. Chargement du tableau de jeu

- XV-C. Affichage du tableau de jeu

- XV-D. Chargement des ressources

- XV-E. Création de du module objet clSprite

- XV-F. Gestion du Pacman

- XV-G. Gestion des pastilles

- XV-H. Gestion des fantômes

- XV-I. Super-pastille : fantômes effrayés

- XV-J. Gestion des collisions entre les sprites

- XV-K. Retour au centre des fantômes

- XV-L. Fin du jeu

- XVI. Développement de l'écran de jeu perdu

- XVII. Développement de l'écran de jeu gagné

- XVIII. Développement de l'écran des scores

- XIX. Développement de l'écran des crédits

- XX. Lancement du jeu à l'ouverture d'Excel

- XXI. Améliorations possibles

- XXII. Conclusion

- XXIII. Remerciements

- XXIV. Téléchargements

I - Introduction

Nous allons, au cours de ce tutoriel, développer un jeu de PacMan en VBA avec Excel.

Toute autre application Office est utilisable moyennement quelques petites adaptations

Pour une première approche de Gdi+ en VBA, consultez la page de la classe clGdiplus.

II - Outils utilisés

On s'en doute, Excel n'est pas prévu pour développer un jeu !

On va donc devoir utiliser des fonctions externes.

Tous les outils que nous allons utiliser pour créer et gérer les ressources du jeu (images, sons…) sont gratuits.

Il vous faut tout de même une version d'Excel 2000 ou supérieure pour dérouler ce tutoriel.

II-A - Graphismes

Pour gérer l'affichage on utilisera une classe VBA spécifique clGdiplus qui s'appuie sur la bibliothèque Gdi+ de Microsoft.

Consultez la documentation et les tutoriels de la classe clGdiplus

Gdi+ est intégrée à partir de Windows XP.
On pourra, pour que le programme fonctionne sur toutes les versions, fournir les fichiers redistribuables de Gdi+, que vous pouvez télécharger en suivant ce lien.

Les images seront dessinées avec msPaint.

II-B - Sons

Pour gérer les sons on utilisera l'API multimédia Windows (bibliothèque winmm.dll intégrée à Windows).

Les sons seront enregistrés avec le magnétophone de Windows.

II-C - Commandes

Les commandes claviers seront détectées avec la fonction GetAsyncKeyState de la bibliothèque user32.dll.

Les commandes du JoyPad seront détectées avec l'API multimédia Windows (bibliothèque winmm.dll).

II-D - Minuterie

Pour permettre un réglage précis de la vitesse du jeu, on utilisera encore une fois l'API multimédia Windows (bibliothèque winmm.dll), à l'aide de la classe clGdiplus qui implémente les fonctions nécessaires.

III - Spécifications du jeu

Commençons par définir les fonctionnalités du jeu.

III-A - Déroulement du jeu

C'est un PacMan, les règles sont bien connues.

Le PacMan se déplace avec les touches ou avec un JoyPad et mange des pastilles.

Les fantômes ont deux états :

- À l'état « normal » ils sont en couleurs et se déplacent de manière à se rapprocher du PacMan.
Si le PacMan rencontre un fantôme en état « normal », la partie est perdue.

- Il y a une super-pastille à chaque coin de l'écran. Si le PacMan en mange une, les fantômes passent à l'état « effrayé » pendant une durée déterminée.
Dans cet état les fantômes sont bleus et ralentis.
Ils se déplacent de manière à s'éloigner du PacMan.
Si le PacMan rencontre un fantôme en état « effrayé », le fantôme est mangé.
Ce fantôme retourne alors au centre (on ne voit que ses yeux durant ce retour) et redevient à l'état « normal ».

La partie est gagnée lorsque le PacMan a mangé toutes les pastilles du jeu.

III-B - L'écran de menu

Le menu est le premier écran affiché au lancement du programme.

Il propose les options suivantes :

- Commencer le jeu => lance l'écran de jeu.

- Options => affiche l'écran des options.

- Scores => affiche l'écran des scores.

- Crédits => affiche l'écran des crédits.

- Quitter => ferme le programme.

III-C - L'écran de jeu

Affiche le jeu qui répond aux fonctionnalités énoncés dans le paragraphe précédent « Déroulement du jeu ».

Si le PacMan est mangé => affichage de l'écran partie perdue.

Si le PacMan a mangé toutes les pastilles => affichage de l'écran partie gagnée.

III-D - L'écran partie gagnée

Affichage d'un texte de fin de partie et du score.
Saisie éventuelle du nom du joueur pour une entrée dans les scores.

Enchaîne sur l'écran des scores.

III-E - L'écran partie perdue

Affichage d'un texte de fin de partie et du score.
Saisie éventuelle du nom du joueur pour une entrée dans les scores.

Enchaîne sur l'écran des scores.

III-F - L'écran des scores

Affichage des 10 meilleurs scores.

Enchaîne sur l'écran des crédits si fin de partie ou retour au menu si l'écran a été demandé depuis le menu.

III-G - L'écran des crédits

Affichage d'information au sujet du développement du jeu.
Ces informations défilent verticalement.

Enchaîne sur l'écran de menu.

III-H - L'écran des options

Paramétrage du jeu :

- nombre de fantômes à l'écran ;

- choix des touches de déplacement.

Enchaîne sur l'écran de menu.

III-I - Schemas des écrans

Voici donc le schéma d'enchaînement des écrans :

IV - Création du projet

Créez un nouveau dossier sur votre PC pour y héberger le jeu.
Créez ensuite un nouveau fichier Excel que vous placez dans ce dossier.
Les ressources du jeu (images, sons, fichier de configuration, fichiers de gdi+) seront placées dans des sous-dossiers.

V - Création des images et sons

Avant de commencer le développement en VBA, nous allons créer les ressources nécessaires au jeu.

V-A - Les images

Les images de ce tutoriel sont créées avec Microsoft ® Paint.
Leur taille est de 20px * 20px.
Le format d'enregistrement des images est le format PNG : petite taille et peu d'altération de l'image.
La couleur noire est utilisée comme couleur de transparence.
On aurait pû utiliser la transparence gérée par le format PNG, mais comme je crée les images avec msPaint qui ne la gère pas, la transparence sera gérée dans le code.

Les images du jeu sont stockées dans un sous-dossier images

Les fantômes :

Il nous faut trois images de fantômes :

- un fantôme pour l'état « normal », nommé fantome.png  ;

- un fantôme pour l'état « effrayé », nommé fantomeeffraye.png  ;

- une paire d'yeux pour le retour au centre, nommée yeux.png .

Le Pacman :

Il nous faut treize images de PacMan car il sera animé en fonction de son déplacement :

- un PacMan immobile, nommé pac0.png  ;

- trois PacMans pour les déplacements vers la droite, nommés pacd1.png, pacd2.png et pacd3.png  ;

- trois PacMans pour les déplacements vers la gauche, nommés pacg1.png, pacg2.png et pacg3.png  ;

- trois PacMans pour les déplacements vers le haut, nommés pach1.png, pach2.png et pach3.png  ;

- trois PacMans pour les déplacements vers le bas, nommés pacb1.png, pacb2.png et pacb3.png .

V-B - Les sons

Les sons de ce tutoriel sont créés avec le magnétophone de Windows.
Menu Démarrer => Tous les programmes => Accessoires => Divertissement => Magnétophone.

Enregistrez un son (il vous faut bien sûr un microphone), puis augmentez la vitesse dans le menu Effets => Augmenter la vitesse.
Augmentez la vitesse permet de rendre votre voix méconnaissable et donner un air « dessin animé » au son.
Les sons sont enregistrés au format WAV et stockés dans un sous-dossier sons

Trois sons sont nécessaires à ce jeu :

- un son pour chaque pastille mangée, nommé blip.wav ;

- un son pour les fantômes mangés, nommé aie.wav ;

- un son pour le PacMan mangé, nommé aouch.wav ;

Pensez à réduire le volume du son de pastille mangée car il devient agaçant s'il est trop fort.

VI - Création du tableau de jeu

Le jeu fonctionnant sur Excel, servons nous des feuilles Excel pour y stocker le contenu de chaque case du tableau.

Créez une nouvelle feuille nommée jeu (ou renommez une feuille existante).
Le tableau de jeu est composé de 28 colonnes et 30 lignes.
Définissez le contenu de chaque case par un symbole sur la feuille :

- X pour un mur ;

- pour une pastille ;

- pour une super-pastille ;

- P pour un passage (2 cases « passage » dans le tableau) ;

- _ pour une porte (c'est l'endroit d'où sortent les fantômes, on ne peut pas y retourner).

Remarque : il n'y a qu'un seul tableau pour le jeu.

Nous définissons ensuite un tableau spécial pour le retour au centre des fantômes.
Ce tableau défini pour chaque fantôme la direction à suivre pour retourner au centre lorsqu'il est mangé.

Dans une feuille nommée chemin, définissez la direction à suivre pour chaque case :

- d pour droite ;

- g pour gauche ;

- h pour haut ;

- b pour bas.

Lorsqu'un fantôme sera mangé, nous utiliserons ce tableau pour connaître le déplacement du fantôme sans avoir besoin d'un algorithme évolué du style pathfinding A*.

VII - Préparation du projet VBA

La gestion des graphismes, des sons et des commandes nécessite l'ajout de code VBA pour déclarer les fonctions de l'API Windows.

Préparons dans ce chapitre le projet VBA en important les modules VBA nécessaires.

Pour vous placer dans le projet VBA, ouvrez le fichier Excel et tapez ALT + F11.
Ou par le menu : Outils => Macro => Visual Basic Editor.

VII-A - Le graphisme

Nous ciblons une version de Windows au moins égale à XP.
Dans le cas contraire, téléchargez les fichiers redistribuables de Gdi+ et placez les fichiers dézippés dans le même répertoire que le classeur Excel.

Nous utilisons le module de classe clGdiplus pour faciliter l'utilisation de gdi+ en VBA.

Téléchargez le module clGdiplus au format texte.
Dans l'éditeur VBA :

1. Dans le menu : Fichier => Importer un fichier…

2. Choisir le fichier clGdiPlus.cls téléchargé.

3. Modifiez la constante de compilation pour une utilisation avec Excel.
Remplacez :

#Const AppName = "A"

Par :

#Const AppName = "E"

4. La référence à Microsoft Forms 2.0 Object Library est requise.
Vous pouvez ajouter cette référence dans le menu Outils => Références….
Sinon si vous ne la trouvez pas, cette référence sera ajoutée lors de la création d'un UserForm.

5. Compiler pour vérifier que tout est correct (menu Débogage => Compiler).

VII-B - Les sons

Pour jouer des sons, on utilise la fonction mciSendString de la bibliothèque winmm.dll
Créez un module de classe nommé clSound contenant le code suivant :

Code du module de son

'***************************************************************************************
'*                             CLASSE POUR SON                                         *
'***************************************************************************************
Option Explicit

#If VBA7 Then
Private Declare PtrSafe Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" _
(ByVal lpstrCommand As String, ByVal lpstrReturnString As String, _
ByVal uReturnLength As Long, ByVal hwndCallback As LongPtr) As Long
Private Declare PtrSafe Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
(ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long
#Else
Private Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" _
(ByVal lpstrCommand As String, ByVal lpstrReturnString As String, _
ByVal uReturnLength As Long, ByVal hwndCallback As Long) As Long
Private Declare Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
(ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long
#End If

'------------------------------------------------
' Ouvre un son
'------------------------------------------------
Public Sub OpenSound(pPath As String, pName As String)
Dim lPath As String
' Récupère le chemin court
lPath = pPath
lPath = Left(lPath, GetShortPathName(pPath, lPath, Len(pPath)))
' Ouvre le fichier son sous l'alias pName
mciSendString "Open " & lPath & " alias " & pName, vbNullString, 0&, 0&
End Sub

'------------------------------------------------
' Joue un son
'------------------------------------------------
Public Sub PlaySound(pName As String)
' Joue le son depuis la début
mciSendString "Play " & pName & " From 0", vbNullString, 0&, 0&
End Sub

'------------------------------------------------
' Stoppe un son
'------------------------------------------------
Public Sub StopSound(pName As String)
' Stoppe le son
mciSendString "Stop " & pName, vbNullString, 0&, 0&
End Sub

'------------------------------------------------
' Ferme un son
'------------------------------------------------
Public Sub CloseSound(pName As String)
' Stoppe le son
mciSendString "Close " & pName, vbNullString, 0&, 0&
End Sub

'------------------------------------------------
' Initialisation de la classe
'------------------------------------------------
Private Sub Class_Initialize()
'
End Sub

'------------------------------------------------
' Fermeture de la classe
'------------------------------------------------
Private Sub Class_Terminate()
mciSendString "Close All", vbNullString, 0&, 0&
End Sub

Ce module nous permettera de charger les sons en mémoire avec la fonction OpenSound, puis de les jouer avec la fonction PlaySound.

VII-C - Les commandes

Créez un nouveau module de classe nommé clCommand contenant le code suivant :

Code du module de commandes

'***************************************************************************************
'*                             CLASSE POUR COMMANDES                                   *
'***************************************************************************************
Option Explicit

'---------------------------------------------------------------------------------------
' Déclarations pour Joystick
'---------------------------------------------------------------------------------------
#If VBA7 Then
Private Declare PtrSafe Function joyGetNumDevs Lib "winmm.dll" Alias "joyGetNumDev" () As Long
Private Declare PtrSafe Function joyGetPos Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFO) As Long
Private Declare PtrSafe Function joyGetDevCaps Lib "winmm.dll" Alias "joyGetDevCapsA" _
(ByVal id As LongPtr, lpCaps As JOYCAPS, ByVal uSize As Long) As Long
Private Declare PtrSafe Function joyGetPosEx Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFOEX) As Long
#Else
Private Declare Function joyGetNumDevs Lib "winmm.dll" () As Long
Private Declare Function joyGetPos Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFO) As Long
Private Declare Function joyGetDevCaps Lib "winmm.dll" Alias "joyGetDevCapsA" _
(ByVal id As Long, lpCaps As JOYCAPS, ByVal uSize As Long) As Long
Private Declare Function joyGetPosEx Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFOEX) As Long
#End If
Private Type JOYCAPS
wMid As Integer
wPid As Integer
szPname As String * 32
wXmin As Long
wXmax As Long
wYmin As Long
wYmax As Long
wZmin As Long
wZmax As Long
wNumButtons As Long
wPeriodMin As Long
wPeriodMax As Long
End Type
Private Type JOYINFOEX
dwSize As Long
dwFlags As Long
dwXpos As Long
dwYpos As Long
dwZpos As Long
dwRpos As Long
dwUpos As Long
dwVpos As Long
dwButtons As Long
dwButtonNumber As Long
dwPOV As Long
dwReserved1 As Long
dwReserved2 As Long
End Type
Private Type JOYINFO
X As Long
Y As Long
Z As Long
Buttons As Long
End Type
Private Const JOY_BUTTON1 = &H1
Private Const JOY_BUTTON2 = &H2
Private Const JOY_BUTTON3 = &H4
Private Const JOY_BUTTON4 = &H8
Private Const JOYERR_BASE = 160
Private Const JOYERR_NOERROR = (0)
Private Const JOYERR_NOCANDO = (JOYERR_BASE + 6)
Private Const JOYERR_PARMS = (JOYERR_BASE + 5)
Private Const JOYERR_UNPLUGGED = (JOYERR_BASE + 7)
Private Const JOY_RETURNX As Long = &H1&
Private Const JOY_RETURNY As Long = &H2&
Private Const JOY_RETURNZ As Long = &H4&
Private Const JOY_RETURNR As Long = &H8&
Private Const JOY_RETURNU As Long = &H10
Private Const JOY_RETURNV As Long = &H20
Private Const JOY_RETURNPOV As Long = &H40&
Private Const JOY_RETURNBUTTONS As Long = &H80&
Private Const JOY_RETURNCENTERED As Long = &H400&
Private Const JOY_RETURNALL As Long = (JOY_RETURNX Or JOY_RETURNY Or JOY_RETURNZ Or JOY_RETURNR _
Or JOY_RETURNU Or JOY_RETURNV Or JOY_RETURNPOV Or JOY_RETURNBUTTONS)

'---------------------------------------------------------------------------------------
' Déclarations pour clavier
' http://msdn2.microsoft.com/en-us/library/ms646293(VS.85).aspx
'---------------------------------------------------------------------------------------
#If VBA7 Then
Private Declare PtrSafe Function GetKeyboardState Lib "user32" (pbKeyState As Byte) As Long
Private Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#Else
Private Declare Function GetKeyboardState Lib "user32" (lpKeyState As Byte) As Long
Private Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Long
#End If
'---------------------------------------------------------------------------------------
' Variables
'---------------------------------------------------------------------------------------
' Numéro du joypad (0 par défaut)
#If VBA7 Then
Private gJoyPadNum As LongPtr
#Else
Private gJoyPadNum As Long
#End If
'---------------------------------------------------------------------------------------
' Nom du joypad
' Propriété en lecture
'---------------------------------------------------------------------------------------
Public Property Get JoyPadName() As String
Dim lCaps As JOYCAPS
Dim lInfo As JOYINFOEX
' Test si joypad OK
lInfo.dwSize = Len(lInfo)
If joyGetPosEx(gJoyPadNum, lInfo) <> JOYERR_NOERROR Then
JoyPadName = ""
Exit Property
End If
' Lecture nom du joypad
joyGetDevCaps gJoyPadNum, lCaps, Len(lCaps)
JoyPadName = Left(lCaps.szPname, InStr(lCaps.szPname, vbNullChar) - 1)
End Property

'---------------------------------------------------------------------------------------
' Nombre de boutons du joypad
' Propriété en lecture
'---------------------------------------------------------------------------------------
Public Property Get JoyPadButtons() As Long
Dim lCaps As JOYCAPS
Dim lInfo As JOYINFOEX
' Test si joypad OK
lInfo.dwSize = Len(lInfo)
If joyGetPosEx(gJoyPadNum, lInfo) <> JOYERR_NOERROR Then
JoyPadButtons = 0
Exit Property
End If
' Lecture nombre de boutons
joyGetDevCaps gJoyPadNum, lCaps, Len(lCaps)
JoyPadButtons = lCaps.wNumButtons
End Property

'---------------------------------------------------------------------------------------
' Numéro du joypad
' Propriété en lecture/écriture
'---------------------------------------------------------------------------------------
Public Property Get JoyPadNum() As Long
JoyPadNum = gJoyPadNum
End Property
Public Property Let JoyPadNum(pJoyPadNum As Long)
gJoyPadNum = pJoyPadNum
End Property

'---------------------------------------------------------------------------------------
' Teste quelle touche est appuyée
' Renvoi un seul code de touche même si plusieurs touche sont appuyées
'---------------------------------------------------------------------------------------
Public Function KeyPressGetCode() As Long
Dim lTouches(0 To 255) As Byte
Dim lCpt As Long
GetKeyboardState lTouches(0)
For lCpt = 0 To 255
If (lTouches(lCpt) And &H80) = &H80 Then
KeyPressGetCode = lCpt
Exit For
End If
Next
End Function

'---------------------------------------------------------------------------------------
' Test d'une touche clavier par son code
' Renvoi True si la touche est appuyée
' Si pTime est renseigné, test si la touche a été appuyée pTime ms après
'    le dernier appui testé (quelque soit le code)
'---------------------------------------------------------------------------------------
Public Function TestKey(pKey As Long, Optional pTime As Long) As Boolean
Static sTime  As Double
TestKey = (GetAsyncKeyState(pKey) And &H8000)
If TestKey Then
If pTime <> 0 Then
If (Timer - sTime) < (pTime / 1000) Then
TestKey = False
Else
sTime = Timer
End If
Else
sTime = Timer
End If
End If
End Function

'---------------------------------------------------------------------------------------
' Test d'une direction joypad
' Renvoi True dans le paramètre où la touche de direction est appuyée
' Si pTime est renseigné, test si la touche a été appuyée pTime ms après
'    le dernier appui testé (quelque soit le code)
'---------------------------------------------------------------------------------------
Public Sub TestJoypadDir(Optional pLeft As Boolean, Optional pRight As Boolean, _
Optional pUp As Boolean, Optional pDown As Boolean, Optional pTime As Long)
Static sTime  As Double
Dim lInfo As JOYINFOEX ' Structure pour lecture de l'état du jostick
Dim lJoyPresent As Boolean ' Variable pour test si joystick présent
lInfo.dwSize = Len(lInfo) ' Il est nécessaire d'initialiser cette variable
lInfo.dwFlags = JOY_RETURNX Or JOY_RETURNY  ' On lit l'état des axes X et Y
' Lecture des infos et flag si joystick présent
lJoyPresent = (joyGetPosEx(gJoyPadNum, lInfo) = JOYERR_NOERROR)
' Etat du joypad
pLeft = (lJoyPresent And lInfo.dwXpos = 0)
pRight = (lJoyPresent And lInfo.dwXpos = 65535)
pUp = (lJoyPresent And lInfo.dwYpos = 0)
pDown = (lJoyPresent And lInfo.dwYpos = 65535)
If pLeft Or pRight Or pUp Or pDown Then
If pTime <> 0 Then
If (Timer - sTime) < (pTime / 1000) Then
pLeft = False: pRight = False: pUp = False: pDown = False
Else
sTime = Timer
End If
Else
sTime = Timer
End If
End If
End Sub

'---------------------------------------------------------------------------------------
' Test d'un bouton du joypad
' Renvoi True si le bouton n° NbButton est appuyé
' Si pTime est renseigné, test si la touche a été appuyée pTime ms après
'    le dernier appui testé (quelque soit le code)
'---------------------------------------------------------------------------------------
Public Function TestJoypadButton(pNbButton As Long, Optional pTime As Long) As Boolean
Static sTime  As Double
Dim lInfo As JOYINFOEX ' Structure pour lecture de l'état du jostick
Dim lJoyPresent As Boolean ' Variable pour test si joystick présent
lInfo.dwSize = Len(lInfo) ' Il est nécessaire d'initialiser cette variable
lInfo.dwFlags = JOY_RETURNBUTTONS  ' On lit l'état des boutons
' Lecture des infos et flag si joystick présent
lJoyPresent = (joyGetPosEx(gJoyPadNum, lInfo) = JOYERR_NOERROR)
' Etat du bouton du joypad
TestJoypadButton = (lJoyPresent And (lInfo.dwButtons And 2 ^ pNbButton) = 2 ^ pNbButton)
If TestJoypadButton Then
If pTime <> 0 Then
If (Timer - sTime) < (pTime / 1000) Then
TestJoypadButton = False
Else
sTime = Timer
End If
Else
sTime = Timer
End If
End If
End Function

Ce module contient les fonctions nécessaires à la détection des commandes (clavier ou joypad).

VII-D - La minuterie

Le module clGdiplus intègre déjà une procédure Wait pour gérer une minuterie.

VII-E - Le formulaire principal

Créez un nouveau formulaire : Insertion => Userform.
Renommez-le en FormGame.

Si nécessaire, affichez la boîte à outils (Affichage => Boîte à outils).
Choisissez le contrôle image dans la boîte à outils et placez une image sur le formulaire.
Renommez cette image (propriété name) pour lui donner le nom img.
Le jeu s'affichera dans cette image, donnez lui une taille raisonnable.

VIII - Préparation des modules objets

Nous allons essayer d'utiliser au mieux les possibilités de VBA en matière de programmation orientée objet.
VBA est assez limité comparé à d'autres langages mais permet tout de même de créer des modules de classe et d'utiliser des interfaces grâce à l'instruction Implements.

Un maximum de code sera donc intégré à des modules de classe de manière à ne pas surcharger le code du formulaire principal.
Le code sera alors plus lisible et plus facile à modifier.

On bénéficie également dans les modules de classe VBA de deux événements : Initialize et Terminate que nous pouvons utiliser pour exécuter des actions à la création et à la destruction d'un objet.

VIII-A - Schemas des modules objets

Voici comment nos objets vont s'imbriquer :

- FormGame est le formulaire principal.

Il héberge le jeu (notons qu'un formulaire est un objet).
Il contient un objet de type clGame.

- clGame est la classe pour l'objet jeu.

Cet objet est contenu dans le formulaire et contient :

- Un objet de type clCommand pour gérer les commandes.

Cet objet sera transmis entre les écrans afin de n'avoir qu'une seule instance de cet objet pour tout le jeu.
Cela nous permet de conserver dans cet objet l'heure de dernier appui de touche détecté (utilisation du paramètre pTime que nous verrons plus loin).

- un objet de type clSharedData pour y stocker les données communes à tous les écrans.

Cela nous permet de partager par exemple le score entre l'écran de jeu et les écrans de fin de jeu.

- un objet de type clScreen qui est l'écran en cours d'affichage.

Chaque écran sera géré dans un module de classe qui hérite de l'interface de ce module clScreen.

On aura ainsi dans l'objet jeu un seul objet de type clScreen.

Chaque écran peut contenir :

- un objet de type clGdiplus pour gérer le graphisme.

- un objet de type clSound pour gérer le son.

- un objet de type clCommand transmis par l'objet jeu.

- un objet de type clSharedData transmis par l'objet jeu.

Ci-dessous le schéma qui résume tout ceci :

Les différents écrans n'apparaissent pas sur ce schéma.
Chaque écran est indépendant et vient prendre la place de clScreen dans le schéma au fur et à mesure que l'on change d'écran.

VIII-B - Création des modules objets

Commençons par créer des nouveaux modules de classe vierges pour chacun des objets dont nous avons besoin.
En en-tête de chaque module on ajoute un commentaire d'information et, si nécessaire, l'instruction Option Explicit pour forcerla déclaration des variables.

module de jeu clGame

'***************************************************************************************
'*                                  OBJET JEU
'***************************************************************************************
Option Explicit

module pour données partagées clSharedData

'***************************************************************************************
'*                 OBJET POUR DONNEES PARTAGEES ENTRE LES ECRANS
'***************************************************************************************
Option Explicit

module pour interface d'écran clScreen

'***************************************************************************************
'*                             OBJET D'INTERFACE POUR ECRAN
'***************************************************************************************
Option Explicit

module pour écran de jeu clScreenGame

'***************************************************************************************
'*                             OBJET ECRAN DE JEU
'***************************************************************************************
Option Explicit

module pour écran de menu clScreenMenu

'***************************************************************************************
'*                             OBJET ECRAN DE MENU
'***************************************************************************************
Option Explicit

module pour écran de jeu perdu clScreenGameOver

'***************************************************************************************
'*                             OBJET ECRAN DE JEU PERDU
'***************************************************************************************
Option Explicit

module pour écran de jeu gagné clScreenGameEnd

'***************************************************************************************
'*                             OBJET ECRAN DE JEU GAGNE
'***************************************************************************************
Option Explicit

module pour écran des options clScreenOptions

'***************************************************************************************
'*                             OBJET ECRAN D'OPTIONS
'***************************************************************************************
Option Explicit

module pour écran des crédits clScreenCredits

'***************************************************************************************
'*                             OBJET ECRAN DES CREDITS
'***************************************************************************************
Option Explicit

module pour écran des scores clScreenScores

'***************************************************************************************
'*                             OBJET ECRAN DES SCORES
'***************************************************************************************
Option Explicit

IX - Préparation du premier écran

Tout est prêt à ce stade pour commencer la programmation des écrans.

Chaque écran sera géré dans un module de classe indépendant : clScreenMenu, clScreenGame…

Chacun de ces modules héritera de l'interface définie dans le module clScreen.
Cet héritage se traduit dans le code avec l'instruction Implements.
On pourra alors dans l'objet jeu faire appel à un unique objet de type clScreen pour faire appel à l'écran en cours.

Afin de pousuivre avec la création de ce module d'interface, écrivons l'algorithme de déroulement du jeu.
On va ainsi mettre en évidence les propriétés et fonctions nécessaires à un écran.

Algorithme de déroulement du jeu dans l'objet Jeu

oScreen = Nouvel écran de menu clScreenMenu
oCommand = Nouvel objet de commandes clCommand
oSharedData = Nouvel objet de données clSharedData
Affecte oCommand et oSharedData à oScreen
Début boucle de jeu
Exécute mise à jour des commandes dans oScreen
Exécute mise à jour de l'écran dans oScreen
Exécute mise à jour de l'affichage dans oScreen
Exécute la fonction d'attente dans oScreen
Si oScreen demande un changement d'écran
oScreen = Nouvel écran fonction de l'écran suivant dans oScreen
Affecte oCommand et oSharedData à oScreen
Fin Si
Fin boucle de jeu

IX-A - L'interface d'écran clScreen

Dans ce paragraphe nous développons l'interface des écrans dans le module de classe clScreen.
Dans un module d'interface, nous écrivons uniquement un squelette.
Les fonctions, procédures ou propriétés ne seront pas exécutées dans ce module mais dans les modules en héritant.
Nous n'écrivons donc qu'un commentaire à intérieur.

Les objets oCommand et oSharedData étant transmis à chaque écran par l'objet Jeu, il est nécessaire de créer une propriété publique en écriture.

Propriétés pour accès en écriture aux objets partagés

'------------------------------------------------------------------------
' Les objets Command et Shared sont en écriture public car ils seront
' transmis par l'objet jeu d'écran en écran
'------------------------------------------------------------------------
Public Property Set Command(poCommand As ClCommand)
'
End Property
Public Property Set SharedData(poSharedData As ClSharedData)
'
End Property

Occupons nous maintenant des variables.

Le nom de l'écran suivant NextScreen doit pouvoir être lu ou écrit de n'importe où.
En effet on doit pouvoir accéder au nom de l'écran suivant :

- en lecture pour lire le nom de l'écran suivant lors d'un changement d'écran ;

- en écriture pour définir un écran suivant différent de celui par défaut.

Le flag de demande de changement d'écran ChangeScreen doit être en lecture uniquement
En effet on doit pouvoir tester la demande de changement d'écran depuis l'objet jeu.

Propriétés pour accès aux variables

'------------------------------------------------------------------------
' Propriété pour nom de l'écran suivant
' Public car doit être accessible de partout
'------------------------------------------------------------------------
Public Property Get NextScreen() As String
'
End Property
Public Property Let NextScreen(pNextScreen As String)
'
End Property

'------------------------------------------------------------------------
' Propriété pour flag de changement d'écran
' Lecture Public
'------------------------------------------------------------------------
Public Property Get ChangeScreen() As Boolean
'
End Property

On note également dans l'algorithme que l'objet jeu appelle des fonctions de :

- mise à jour des commandes pour tester l'appui de touches ;

- mise à jour de l'écran pour gérer l'écran ;

- mise à jour de l'affichage pour afficher sur le formulaire ;

- minuterie pour gérer le framerate.

On ajoute donc une fonction publique pour chacune de ces étapes.

Fonctions pour chaque étape de l'algorithme

'------------------------------------------------------------------------
' Functions à gérer dans les écrans
'------------------------------------------------------------------------
Public Function UpdateScreen() As Boolean
' Mise à jour de l'écran
End Function

Public Function UpdateCommand() As Boolean
' Mise à jour des commandes
End Function

Public Function DisplayScreen() As Boolean
' Affichage de l'écran
End Function

Public Function ScreenWait() As Boolean
' Minuterie d'attente
End Function

Le module d'interface est prêt, nous pouvons maintenant créer le premier écran.

IX-B - Préparation du premier écran : le menu clScreenMenu

Le premier menu que nous allons créer est l'écran de menu.
Tout le code concernant le menu est écrit dans le module de classe clScreenMenu.

Ce module héritant de l'interface clScreen, la première étape est d'implémenter cette interface.

Déclaration de l'interface dans clScreenMenu

'***************************************************************************************
'*                             OBJET ECRAN DE MENU
'***************************************************************************************
Option Explicit

' Implémente l'interface de base des écrans
Implements ClScreen

Si on compile le projet (Débogage => Compiler) on obtient une erreur.

Tout élément public (fonction, propriété, variable, procédure…) déclaré dans clScreen doit en effet être géré dans les modules d'écran qui en héritent.

Pour générer tout le code d'interface nécessaire, sélectionnez clScreen dans la liste déroulante de gauche au dessus du code VBA.

Puis sélectionnez chaque entrée dans la liste déroulante de droite pour générer les déclarations correspondantes.

Placez une apostrophe (') dans chacune des portions de code ajoutée pour éviter qu'elles ne disparaissent (s'il n'y a pas de code à l'intérieur, la compilation supprime les déclarations).

Voici le code obtenu, après ajout de commentaires :

Déclarations ajoutées clScreenMenu

'------------------------------------------------------------------------
' Implémentation des propriétés de l'interface
'------------------------------------------------------------------------
Private Property Get ClScreen_ChangeScreen() As Boolean
'
End Property

Private Property Set ClScreen_Command(RHS As ClCommand)
'
End Property

Private Property Let ClScreen_NextScreen(RHS As String)
'
End Property

Private Property Get ClScreen_NextScreen() As String
'
End Property

Private Property Set ClScreen_SharedData(RHS As ClSharedData)
'
End Property

'------------------------------------------------------------------------
' Mise à jour des commandes
'------------------------------------------------------------------------
Private Function ClScreen_UpdateCommand() As Boolean
'
End Function

'------------------------------------------------------------------------
' Mise à jour de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_UpdateScreen() As Boolean
'
End Function

'------------------------------------------------------------------------
' Affichage de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_DisplayScreen() As Boolean
'
End Function

'------------------------------------------------------------------------
' Minuterie d'attente
'------------------------------------------------------------------------
Private Function ClScreen_ScreenWait() As Boolean
'
End Function

Maintenant le projet compile, mais il nous faut gérer toutes ces propriétés et fonctions.

Nous remarquons qu'aucune variable n'a été définie dans notre module clScreenMenu.

D'après le schéma des objets, nous avons besoin de 4 objets par écran.

Déclaration des objets dans clScreenMenu

' Objet pour commandes
Private oCommand As ClCommand
' Objet pour données partagées
Private oSharedData As ClSharedData
' Objet pour sons
Private oSound As ClSound
' Objet pour gdiplus
Private oGdi As ClGdiplus

Et d'après l'algorithme de déroulement du jeu, nous avons besoin de deux variables :

- un flag booléen pour demander le changement d'écran ;

- et une variable contenant le nom de l'écran suivant.

Déclaration des variables dans clScreen

' Indicateur pour changement d'écran
Private gChangeScreen As Boolean
' Nom de l'écran suivant
Private gNextScreen As String

Puis gérons les propriétés :

Gestion des propriétés

'------------------------------------------------------------------------
' Implémentation des propriétés de l'interface
'------------------------------------------------------------------------
Private Property Get ClScreen_ChangeScreen() As Boolean
ClScreen_ChangeScreen = gChangeScreen
End Property

Private Property Set ClScreen_Command(RHS As ClCommand)
Set oCommand = RHS
End Property

Private Property Let ClScreen_NextScreen(RHS As String)
gNextScreen = RHS
End Property

Private Property Get ClScreen_NextScreen() As String
ClScreen_NextScreen = gNextScreen
End Property

Private Property Set ClScreen_SharedData(RHS As ClSharedData)
Set oSharedData = RHS
End Property

Occupons nous maintenant de l'initialisation de l'écran.
Cette initialisation a lieu dans l'événement Initialize lorsque l'objet est instancié.

Les objets oCommand et oSharedData sont transmis à chaque écran par l'objet Jeu.
Par contre, les autres objets sont spécifiques à chaque écran et doivent donc être créés à l'initialisation de l'écran.

Initialisation des objets dans clScreen

'------------------------------------------------------------------------
' Initialisation de l'objet
' =>création des objets nécessaire à l'écran
'------------------------------------------------------------------------
Private Sub Class_Initialize()
Set oGdi = New ClGdiplus ' Objet pour graphiques
Set oSound = New ClSound ' Objet pour sons
End Sub

Nous allons également définir un écran suivant par défaut dans cet événement.
Comme on est dans l'écran de menu, il n'y a pas vraiment d'écran suivant par défaut, le choix se fait par le menu.
Mettons tout de même un écran « quit » par défaut.

Ecran suivant par défaut à la suite des initialisations d'objets

' Ecran suivant par défaut
gNextScreen = "quit"

Il est maintenant grand temps de tester le code !

Créez un nouveau module (attention : pas un module de classe) : Insertion => Module.

Ecrivez-y une fonction de test :

Fonction de test dans un nouveau module

Option Explicit

' Test de l'écran de menu
Function TestEcran()
Dim oScreen As ClScreen
Set oScreen = New ClScreenMenu
MsgBox oScreen.NextScreen
End Function

Cette fonction :

- déclare un objet de type clScreen ;

- instancie un objet de type clScreenMenu (qui hérite de l'interface clScreen) ;

- affiche un message contenant le nom de l'écran suivant.

Exécutez la fonction : le message attendu apparaît !

La base de notre écran est prête.
Elle pourra servir à la création des autres écrans car nous n'avons pas commencé à programmer le menu dans cet écran.

Mais il est difficile de continuer ainsi « en aveugle » à ce stade du projet.
Il faut désormais mettre en place la boucle de jeu dans l'objet Jeu (classe clGame) et l'intégrer au formulaire pour pouvoir ensuite commencer le développement graphique des écrans et tester l'affichage.

X - Création de la boucle principale dans l'objet Jeu

On a à ce stade du projet tous les modules objets nécessaires à la création de l'objet Jeu.
Cet objet est programmé dans la classe clGame qui est pour l'instant vide.

On a déterminé précédemment dans le schéma des objets que l'objet Jeu (clGame) contenait :

- un objet de type clSharedData pour le partage des données entre les écrans ;

- un objet de type clScreen qui contient une instance de l'écran courant ;

- un objet de type clCommand pour la gestion des commandes.

Déclaration ces objets en en-tête du module clGame.

déclaration des objets inclus dans l'objet Jeu

' Objet Ecran
Private oScreen As ClScreen
' Objet pour commandes
Private oCommand As ClCommand
' Objet pour données partagées entre les écrans
Private oSharedData As ClSharedData

L'algorithme de déroulement du jeu à été écrit sommairement lors de la création de la classe d'interface clScreen.
Réécrivont l'agorithme complet de cette classe Jeu en « pseudocode ».

Algorithme de déroulement du jeu dans l'objet Jeu

Initialisation de l'objet Jeu
oCommand = Nouvel objet de commandes clCommand
oSharedData = Nouvel objet de données clSharedData
Changement d'écran vers écran de menu
Fin d'initialisation

Procédure de début de jeu
Mettre <demande de sortie> à Faux
Début boucle de jeu
Exécute mise à jour des commandes dans oScreen
Exécute mise à jour de l'écran dans oScreen
Exécute mise à jour de l'affichage dans oScreen
Exécute la fonction d'attente dans oScreen
Si oScreen demande un changement d'écran
oScreen = Nouvel écran fonction de l'écran suivant dans oScreen
Affecte oCommand et oSharedData à oScreen
Fin Si
Si <demande de sortie> = Vrai Alors Sortie de la boucle de jeu
Fin boucle de jeu
Fin procédure de début de jeu

Procédure de fin de jeu
Mettre <demande de sortie> à Vrai
Fin procédure de fin de jeu

Procédure de changement d'écran (paramètre = nom de l'écran)
oScreen = Nouvel écran fonction du pramètre
Affecte oCommand et oSharedData à oScreen
Fin procédure de changement d'écran

Notez qu'on défini deux procédures, une pour lancer le jeu et une autre pour l'arrêter.
Il est nécessaire de pouvoir sortir de la boucle de jeu (qui est sans fin) afin de libérer le programme avant de détruire l'objet jeu.

Traduisons ce « pseudocode » en VBA :

Code complet du module clGame

'***************************************************************************************
'*                                  OBJET JEU
'***************************************************************************************
Option Explicit

' Objet Ecran
Private oScreen As ClScreen
' Objet pour commandes
Private oCommand As ClCommand
' Objet pour données partagées entre les écrans
Private oSharedData As ClSharedData
' Flag pour arrêt de la boucle de jeu
Private StopLoop As Boolean

'------------------------------------------------------------------------
' Initialisation de l'objet de Jeu
'------------------------------------------------------------------------
Private Sub Class_Initialize()
' Objet pour commandes
Set oCommand = New ClCommand
' Objet pour données partagées
Set oSharedData = New ClSharedData
' Premier écran = menu
GameChangeScreen "menu"
End Sub

'------------------------------------------------------------------------
' Procédure de début de jeu
'------------------------------------------------------------------------
Public Sub RunGame()
' Pas de sortie de boucle demandée au lancement du jeu
StopLoop = False
Do
' Mise à jour des commandes
oScreen.UpdateCommand
' Mise à jour de l'écran
oScreen.UpdateScreen
' Affichage de l'écran
oScreen.DisplayScreen
' Minuterie
oScreen.ScreenWait
' Test si changement d'écran demandé
If oScreen.ChangeScreen Then
' Change l'écran
If GameChangeScreen(oScreen.NextScreen) = False Then
' Si pas d'écran suivant => quitte le jeu
Exit Do
End If
End If
' Si sortie de boucle demandée => sort de la boucle
If StopLoop Then Exit Do
Loop
End Sub

'------------------------------------------------------------------------
' Procédure de fin de jeu
'------------------------------------------------------------------------
Public Sub StopGame()
'  Sortie de boucle demandée pour l'arrêt du jeu
StopLoop = True
End Sub

'------------------------------------------------------------------------
' Procédure de changement d'écran
'------------------------------------------------------------------------
Private Function GameChangeScreen(pScreenName) As Boolean
' Renvoit Vrai par défaut
GameChangeScreen = True
' Changement d'écran en fonction du nom d'écran en pramètre
Select Case pScreenName
' Changement pour le menu
Case "menu"
Set oScreen = Nothing
Set oScreen = New ClScreenMenu
' Autre changement
Case Else
' Si pas d'écran suivant connu => renvoit Faux pour arrêter le jeu
GameChangeScreen = False
End Select
' Réaffecte les objets de commande et de données partagées au nouvel écran courant
Set oScreen.Command = oCommand
Set oScreen.SharedData = oSharedData
End Function

L'objet Jeu est prêt !

On commence à voir l'intérêt de la programmation orientée objet.
Pour ajouter un nouvel écran il suffira de rajouter son enchainement dans la fonction GameChangeScreen.

XI - Lancement du jeu dans le formulaire

On a défini dans le schéma des objets que le formulaire contenait un objet de type clGame.
On déclare donc en en-tête de module un objet oGame que l'on instanciera au lancement du jeu.

Justement, parlons du lancement du jeu :
Si on souhaite lancer le jeu à l'ouverture du formulaire, on doit placer le code de lancement dans l'événement Initialize du formulaire.
On a alors un problème car le lancement du jeu fait entrer le programme dans une boucle sans fin.
C'est la boucle de la procédure RunGame.
Or la procédure Initialize du formulaire doit être menée à son terme afin que le formulaire soit totalementinitialisé et affiché.
Nous allons donc faire exécuter notre procédure de démarrage en différé par une minuterie, à l'aide de l'instruction OnTime de l'objet Application.
Cette instruction OnTime nécessite une fonction publique dans un module standard (donc pas dans le module du formulaire qui est un module de classe).

Voici en pseudocode le formulaire et la fonction différée dans un nouveau module ModFunctions.

Pseudo code du formulaire FormGame

Initialisation du formulaire
Appel différé d'une fonction StartGame d'un module nommé ModFunctions
Fin d'initialisation du formulaire

Fonction Principale Main
Crée l'objet Jeu
Lance le jeu (procédure RunGame de l'objet Jeu) => entre dans une boucle sans fin
Ferme le formulaire
Fin de la fonction Main

Evénément Demande de fermeture du formulaire
Arrête le jeu (procédure StopGame de l'objet Jeu) => sort de la boucle sans fin
Fin d'Evénément Demande de fermeture

Pseudo code du module de fonctions ModFunctions

Function StartGame
Exécute la fonction principale du formulaire
Fin function StartGame

Traduisons en VBA :

Code VBA du formulaire FormGame

'***************************************************************************************
'*                                 FORMULAIRE DE JEU
'***************************************************************************************
Option Explicit

' objet Jeu
Private oGame As ClGame

'------------------------------------------------------------------------
' Initialisation du formulaire
'------------------------------------------------------------------------
Private Sub UserForm_Initialize()
' Délais d'une seconde avant excution de la fonction principal
' La procédure UserForm_Initialize doit arriver à son terme
Application.OnTime DateAdd("s", 1, Now), "StartGame"
End Sub

'------------------------------------------------------------------------
' Fonction principal
'------------------------------------------------------------------------
Public Function Main()
' Instancie l'objet Jeu
Set oGame = New ClGame
' Lance le jeu
oGame.RunGame
' Une fois le jeu terminé, décharge le formulaire
Unload Me
End Function

'------------------------------------------------------------------------
' Demande de fermeture du formulaire
'------------------------------------------------------------------------
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
' Arrête le jeu si on demande la fermeture du formulaire
oGame.StopGame
End Sub

Code VBA du module de fonctions ModFunctions

'***************************************************************************************
'*                             FUNCTIONS DIVERSES
'***************************************************************************************
Option Explicit

'------------------------------------------------------------------------
' Fonction de lancement du jeu
' Cette fonction est appelée depuis le formulaire par Application.OnTime
'------------------------------------------------------------------------
Public Function StartGame()
FormGame.Main
End Function

L'affichage du formulaire va donc demander l'exécution différée de la fonction StartGame qui à sont tour demandera l'exécution de la fonction Main du formulaire afin de lancer le jeu.

Il est important d'arrêter le jeu à la demande de fermeture du formulaire.
Sans ça, la boucle sans fin ne s'arrêterait pas.

Attention : la procédure ClScreen_ScreenWait n'a pas été programmée.
Pour tester à ce stade, il faudrait ajouter un DoEvents dans cette procédure.

XII - Création du menu : affichage et choix dans le menu

Le résultat du premier test de l'écran n'était pas impressionnant mais nous a permis de valider le fonctionnement de l'interface d'écran clScreen

Il faut maintenant lui ajouter de l'affichage et répondre aux commandes de l'utilisateur pour faire un choix dans le menu.

Le code qui suit s'écrit dans le module clScreenMenu.

XII-A - Minuterie

N'oublions pas de mettre en place la minuterie !

Dans la procédure ClScreen_ScreenWait, ajoutez l'appel à la méthode Wait de l'objet clGdiplus.

Pour un menu, 10 images par seconde suffisent.

Cette méthode Wait n'est pas bloquante. DoEvents est déclenché dès que nécessaire pour traiter la file de message de l'application et permettre à l'utilisateur d'interagir.

Appel de la minuterie dans la fonction ClScreen_ScreenWait

'------------------------------------------------------------------------
' Minuterie d'attente
'------------------------------------------------------------------------
Private Function ClScreen_ScreenWait() As Boolean
' Attente de 1000 / 10 = 100 ms pour affichage de 10 images/s
oGdi.Wait 100
End Function

Maintenant nous pouvons tester en affichant le UserForm FormGame.

Il ne s'affiche qu'un UserForm vide.

XII-B - Affichage du menu

Pour pouvoir dessiner, il faut créer avec gdi+ une image en mémoire.

La création de l'image se fait avec la fonction CreateBitmapForControl de l'objet clGdiplus de l'écran.

L'image créée est de la même taille (en pixels) que le contrôle image Img que nous avions créé sur le formulaire FormGame.

Nous créons cette image à l'initialisation de l'écran de menu dans l'événement Initialize.

Code de création de l'image dans Class_Initialize

With oGdi
' Nouveau bitmap pour dessiner
.CreateBitmapForControl FormGame
End With

Ensuite il faut créer le contenu statique du menu, c'est-à-dire le fond (uni) et le texte des options.
Ce contenu est dessiné une seule fois à l'initialisation de l'écran, puis sauvegardé pour pouvoir être restauré à la demande.
En effet la mise à jour de l'écran dans la fonction UpdateScreen devra :

- recharger le contenu statique du menu ;

- dessiner le contenu dynamique du menu : encadrement de l'entrée sélectionnée, autres animations si nécessaire…

Dans l'événement Initialize nous allons :

1. Remplir le fond de gris avec la fonction Clear.

2. Écrire le nom du jeu en haut de l'image avec la fonction DrawText.

3. Écrire texte de chacune des options avec la fonction DrawText.

Commençons par les deux premières étapes.

Début du dessin du contenu statique

' Dessin du contenu statique
With oGdi
' Nouveau bitmap pour dessiner
.CreateBitmapForControl FormGame
' Rempli l'image de gris
.Clear RGB(150, 150, 150)
' Ecrit le nom du jeu en haut
.DrawText "PacMan en VBA", 35, "Arial", 0, 10, _
.ImageWidth, 10 + 50, 1, 0, vbBlue, , vbYellow
End With

Le code ci dessus rempli l'image de noir puis affiche le texte « Pacman en VBA » en haut et en bleu sur fond jaune.

Pour tester l'affichage, il faut gérer la fonction ClScreen_DisplayScreen.
Utilisons la fonction Repaint de clGdiplus pour dessiner l'image dans le contrôle du formulaire.

Affichage de l'image sur le formulaire

'------------------------------------------------------------------------
' Affichage de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_DisplayScreen() As Boolean
' Dessine l'image sur le formulaire
oGdi.Repaint FormGame.Img
End Function

Affichez le formulaire avec F5 ou dans le menu Exécution => Exécuter Sub/Userform.

On obtient notre première image.

Ajoutons l'affichage des options :
Pour éviter d'écrire plusieurs fois le même code, on crée une petite procédure d'affichage d'une option.
En paramètre de cette procédure : le numéro de l'option et son texte.

Procédure d'affichage d'une option

'------------------------------------------------------------------------
' Affichage de l'option numéro pNum avec le texte pText
'------------------------------------------------------------------------
Private Sub DisplayOption(pNum As Long, pText As String)
' Position verticale de la première option
Const cPos As Long = 140
With oGdi
' Dessine un rectangle de fond en jaune pâle
.DrawRectangle 150, cPos + (pNum - 1) * 60, 600, cPos + (pNum - 1) * 60 + 50, _
RGB(255, 255, 200), , , , , "menu" & pNum
' Dessine le texte de l'option en bleu
.DrawText pText, 50, "Arial", 150, cPos + (pNum - 1) * 60, _
600, cPos + (pNum - 1) * 60 + 50, , , vbBlue
End With
End Sub

Le dessin d'une option se fait en deux étapes :

1. Dessin d'un rectangle de fond.

2. Dessin du texte.

Il faut ajuster les paramètres de position en pixels pour centrer les options.

On pense également à ajouter une région correspondant au rectangle et ayant pour identifiant « menu1 », « menu2 »…

La fonction DrawRectangle permet d'ajout cette région (paramètre pRegion).
Cette région nous servira ensuite à déterminer la région survolée par la souris.

On appelle ensuite cette fonction dans Class_Initialize pour chaque option.

A la fin du dessin du contenu statique, on le sauvegarde en mémoire comme prévu (pour pouvoir restaurer le contenu statique dans la fonction ClScreen_UpdateScreen).

Suite du dessin du contenu statique

[...]
' Affichage des options
DisplayOption 1, "Commencer le jeu"
DisplayOption 2, "Options"
DisplayOption 3, "Scores"
DisplayOption 4, "Crédits"
DisplayOption 5, "Quitter"
' Sauvegarde l'image en mémoire
.ImageKeep
End With

Affichez le formulaire pour voir le résultat.

XII-C - Encadrement de l'entrée de menu sélectionnée

Pour l'instant on n'a pas encore travaillé sur la gestion des commandes.
On ne connait pas les touches définies par l'utilisateur, nous le verrons lors de la création de l'écran des options.

Nous allons dans un premier temps gérer les choix d'option de menu à la souris.

Lors du survol ou du click sur l'image, un événement est envoyé au contrôle du formulaire.
Pour récupérer cet événement, il nous faut dans notre module clScreenMenu un objet image qui recevra les événéments.

On déclare donc une variable de type Image, avec le mot-clé WithEvents qui permet de recevoir les événéments.

Déclaration de l'objet image en en-tête de module

' Contrôle image pour événements
Private WithEvents oImg As MSForms.image

Il faut également initialiser l'objet à la création de l'écran.

Initialisation de l'objet image à la création de l'écran

[...]
Private Sub Class_Initialize()
' Affectation de l'image pour événements
Set oImg = FormGame.Img
[...]

Pour générer le code événément correspondant à un survol de l'image, sélectionnez dans la liste déroulante en haut à gauche l'objet oImg puis dans la liste déroulante à droite sélectionnez MouseMove.
Lors du survol de l'image, la procédure oImg_MouseMove sera exécutée.

Dans cette procédure, nous allons lire (avec RegionGetXY) la région située sous la souris, puis l'affecter à une variable.
Nous pourrons ensuite encadrer cette région dans la fonction ClScreen_UpdateScreen.

On commence par déclarer la variable en en-tête de module :

Déclaration de la variable pour option sélectionnée

' Nom de l'option sélectionnée
Private gOption As String

Et ensuite on modifie cette variable au survol de la souris.

Modification de la variable gOption au survol de la souris

'------------------------------------------------------------------------
' Sur déplacement de la souris
'------------------------------------------------------------------------
Private Sub oImg_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Dim lRegion As String
With oGdi
' Région aux coordonnées X et Y
' Les valeurs de X et Y étant des coordonnées prises sur le contrôle,
'     on passe ce contrôle en cinquième paramètre
lRegion = .RegionGetXY(X, Y, , , oImg)
If lRegion <> "" Then
gOption = lRegion
End If
End With
End Sub

Et enfin dans la fonction ClScreen_UpdateScreen, on restaure le contenu statique et on encadre l'option sélectionnée.

Encadre l'option sélectionnée

'------------------------------------------------------------------------
' Mise à jour de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_UpdateScreen() As Boolean
With oGdi
' Recharge l'image de la mémoire
.ImageReset
' Encadre en rouge l'entrée de menu sélectionnée
.RegionFrame gOption, vbRed, 4
End With
End Function

Si on affiche le formulaire, l'option de menu survolée s'encadre en rouge.
Mais on a oublié de sélectionner une option par défaut à l'initialisation de l'écran.

Option sélectionnée par défaut

'------------------------------------------------------------------------
' Initialisation de l'écran
'------------------------------------------------------------------------
Private Sub Class_Initialize()
' Option sélectionnée par défaut
gOption = "menu1"
[...]

Deux petits soucis à ce stade de la programmation du menu :

1. Si on clique sur le formulaire, l'image ne change plus ensuite.

2. L'affichage « clignote » et ça fait mal aux yeux.

Pour le premier problème, on peut forcer le formulaire à se redessiner avec un appel à la méthode Repaint du formulaire.

Pour le second problème, on peut augmenter le paramètre DrawBuffer du formulaire jusqu'à ce que les clignotements disparaissent (valeur maxi = 1048576).

Sinon une autre solution plus simple qui règle les deux problèmes : utilisez la méthode RepaintNoFormRepaint.
Cette méthode dessine l'image directement sur l'écran et évite ainsi tout problème d'affichage.

Améliore l'affichage

'------------------------------------------------------------------------
' Affichage de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_DisplayScreen() As Boolean
' Dessine l'image sur le formulaire
oGdi.RepaintNoFormRepaint FormGame.Img
End Function

XII-D - Sélection d'une option de menu

Toujours à la souris, on va choisir l'option de menu désirée en cliquant sur l'image.
Créez le code événementiel sur souris appuyée MouseDown.

Code pour souris appuyée

'------------------------------------------------------------------------
' Sur click de la souris
'------------------------------------------------------------------------
Private Sub oImg_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MenuSelect
End Sub

On garde en tête qu'on souhaitera plus tard gérer le choix des options au clavier, donc on appelle une fonction MenuSelect qui pourra ensuite servir au clavier.

En fonction du contenu de gOption, on défini le nom de l'écran suivant et on demande le changement d'écran en donnant la valeur Vrai à gChangeScreen.

Code pour choix d'option dans le menu

'------------------------------------------------------------------------
' Sélection dans le menu
'------------------------------------------------------------------------
Private Sub MenuSelect()
Select Case gOption
Case "menu1"
gNextScreen = "game"
gChangeScreen = True
Case "menu2"
gNextScreen = "options"
gChangeScreen = True
Case "menu3"
gNextScreen = "scores"
gChangeScreen = True
Case "menu4"
gNextScreen = "credits"
gChangeScreen = True
Case "menu5"
gNextScreen = "quit"
gChangeScreen = True
End Select
End Sub

Si vous testez le click sur une option de menu, le formulaire se ferme quelque soit l'option choisie.
C'est normal car nous allons ensuite gérer l'enchaînement des écrans dans GameChangeScreen du module clGame au fur et à mesure de leur création.

XIII - Gestion des options de jeu

XIII-A - Stockage des options

Les options de jeu sont des valeurs modifiables par le joueur.
Il faut donc les stocker quelque part pour ne pas avoir à les redéfinir à chaque jeu.

On a plusieurs possibilités pour stocker ces valeurs d'options :

1. Etant sous Excel on pourrait les stocker dans une feuille.
Mais ça impliquerait une sauvegarde du fichier Excel complet à chaque modification d'option et c'est un peu lourd.

2. On peut les stocker dans un fichier texte.
Gérer un fichier ini se fait facilement à l'aide des fonctions WritePrivateProfileString et GetPrivateProfileString de la bibliothèque kernel32.dll.

On va donc se créer une fonction de lecture et une fonction d'écriture d'option dans le fichier ini.
Ce fichier s'appelera config.ini et sera placé dans un sous-dossier config que nous créons dès maintenant dans le répertoire du projet.

Dans le module ModFunctions, ajoutez les déclarations en en-tête :

Déclaration pour gestion de fichier ini

' Déclarations pour fichier ini
#If VBA7 Then
Private Declare PtrSafe Function WritePrivateProfileString Lib "kernel32" Alias _
"WritePrivateProfileStringA" (ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Long
Private Declare PtrSafe Function GetPrivateProfileString Lib "kernel32" Alias _
"GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, _
ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, _
ByVal lpFileName As String) As Long
#Else
Private Declare Function WritePrivateProfileString Lib "kernel32" Alias _
"WritePrivateProfileStringA" (ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Long
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias _
"GetPrivateProfileStringA" (ByVal lpApplicationName As Any, ByVal lpKeyName As Any, _
ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, _
ByVal lpFileName As String) As Long
#End If

Puis ajoutez ces deux fonctions :

Fonctions de lecture/écriture de fichier ini

'------------------------------------------------------------------------
' Lecture de la valeur d'une clé dans un fichier ini
'------------------------------------------------------------------------
Public Function ReadOption(pOption As String, Optional pDefault As String) As String
Dim lRet As Long
Dim lData As String
Dim lSize As Long
lData = Space$(8192)
lSize = 8192
lRet = GetPrivateProfileString("Options", pOption, pDefault, lData, lSize, ThisWorkbook.path & "\config\config.ini")
If lSize > 0 Then
ReadOption = Left$(lData, lRet)
Else
ReadOption = ""
End If
End Function

'------------------------------------------------------------------------
' Ecriture de la valeur d'une clé dans un fichier ini
'------------------------------------------------------------------------
Public Function WriteOption(pOption As String, Optional pValue As String) As Boolean
Dim lRet As Long
Dim lSize As Long
If pValue = "" Then
lRet = WritePrivateProfileString("Options", pOption, 0&, ThisWorkbook.path & "\config\config.ini")
Else
lRet = WritePrivateProfileString("Options", pOption, pValue, ThisWorkbook.path & "\config\config.ini")
End If
WriteOption = (lRet <> 0)
End Function

ReadOption pour lire une valeur.
WriteOption pour écrire une valeur.

Inutile de créer le fichier config.ini à la main, il sera créé automatiquement à la première écriture d'option.
Par contre le dossier dans lequel on écrit le fichier ini doit exister.

XIII-B - Les variables des options

Nos options devront être accessibles par tous les écrans.
En effet les touches paramétrées serviront aussi bien dans l'écran de jeu que dans les autres écrans (pour validation, choix dans le menu…).

Nous allons donc créer des variables dans le module de données partagées clSharedData.

Déclaration des variables dans clSharedData pour options de jeu

' Code des touches
Public ToucheBas As Long
Public ToucheHaut As Long
Public ToucheDroite  As Long
Public ToucheGauche  As Long
Public ToucheValid  As Long
' Nombre de fantômes
Public NbFantomes As Long
' Joystick utilisé
Public JoyNum As Long
' Bouton de validation du joystick
Public JoyButton As Long

Nous retrouvons les options définies au début :

- le nombre de fantômes ;

- les différentes commandes clavier et joystick.

XIII-C - Chargement des options

Nos options devront être chargées au lancement du jeu, dans la procédure RunGame de l'objet Jeu.

Chargement des options au début de la procédure RunGame du module clGame

' Chargement des options
oSharedData.NbFantomes = ReadOption("NbFantomes", 5)
oSharedData.ToucheBas = ReadOption("ToucheBas", vbKeyDown)
oSharedData.ToucheDroite = ReadOption("ToucheDroite", vbKeyRight)
oSharedData.ToucheGauche = ReadOption("ToucheGauche", vbKeyLeft)
oSharedData.ToucheHaut = ReadOption("ToucheHaut", vbKeyUp)
oSharedData.ToucheValid = ReadOption("ToucheValid", vbKeyReturn)
oSharedData.JoyNum = ReadOption("JoyNum", 0)
oSharedData.JoyButton = ReadOption("JoyButton", 0)

Les options sont lues dans le fichier config.ini grâce à la fonction ReadOption écrite précédemment.
On passe en paramètre une valeur par défaut pour le cas où l'option n'est pas définie dans le fichier de configuration.

Pour le joypad, une fois lu le joypad choisi dans les options, on affecte ce joypad à l'objet command.

Affectation du jopypad

' Affecte le joystick à l'objet command
oCommand.JoyPadNum = oSharedData.JoyNum

XIII-D - Création de l'écran des options de jeu

Cet écran se programme dans le module clScreenOption.

Préparons l'écran en copiant dans ce module le contenu du module clScreenMenu de l'écran de menu.

Nettoyez le module en retirant la fonction DisplayOption qui ne nous servira pas pour cet écran, ainsi que tous les appels à cette fonction.

Il faut ensuite gérer l'enchaînement de l'écran des options.

Dans la fonction GameChangeScreen du module clGame, il faut ajouter l'enchainement vers l'écran des options.

Ajoutez un Case au Select.

Enchainement vers écran d'options dans clGame

' Changement pour les options
Case "options"
Set oScreen = Nothing
Set oScreen = New clScreenOptions

On teste la valeur « options » car dans l'écran de menu c'est cette valeur qu'on utilise lors du click sur Options.

Puis modifier le texte de l'écran d'options pour pouvoir tester, en remplaçant la ligne qui écrivait « Pacman en VBA ».

Texte en haut de l'écran clScreenOptions

' 0 - Ecrit le titre de l'écran en haut
.DrawText "Options de jeu", 35, "Arial", 0, 10, .ImageWidth, _
10 + 50, 1, 0, vbBlue, , vbYellow

Cliquez dans le menu sur « OPTIONS » affiche bien l'écran des options.

XIII-E - Affichage des options de jeu

Faisons une petite maquette (sous PowerPoint par exemple) pour se donner une idée de l'écran d'options que l'on doit réaliser.

Chaque cadre sera encadré lorsque sélectionné.

Après l'écriture du texte « Options de jeu », on écrit le code qui dessine la partie statique de l'écran d'options.

Code de dessin à placer après l'écriture du titre et avant le ImageKeep

' 1 - Affichage du nombre de fantômes
.DrawText "Nombre de fantômes", 20, , 120, 100, 400, 130, 0, , vbBlue
.DrawRectangle 310, 100, 370, 130, , vbWhite, 2, , , "NbFantomes"
' 2 - Affichage commandes clavier
.DrawText "Commandes clavier", 20, , 120, 140, 400, 170, 0, , vbBlue
.DrawRectangle 120, 170, 600, 410, , vbBlack, 1
.DrawRectangle 310, 180, 400, 210, , vbWhite, 2, , , "ToucheHaut"
.DrawText "Haut", 20, , 310, 180, 400, 210, , , vbBlue
.DrawRectangle 310, 280, 400, 310, , vbWhite, 2, , , "ToucheBas"
.DrawText "Bas", 20, , 310, 280, 400, 310, , , vbBlue
.DrawRectangle 310, 350, 400, 380, , vbWhite, 2, , , "ToucheValid"
.DrawText "Valider", 20, , 310, 350, 400, 380, , , vbBlue
.DrawRectangle 160, 230, 250, 260, , vbWhite, 2, , , "ToucheGauche"
.DrawText "Gauche", 20, , 160, 230, 250, 260, , , vbBlue
.DrawRectangle 460, 230, 550, 260, , vbWhite, 2, , , "ToucheDroite"
.DrawText "Droite", 20, , 460, 230, 550, 260, , , vbBlue
' 2 - Affichage commandes joystick
.DrawText "Commandes joystick", 20, , 120, 420, 400, 450, 0, , vbBlue
.DrawRectangle 120, 450, 600, 510, , vbBlack, 1
.DrawRectangle 200, 460, 500, 490, , vbWhite, 2, , , "JoyButton"
.DrawText "Bouton de validation", 20, , 200, 460, 500, 490, , , vbBlue
' 4 - Affichage des boutons valider et annuler
.DrawRectangle 180, 530, 310, 560, RGB(150, 255, 150), vbBlack, 1, , , "BoutonValid"
.DrawText "Valider", 20, , 180, 530, 310, 560, , , vbBlue
.DrawRectangle 400, 530, 550, 560, RGB(255, 150, 150), vbBlack, 1, , , "BoutonCancel"
.DrawText "Annuler", 20, , 400, 530, 550, 560, , , vbBlue

Comme pour le menu, on ajoute des régions rectangulaires à l'appel des fonctions DrawRectangle.

Lancez le formulaire et cliquez sur le menu « OPTIONS » pour tester.

On s'aperçoit que le code utilisé pour encadrer les options de menu fonctionne toujours dans cet écran.
Par contre on va modifier l'option choisie par défaut :

Option choisie par défaut dans Class_Initialize

' Option sélectionnée par défaut
gOption = "BoutonCancel"

Ainsi le bouton « Annuler » est sélectionné par défaut.

Une fois tous les cadres dessinés, on va écrire les valeurs des options :

- on écrit le nombre de fantôme dans la région (=le rectangle) ;

- et on écrit le nom de chaque touche et bouton sous la région (=le rectangle).

Ecrivons deux petites procédures pour écrire ces données :

Procédure pour écrire sous une région

'------------------------------------------------------------------------
' Affichage texte sous une région
'------------------------------------------------------------------------
Private Function DisplayTextUnderRegion(pRegion As String, pText As String)
Dim lX1 As Long, lY1 As Long, lX2 As Long, lY2 As Long
' Position de la région pRegion
If oGdi.RegionGetRect(pRegion, lX1, lY1, lX2, lY2) Then
' Ecrit le texte en noir et italique
oGdi.DrawText pText, 15, , (lX2 + lX1) / 2, lY2 + 10, , , , , vbBlack, , , , True
End If
End Function

Procédure pour écrire dans une région

'------------------------------------------------------------------------
' Affichage texte sur une région
'------------------------------------------------------------------------
Private Function DisplayTextInRegion(pRegion As String, pText As String)
Dim lX1 As Long, lY1 As Long, lX2 As Long, lY2 As Long
' Position de la région pRegion
If oGdi.RegionGetRect(pRegion, lX1, lY1, lX2, lY2) Then
'  Ecrit le texte en noir
oGdi.DrawText pText, 20, , lX1, lY1, lX2, lY2, , , vbBlack
End If
End Function

Nous utiliserons ces procédures pour afficher les valeurs des options dans la procédure de mise à jour ClScreen_UpdateScreen.

Les différentes valeurs sont stockées dans l'objet oSharedData.

Pour les touches, la valeur de l'option est le code numérique de la touche.
On va créer une petite fonction pour récupérer le nom de la touche en toute lettre.
Plaçons cette procédure dans le module ModFunctions.

Déclarations en haut de module

' Déclaration pour nom des touches
#If VBA7 Then
Private Declare PtrSafe Function MapVirtualKey Lib "user32" Alias "MapVirtualKeyA" _
(ByVal wCode As Long, ByVal wMapType As Long) As Long
Private Declare PtrSafe Function GetKeyNameText Lib "user32" Alias "GetKeyNameTextA" _
(ByVal lParam As Long, ByVal lpBuffer As String, ByVal nSize As Long) As Long
#Else
Private Declare Function MapVirtualKey Lib "user32" Alias "MapVirtualKeyA" _
(ByVal wCode As Long, ByVal wMapType As Long) As Long
Private Declare Function GetKeyNameText Lib "user32" Alias "GetKeyNameTextA" _
(ByVal lParam As Long, ByVal lpBuffer As String, ByVal nSize As Long) As Long
#End If

Fonction qui renvoit le texte d'une touche à partir de son code

'------------------------------------------------------------------------
' Renvoit le texte d'une touche à partir de son code
'------------------------------------------------------------------------
Public Function KeyNameFromKeyCode(pKeyCode As Long) As String
Dim lKeyCode As Long
Dim lBuffer As String
Dim lSize As Long

lKeyCode = MapVirtualKey(pKeyCode, 0)
lKeyCode = lKeyCode * &H10000
lBuffer = Space$(256)
lSize = GetKeyNameText(lKeyCode, lBuffer, 256)
KeyNameFromKeyCode = Left$(lBuffer, lSize)
End Function

Retour au module clScreenOptions.
Après l'encadrement de région, dans la procédure ClScreen_UpdateScreen, on ajoute le dessin des valeurs des options.

Dessin des valeurs des options

' Ecrit le nombre de fantômes
DisplayTextInRegion "NbFantomes", CStr(oSharedData.NbFantomes)
' Ecrit le texte des touches
DisplayTextUnderRegion "ToucheBas", "( " & KeyNameFromKeyCode(oSharedData.ToucheBas) & " )"
DisplayTextUnderRegion "ToucheHaut", "( " & KeyNameFromKeyCode(oSharedData.ToucheHaut) & " )"
DisplayTextUnderRegion "ToucheDroite", "( " & KeyNameFromKeyCode(oSharedData.ToucheDroite) & " )"
DisplayTextUnderRegion "ToucheGauche", "( " & KeyNameFromKeyCode(oSharedData.ToucheGauche) & " )"
DisplayTextUnderRegion "ToucheValid", "( " & KeyNameFromKeyCode(oSharedData.ToucheValid) & " )"
' Ecrit le texte pour le bouton de joystick
DisplayTextUnderRegion "JoyButton", _
"( Bouton " & oSharedData.JoyButton & " du joystick " & oCommand.JoyPadName & " )"

Lancez le formulaire et cliquez sur le menu « OPTIONS » pour tester l'affichage des valeurs.

XIII-F - Modification des options de jeu

Pour modifier une option, l'utilisateur devra survoler l'option à la souris puis appuyer sur la touche souhaitée.

Pour connaître la touche appuyée, on utilisera la fonction KeyPressGetCode de l'objet oCommand.
Cette fonction revoit le code numérique d'une seule touche appuyée (même si on appuie sur plusieurs touches simultanément).

Si une touche est appuyée et qu'une option est survolée, alors on modifie la valeur de l'option.

Pour le joystick, on teste l'état de tous les boutons de tous les joysticks présents.
Si un bouton est appuyé, il devient le bouton de validation.

La détection de touche ou joystick se fait dans la fonction ClScreen_UpdateCommand.

Modification des options sur appuis de touche ou joystick dans ClScreen_UpdateCommand

Dim lKeyPress As Long
Dim loCommand As ClCommand
Dim lCpt As Long
' Code de la touche appuyée
lKeyPress = oCommand.KeyPressGetCode
' Si une touche est appuyée
If lKeyPress <> 0 Then
Select Case gOption
Case "ToucheHaut"
oSharedData.ToucheHaut = lKeyPress
Case "ToucheBas"
oSharedData.ToucheBas = lKeyPress
Case "ToucheDroite"
oSharedData.ToucheDroite = lKeyPress
Case "ToucheGauche"
oSharedData.ToucheGauche = lKeyPress
Case "ToucheValid"
oSharedData.ToucheValid = lKeyPress
Case "NbFantomes"
If IsNumeric(Chr(lKeyPress)) Then
' Touche numérique
oSharedData.NbFantomes = Val(Chr(lKeyPress))
ElseIf Val(KeyNameFromKeyCode(lKeyPress)) <> 0 Then
' Touche du pavé numérique
oSharedData.NbFantomes = Val(KeyNameFromKeyCode(lKeyPress))
End If
End Select
End If
' Teste des touches de joystick
' Objet command local pour test de tous les joysticks
Set loCommand = New ClCommand
' Premier joystick, commence à l'indice 0
loCommand.JoyPadNum = 0
Do
' Si pas de bouton => joystick invalide => sort de la boucle
If loCommand.JoyPadButtons = 0 Then Exit Do
' Teste l'état de tous les boutons
For lCpt = 0 To loCommand.JoyPadButtons
' Teste l'appui sur le bouton n° lCpt
If loCommand.TestJoypadButton(lCpt) Then
' Modifie les options en fonction du bouton et du joystick utilisé
oSharedData.JoyButton = lCpt
oSharedData.JoyNum = loCommand.JoyPadNum
' Affecte le joystick choisi à l'objet command global
oCommand.JoyPadNum = loCommand.JoyPadNum
' Sort de la boucle
Exit Do
End If
Next
' Joystick suivant
loCommand.JoyPadNum = loCommand.JoyPadNum + 1
Loop

Pour le nombre de fantôme, on vérifie si la touche appuyée correspond à une touche de chiffre (en haut sur le clavier), ou à une touche du pavé numérique.

XIII-G - Validation ou annulation des options de jeu

Il faut maintenant gérer la validation ou l'annulation des options.

Sur click sur le bouton « Valider » ou le bouton « Annuler », on retourne au menu.
Modifions donc l'écran suivant par défaut de cet écran d'options.

Retour par défaut au menu dans Class_Initialize

' Ecran suivant par défaut
gNextScreen = "menu"

Ensuite modifions la procédure MenuSelect de notre écran.

Sélection de valider ou annuler

'------------------------------------------------------------------------
' Sélection dans le menu
'------------------------------------------------------------------------
Private Sub MenuSelect()
Select Case gOption
Case "BoutonValid"
gChangeScreen = True
Case "BoutonCancel"
gChangeScreen = True
End Select
End Sub

Inutile de préciser l'écran suivant, c'est l'écran par défaut dans les deux cas.

Pour compléter :
Sur click sur le bouton annuler on va recharger les options pour annuler les changements.
Par contre sur click sur le bouton valider, on va sauvegarder les options.

Code complet : Sélection de valider ou annuler

'------------------------------------------------------------------------
' Sélection dans le menu
'------------------------------------------------------------------------
Private Sub MenuSelect()
Select Case gOption
Case "BoutonValid"
' Sauvegarde des options
WriteOption "NbFantomes", oSharedData.NbFantomes
WriteOption "ToucheBas", oSharedData.ToucheBas
WriteOption "ToucheDroite", oSharedData.ToucheDroite
WriteOption "ToucheGauche", oSharedData.ToucheGauche
WriteOption "ToucheHaut", oSharedData.ToucheHaut
WriteOption "ToucheValid", oSharedData.ToucheValid
WriteOption "JoyNum", oSharedData.JoyNum
WriteOption "JoyButton", oSharedData.JoyButton
' Demande le changement d'écran
gChangeScreen = True
Case "BoutonCancel"
' Rechargement des options
oSharedData.NbFantomes = ReadOption("NbFantomes", 5)
oSharedData.ToucheBas = ReadOption("ToucheBas", vbKeyDown)
oSharedData.ToucheDroite = ReadOption("ToucheDroite", vbKeyRight)
oSharedData.ToucheGauche = ReadOption("ToucheGauche", vbKeyLeft)
oSharedData.ToucheHaut = ReadOption("ToucheHaut", vbKeyUp)
oSharedData.ToucheValid = ReadOption("ToucheValid", vbKeyReturn)
oSharedData.JoyNum = ReadOption("JoyNum", 0)
oSharedData.JoyButton = ReadOption("JoyButton", 0)
' Affecte le joystick à l'objet command
oCommand.JoyPadNum = oSharedData.JoyNum
' Demande le changement d'écran
gChangeScreen = True
End Select
End Sub

Affichez l'écran d'options et validez : le fichier congfig.ini a été créé dans le sous-dossier config.

Une dernière chose avant de clore ce chapitre.
Si on utilise le joystick, une fois entré dans l'écran d'options on ne peut en sortir qu'avec la souris.
Cela n'est pas très pratique, on va donc tester si on appuie sur le bouton de joystick de validation afin de pouvoir quitter l'écran au joystick.

Dans clScreenOptions, au début de la fonction ClScreen_UpdateCommand

' Si appui sur la touche de validation du joystick
If oCommand.TestJoypadButton(oSharedData.JoyButton) Then
' Valide le menu sélectionné (annuler par défaut)
MenuSelect
Exit Function
End If

Voilà qui est plus pratique.

XIII-H - Navigation dans l'écran de menu avec le clavier ou le joystick

Maintenant que nous avons toutes nos commandes définies, nous pouvons également les utiliser pour naviguer dans l'écran de menu.

Ouvrez le module clScreenMenu et ajouter la navigation au clavier et joystick dans la procédure ClScreen_UpdateCommand.

Dans UpdateCommand du module clScreenMenu

Dim lCpt As Long
Dim lDown As Boolean
Dim lUp As Boolean
' Test directions du joystick
oCommand.TestJoypadDir , , lUp, lDown
' Numéro du menu sélectionné
lCpt = Right(gOption, 1)
' Si appuis touche vers le bas
If oCommand.TestKey(oSharedData.ToucheBas, 100) Or lDown Then
lCpt = lCpt + 1
If lCpt = 6 Then lCpt = 1
gOption = "menu" & lCpt
End If
' Si appuis touche vers le haut
If oCommand.TestKey(oSharedData.ToucheHaut, 100) Or lUp Then
lCpt = lCpt - 1
If lCpt = 0 Then lCpt = 5
gOption = "menu" & lCpt
End If
' Test si validation
If oCommand.TestKey(oSharedData.ToucheValid, 100) Or oCommand.TestJoypadButton(oSharedData.JoyButton, 100) Then
MenuSelect
End If

On peut ainsi naviguer dans menu et valider soit à la souris, au clavier, ou au joystick.

XIV - Bilan à mi-parcours

Faisons un petit bilan pour y voir plus clair.

Nous avons terminé :

- l'écran de menu ;

- l'écran des options.

Il nous reste :

- l'écran des scores ;

- l'écran des crédits ;

- les écrans de fin de partie (gagné / perdu) ;

- et l'écran de jeu !

On constate après tout ce temps que nous n'avons pas encore vraiment commencé le développement du jeu.
Pour l'instant on ne voit pas franchement que nous sommes en train de créer un PacMan.

Par contre tout est prêt pour continuer le développement dans de bonnes conditions.

Pour la suite du tutoriel, nous allons enfin commencer le développement de l'écran de jeu.

XV - Développement de l'écran de jeu

L'écran de jeu est développé dans le module clScreenGame.
Ce sera bien entendu le plus complexe des écrans du jeu.

XV-A - Création de l'écran de jeu

Préparez le module clScreenGame de la même manière que le module clScreenMenu au chapitre Préparation du premier écran : le menu clScreenMenu.

Pensez à gérer l'affichage et la minuterie (40 images par seconde pour le jeu).

Affichage et minuterie dans clScreenGame

'------------------------------------------------------------------------
' Affichage de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_DisplayScreen() As Boolean
' Dessine l'image sur le formulaire
oGdi.RepaintNoFormRepaint FormGame.Img
End Function

'------------------------------------------------------------------------
' Minuterie d'attente
'------------------------------------------------------------------------
Private Function ClScreen_ScreenWait() As Boolean
' Attente de 1000 / 40 = 25 ms pour affichage de 40 images/s
oGdi.Wait 25
End Function

Il faut ensuite gérer l'enchaînement de l'écran de jeu.

Dans la fonction GameChangeScreen du module clGame, il faut ajouter l'enchainement vers l'écran de jeu.

Ajoutez un Case au Select.

Enchainement vers écran de jeu dans clGame

' Changement pour le jeu
Case "game"
Set oScreen = Nothing
Set oScreen = New ClScreenGame

On teste la valeur « game » car dans l'écran de menu c'est cette valeur qu'on utilise lors du click sur Commencer le jeu.

XV-B - Chargement du tableau de jeu

On a défini le tableau de jeu dans la feuille Jeu.

Ce tableau fait 28 colonnes et 30 lignes.
On va stocker les valeurs de ce tableau dans une variable du module clScreenGame.

Déclaration du tableau en en-tête de clScreenGame

' Tableau de jeu
Private gTableau(0 To 27, 0 To 29) As String

Puis on charge les données de la feuille Jeu dans ce tableau.

Chargement du tableau dans l'initialisation de clScreenGame au début de Class_Initialize

' Compteurs
Dim lCptY As Integer
Dim lCptX As Integer
' Charge les données du tableau
With ThisWorkbook.Sheets("Jeu")
For lCptX = 1 To 28
For lCptY = 1 To 30
gTableau(lCptX - 1, lCptY - 1) = .Cells(lCptY, lCptX).Value
Next
Next
End With

XV-C - Affichage du tableau de jeu

Le tableau de jeu est dessiné en fonction du contenu des cases, à base de lignes et d'arcs de cercle.
Pour rappel, le tableau de jeu a été chargé dans gTableau à partir des données de la feuille Jeu.

Pour chaque case, on va lire les valeurs des cases adjacentes pour déterminer le type de trait à dessiner.

Créons donc une petite fonction de lecture du contenu d'une case.

Lecture du contenu d'une case

'---------------------------------------------------------------------------------------
' Lecture du contenu d'une case
'---------------------------------------------------------------------------------------
' pX,pY : Coordonnées de la case
' La fonction renvoie le contenu de la case dans un string
' Si on dépasse le tableau, renvoit ""
'---------------------------------------------------------------------------------------
Private Function GetValue(pX As Integer, pY As Integer) As String
On Error Resume Next
' Lecture de la case dans le tableau représentant le niveau
GetValue = gTableau(pX, pY)
If Err.Number <> 0 Then GetValue = ""    ' Renvoit une chaîne vide si erreur
End Function

Cette fonction renvoit le contenu de la case de coordonnées pX, pY.
Si les coordonnées n'appartiennent pas au tableau, une chaîne vide est renvoyée.
Ceci nous permettera de nous passer d'un test pour vérifier si la case est bien contenue dans le tableau.

Ensuite, pour dessiner les murs sur l'image, il nous faut connaître le centre de la case.

Calcul la position sur l'image en fonction de la position dans le tableau

'---------------------------------------------------------------------------------------
' Calcul la position sur l'image en fonction de la position dans le tableau
'---------------------------------------------------------------------------------------
' pX,pY : Coordonnées dans le tableau
' pXOut,pYOut : Coordonnées sur l'image en pixel
'---------------------------------------------------------------------------------------
Private Sub GetCenter(pX As Currency, pY As Currency, pXOut As Long, pYOut As Long)
pXOut = gBordure + (pX + 0.5) * gTx
pYOut = gBordure + (pY + 0.5) * gTy
End Sub

Le centre d'une case est déterminé en fonction de la taille des cases et de la bordure.
On a donc besoin de déclarer ces variables, qui seront ensuite calculées à l'initialisation de l'écran.

Déclaration des variables en en-tête de module

' Taille d'une case
Private gTx As Long, gTy As Long
' Bordure en pixels
Private gBordure As Long

A l'initialisation de l'écran, nous allons dessiner le contenu statique : c'est-à-dire le tableau de jeu.

Déclarations pour le dessin du contenu statique dans Class_Initialize de clScreenGame

' Position du centre de la case
Dim lX As Long
Dim lY As Long
' Valeur des cases adjacente
Dim lUp As String
Dim lDown As String
Dim lRight As String
Dim lLeft As String
' Valeur de la case
Dim lValue As String

Dessin du contenu statique dans Class_Initialize de clScreenGame

' Dessine le contenu statique
With oGdi
' Nouveau bitmap pour dessiner
.CreateBitmapForControl FormGame.Img
' Dessin lissé pour les ellipses/lignes ...
.SmoothingMode = GdipSmoothingAntialias
' Début et fin de traits arrondis
.LineStart = LineCapRound
.LineEnd = LineCapRound
' Bordure de 5 pixels
gBordure = 5
' Taille des cases
gTx = (.ImageWidth - 2 * gBordure) \ 28
gTy = (.ImageHeight - 2 * gBordure) \ 30
' On s'assure que la taille des cases est un multiple de 2
gTx = gTx - (gTx Mod 2)
gTy = gTy - (gTy Mod 2)
' Fond noir
.FillColor vbBlack
' On dessine les murs
For lCptY = 0 To 29    ' 30 lignes
For lCptX = 0 To 27    ' 28 colonnes
' Calcul du centre de la case de coordonnéex lcptx,lcpty
GetCenter CSng(lCptX), CSng(lCptY), lX, lY
' Lecture du contenu de la case
lValue = GetValue(lCptX, lCptY)
' Lecture du contenu des cases adjacentes
lRight = GetValue(lCptX + 1, lCptY)
lLeft = GetValue(lCptX - 1, lCptY)
lUp = GetValue(lCptX, lCptY - 1)
lDown = GetValue(lCptX, lCptY + 1)
Select Case lValue
' Il y a un mur dans la case
Case "X"
' Mur horizontal
If lRight = "X" And lLeft = "X" And (Not lUp = "X" Or Not lDown = "X" Or Not lRight = "X") Then
.DrawLine lX - gTx / 2, lY, lX + gTx / 2, lY, RGB(0, 0, 255), 3
End If
' Mur vertical
If lUp = "X" And lDown = "X" And (Not lRight = "X" Or Not lLeft = "X") Then
.DrawLine lX, lY - gTy / 2, lX, lY + gTy / 2, RGB(0, 0, 255), 3
End If
' Mur en haut et à droite mais pas ailleurs
If lUp = "X" And lRight = "X" And lLeft <> "X" And lDown <> "X" Then
.DrawEllipse lX + gTx / 2, lY - gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , 180, -90
End If
' Murs en coin (arrondis)
' Mur en haut et à gauche mais pas ailleurs
If lUp = "X" And lLeft = "X" And lRight <> "X" And lDown <> "X" Then
.DrawEllipse lX - gTx / 2, lY - gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , 90, -90
End If
' Mur en bas et à droite mais pas ailleurs
If lDown = "X" And lRight = "X" And lLeft <> "X" And lUp <> "X" Then
.DrawEllipse lX + gTx / 2, lY + gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , -180, 90
End If
' Mur en bas et à gauche mais pas ailleurs
If lDown = "X" And lLeft = "X" And lRight <> "X" And lUp <> "X" Then
.DrawEllipse lX - gTx / 2, lY + gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , -90, 90
End If
Case Else
' La case ne contient pas un mur mais est dans un angle formé par deux murs
' On dessine un coin arrondi
' Mur en haut et à droite mais pas sur les deux autres côtés à la fois
If lUp = "X" And lRight = "X" And Not (lLeft = "X" And lDown = "X") Then
.DrawEllipse lX + gTx / 2, lY - gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , 0, -90
End If
' Mur en haut et à gauche mais pas sur les deux autres côtés à la fois
If lUp = "X" And lLeft = "X" And Not (lRight = "X" And lDown = "X") Then
.DrawEllipse lX - gTx / 2, lY - gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , -90, -90
End If
' Mur en bas et à droite mais pas sur les deux autres côtés à la fois
If lDown = "X" And lRight = "X" And Not (lLeft = "X" And lUp = "X") Then
.DrawEllipse lX + gTx / 2, lY + gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , 0, 90
End If
' Mur en bas et à gauche mais pas sur les deux autres côtés à la fois
If lDown = "X" And lLeft = "X" And Not (lRight = "X" And lUp = "X") Then
.DrawEllipse lX - gTx / 2, lY + gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , 90, 90
End If
End Select
Next
Next
End With

On commence par créer une image avec CreateBitmap.
Cette image est remplie de noir, le jeu se déroulant sur fond noir.
La propriété DrawSmooth permet de lisser les dessins de traits.
Les propriétés LineStart et LineEnd définissent le type de début et fin de trait.
La bordure est définie à 5 pixels (pour éviter que le dessin soit « collé » aux bords de l'image).
Puis on calcule la taille de chaque case, en fonction de la taille de l'image, du nombre de cases, et de la bordure.
Notez qu'on s'assure que la taille des cases soit un multiple de 2 pour éviter des décalages dus à des arrondis.
Puis on dessine chaque mur (case contenant « X ») en fonction du contenu des cases adjacentes.

Pour chaque cas rencontré, on peut faire un petit dessin pour mieux visualiser le type de mur à dessiner.
Par exemple pour le troisième cas avec un mur dans la case.

Code du troisième cas avec mur dans la case

' Mur en haut et à droite mais pas ailleurs
If lUp = "X" And lRight = "X" And lLeft <> "X" And lDown <> "X" Then
.DrawEllipse lX + gTx / 2, lY - gTy / 2, gTx / 2, gTy / 2, _
1, -1, RGB(0, 0, 255), 3, , 180, -90

S'il y a un mur en haut et un mur à droite, mais pas de mur sur les autres cases adjacentes :

On doit donc dessiner un arc centré sur le point (lX + gTx / 2, lY - gTy / 2), qui démarre à 180° et qui parcours 90°.
On procède de même pour gérer tous les cas possibles.

On obtient enfin le dessin des murs.

Ensuite on dessine les pastilles.
Les pastilles sont dessinées dans le contenu statique, même si elles devront disparaître de l'affichage.
En effet, on ne va pas redessiner les pastilles à chaque affichage.
On supprimera les pastilles de ce contenu statique en noircissant la case au fur et à mesure qu'elles seront mangées.

Pour l'occasion on crée une variable en-tête de module qui contient le nombre de pastilles du niveau.
Cette variable est initialisée au chargement de l'écran avec le nombre de pastilles dessinées.
Puis elle sera décrémentée durant le jeu.
Une fois la variable égale à zéro, cela signifiera que toutes les pastilles auront été mangées.

Déclaration en en-tête de module clScreenGame

' Nombre de pastilles
Private gNbPastilles As Integer

Les pastilles sont représentées par des ellipses blanches.
Les super-pastilles ont un rayon plus important.

Dessin des pastilles dans Class_Initialize de clScreenGame, avant le End With

' On dessine les pastilles
' Pas de pastilles sur les bords du niveau, on commence à 1 et on termine 1 case avant la fin
' Initialise le décompte de pastille du niveau
gNbPastilles = 0
' Boucle sur le tableau
For lCptY = 1 To 28
For lCptX = 1 To 26
' Calcul du centre de la case pour y placer la pastille
GetCenter CSng(lCptX), CSng(lCptY), lX, lY
' Contenu de la case
lValue = GetValue(lCptX, lCptY)
Select Case lValue
' Il y a une pastille dans la case
Case "o"
' Ellipse de deux pixels de rayon
.DrawEllipse lX, lY, 2, 2, 1, vbWhite, vbWhite
gNbPastilles = gNbPastilles + 1
' Il y a une super-pastille dans la case
Case "O"
' Ellipse de quatre pixels de rayon
.DrawEllipse lX, lY, 4, 4, 1, vbWhite, vbWhite
gNbPastilles = gNbPastilles + 1
End Select
Next
Next
' On conserve le dessin du niveau avec ses murs et ses pastilles
.ImageKeep

Notez qu'on conserve ce contenu statique avec un appel à ImageKeep une fois le dessin terminé.

On obtient le dessin des murs et des pastilles.

XV-D - Chargement des ressources

A l'initialisation de l'écran, nous devons charger les ressources, c'est-à-dire les images et les sons.

Chargement des images et sons

' Chargement des images du PacMan
Dim lCpt As Integer
With oGdi.ImgNew("pac0")
.LoadFile ThisWorkbook.Path & "\images\pac0.png"
.Resize gTx
End With
For lCpt = 1 To 3
With oGdi.ImgNew("pacg" & lCpt)
.LoadFile ThisWorkbook.Path & "\images\pacg" & lCpt & ".png"
.Resize gTx
End With
With oGdi.ImgNew("pach" & lCpt)
.LoadFile ThisWorkbook.Path & "\images\pach" & lCpt & ".png"
.Resize gTx
End With
With oGdi.ImgNew("pacd" & lCpt)
.LoadFile ThisWorkbook.Path & "\images\pacd" & lCpt & ".png"
.Resize gTx
End With
With oGdi.ImgNew("pacb" & lCpt)
.LoadFile ThisWorkbook.Path & "\images\pacb" & lCpt & ".png"
.Resize gTx
End With
Next
' Chargement des images des fantômes en état "normal"
With oGdi.ImgNew("ghost1")
.LoadFile ThisWorkbook.Path & "\images\fantome.png"
With oGdi.ImgClone("ghost1", "ghost2")
.ReplaceColor vbRed, vbGreen
.Resize gTx
End With
With oGdi.ImgClone("ghost1", "ghost3")
.ReplaceColor vbRed, vbYellow
.Resize gTx
End With
.Resize gTx
End With
' Chargement de l'images des fantômes  en état "effrayé"
With oGdi.ImgNew("ghostscared")
.LoadFile ThisWorkbook.Path & "\images\fantomeeffraye.png"
.ReplaceColor vbBlack, vbBlack, , 0
End With
' Chargement de l'images des yeux de fantômes pour le retour au centre
With oGdi.ImgNew("yeux")
.LoadFile ThisWorkbook.Path & "\images\yeux.png"
.ReplaceColor vbBlack, vbBlack, , 0
End With
' Chargement des sons
oSound.OpenSound ThisWorkbook.Path & "\sons\aie.wav", "aie"
oSound.OpenSound ThisWorkbook.Path & "\sons\aouch.wav", "aouch"
oSound.OpenSound ThisWorkbook.Path & "\sons\blip.wav", "blip"

Les images sont chargées dans l'objet oGdi.
Chaque image est identifiée par une chaîne de caractères.
On redimensionne au chargement chaque image à la taille d'une case (avec le paramètre pWidth = gTx).
Pour les fantômes, on génère trois images, en modifiant la couleur rouge pour obtenir 3 fantômes de couleur différente.

Attention à modifier la couleur avant le redimensionnement qui, avec le lissage de l'image va un peu mélanger les couleurs.
Les sons sont chargés dans l'objet oSound, identifiés également par une chaîne de caractères.

XV-E - Création de du module objet clSprite

Nous allons dans le jeu avoir à gérer deux types d'objets : le PacMan et les fantômes.
Ces objets sont similaires, ce sont tout simplement des sprites.

Les données du Pacman et de chaque fantôme seront stockées dans un objet de type clSprite qui contient :

- leur position ;

- leur mouvement ;

- le mouvement suivant ;

- leur vitesse ;

- un compteur d'image (utilisé pour l'animation du PacMan et pour les couleurs de fantômes).

Créez donc un module de classe nommé clSprite.

Code du module clSprite

'***************************************************************************************
'*                             CLASSE POUR SPRITE
'***************************************************************************************
Option Explicit

' Position
Public X As Currency
Public Y As Currency
' Mouvement (g,d,h,b, ou vide si arrêté)
Public Mvt As String
' Mouvement en attente (entre deux cases)
Public MvtSuivant As String
' Numéro de l'image
Public Img As Integer
' Vitesse
Public Vitesse As Currency

On utilise pour la position et la vitesse un type de données Currency car en VBA ce type permet de gérer correctement des valeurs non entières.
Les types Single ou Double ne conviendraient pas car VBA arrondi ces valeurs.

Ainsi un sprite en position X = 10.4 se trouvera entre la colonne 10 et 11.
Lorsque le sprite atteint la position 11, on sait que le sprite est au centre d'une colonne.

Les mouvements sont des lettres :

- g pour gauche ;

- d pour droite ;

- h pour haut ;

- b pour bas.

Il n'y a pas de mouvement possible en diagonale.

On utilise une variable MvtSuivant pour stocker le mouvement suivant.
Ainsi lorsque le joueur appuie sur une touche, on détecte le mouvement demandé que l'on stocke dans cette variable.
Le sprite continue son chemin jusqu'à atteindre une case « pleine » (positions X et Y entières) et on modifie alors son mouvement.
De cette manière le joueur peut demander le déplacement du sprite sans avoir à appuyer sur la touche de direction pile au moment où le sprite atteint le centre d'une case.

XV-F - Gestion du Pacman

Le Pacman est donc un objet de type clSprite.
Déclarez cet objet dans le module clScreenGame

Déclaration de l'objet PacMan

' Objet Pacman
Private oPacman As clSprite

A l'initialisation de l'écran de jeu, on va créer l'objet Pacman et lui donner ses paramètres par défaut.

Initialisation de l'objet PacMan dans Class_Initialize du module clScreenGame

' Initialisation du Pacman
Set oPacman = New clSprite
' Pac immobile par défaut
oPacman.Mvt = ""
oPacman.MvtSuivant = ""
' Initialise la position de départ
oPacman.X = 14
oPacman.Y = 22
' Image initial du Pac
oPacman.Img = 0
' Vitesse initiale du Pac
oPacman.Vitesse = 0.1

Au début du jeu, le PacMan est immobile.
Il n'y a pas de mouvement.
Par contre on initialise sa vitesse qui est d'un dixième de case.
On le place au départ vers le bas au centre (colonne 14, ligne 22).

Pour dessiner le PacMan à l'écran, on va placer le code de dessin dans la fonction ClScreen_UpdateScreen.
On commence par restaurer le contenu statique, puis on affichage l'image du PacMan.
L'affichage d'une image se fait à l'aide de la fonction DrawImg de l'objet oGdi.
On utilise la fonction GetCenter pour calculer la position du centre du PacMan, puis on affiche l'image.
(Notez qu'on a défini les paramètres de GetCenter en Currency pour pouvoir passer des paramètres non entiers).

Affichage du PacMan dans ClScreen_UpdateScreen du module clScreenGame

' Position sur le dessin
Dim lX As Long, lY As Long
' Affichage
' Reprend l'image de la mémoire
oGdi.ImageReset
' Calcule le centre du PacMan
GetCenter oPacman.X, oPacman.Y, lX, lY
' Dessine le PacMan
oGdi.DrawImg "pac" & IIf(oPacman.Img=0, "", oPacman.Mvt) & oPacman.Img  , lX, lY, , , , GdipSizeModeautosize

Dans la fonction DrawImg, le paramètre GdipSizeModeautosize signifie qu'on affiche l'image sans redimensionnement et centrée sur les deux premières coordonnées.

L'image du PacMan à afficher est fonction de son mouvement.

Si le PacMan est en mouvement (paramètre Mvt non vide, différent de "") alors l'image du PacMan est pac[Mvt][Img].

Si le PacMan est immobile, l'image est pac0.

Par exemple pour un mouvement vers la droite, les images seront :

- pac0 ;

- pacd1 ;

- pacd2 ;

- pacd3.

Si on affiche le formulaire, on visualise alors le PacMan, sagement immobile.

Il faut maintenant tester l'appui de touche afin de modifier le mouvement du PacMan.

Test des commandes dans ClScreen_UpdateCommand du module clScreenGame

Dim lDown As Boolean, lUp As Boolean, lLeft As Boolean, lRight As Boolean
' Test directions du joystick
oCommand.TestJoypadDir lLeft, lRight, lUp, lDown
' Test des commandes de déplacement du Pacman
If oCommand.TestKey(oSharedData.ToucheBas) Or lDown Then
oPacman.MvtSuivant = "b"
End If
If oCommand.TestKey(oSharedData.ToucheDroite) Or lRight Then
oPacman.MvtSuivant = "d"
End If
If oCommand.TestKey(oSharedData.ToucheGauche) Or lLeft Then
oPacman.MvtSuivant = "g"
End If
If oCommand.TestKey(oSharedData.ToucheHaut) Or lUp Then
oPacman.MvtSuivant = "h"
End If

Comme prévu, on modifie le paramètre MvtSuivant de l'objet Pacman.

Dans la fonction de mise à jour de l'écran, on modifie la position du PacMan en fonction de son mouvement.
Puis on teste si le PacMan est sur une case « pleine » (positions entière) pour remplacer le mouvement en cours par le mouvement suivant.
On effectue le déplacement des sprites avant leur dessin, donc en début de fonction.

Déplacement du Pacman dans ClScreen_UpdateScreen (au début) du module clScreenGame

' Déplacement du PacMan en fonction de son mouvement et sa vitesse
Select Case oPacman.Mvt
Case "d"
oPacman.X = oPacman.X + oPacman.Vitesse
Case "g"
oPacman.X = oPacman.X - oPacman.Vitesse
Case "h"
oPacman.Y = oPacman.Y - oPacman.Vitesse
Case "b"
oPacman.Y = oPacman.Y + oPacman.Vitesse
End Select
' Si le PacMan atteint une case "pleine" => modifie le mouvement en fonction du mouvement suivant demandé
If oPacman.X = Int(oPacman.X) And oPacman.Y = Int(oPacman.Y) Then
' Mouvement suivant
oPacman.Mvt = oPacman.MvtSuivant
End If

Il manque l'animation du PacMan.

Animation du Pacman dans ClScreen_UpdateScreen du module clScreenGame

' Animation du PacMan
' On tourne sur 4 images
If oPacman.Mvt = "" Then
' Si PacMan immobile => image 0
oPacman.Img = 0
Else
oPacman.Img = (oPacman.Img + 1) Mod 4
End If

Le déplacement du PacMan fonctionne correctement, sauf qu'il peut traverser les murs.

Il faut donc tester s'il y a un mur dans la direction du mouvement suivant et annuler le mouvement si nécessaire.
On empêche également que le PacMan ne traverse la porte au centre d'où les fantômes sortent.

Et enfin on en profite pour gérer le passage d'un côté à l'autre du jeu si le Pacman est sur une case contenant « P » (pour simplifier le tutoriel, on met les valeurs du passage « en dur » = colonne 27 ou 0).

On a besoin de déclarer quelques variables qui contiendront le contenu des cases.

Déclarations pour valeurs des cases dans ClScreen_UpdateScreen

' Valeur des cases et des alentours
Dim lCase As String
Dim lUp As String
Dim lDown As String
Dim lRight As String
Dim lLeft As String

Empêche le Pacman de traverser les murs

' Si le PacMan atteint une case "pleine" => modifie le mouvement en fonction du mouvement suivant demandé
If oPacman.X = Int(oPacman.X) And oPacman.Y = Int(oPacman.Y) Then
' Contenu des cases
lCase = GetValue(oPacman.X, oPacman.Y)
lRight = GetValue(oPacman.X + 1, oPacman.Y)
lLeft = GetValue(oPacman.X - 1, oPacman.Y)
lUp = GetValue(oPacman.X, oPacman.Y - 1)
lDown = GetValue(oPacman.X, oPacman.Y + 1)
' Passage d'un côté à l'autre du jeu
If lCase = "P" Then
If oPacman.X = 0 Then
oPacman.X = 27
ElseIf oPacman.X = 27 Then
oPacman.X = 0
End If
End If
' Mouvement suivant
oPacman.Mvt = oPacman.MvtSuivant
' Teste si le mouvement suivant est possible
If oPacman.Mvt = "d" And lRight = "X" Then
oPacman.Mvt = ""
End If
If oPacman.Mvt = "g" And lLeft = "X" Then
oPacman.Mvt = ""
End If
If oPacman.Mvt = "b" And (lDown = "X" Or lDown = "_") Then
oPacman.Mvt = ""
End If
If oPacman.Mvt = "h" And lUp = "X" Then
oPacman.Mvt = ""
End If
End If

Le Pacman peut maintenant être déplacé correctement dans tout le jeu.

XV-G - Gestion des pastilles

Les pastilles sont mangées par le PacMan.
Lorsqu'une pastille est mangée, on noirci la case en noir sur le contenu statique puis on resauvegarde ce contenu.

Il faut donc déplacer l'appel à la fonction ImageReset en début de fonction ClScreen_UpdateScreen, après les déclarations.

En effet on va :

- d'abord restaurer le contenu statique ;

- puis tester si le PacMan mange une pastille et la supprimer de l'affichage ;

- et enfin on dessinera les sprites.

A déplacer en début de fonction ClScreen_UpdateScreen du module clScreenGame

' Reprend l'image de la mémoire
oGdi.ImageReset

Lorsque le Pacman atteint une case pleine, on va alors tester si c'est une pastille et la supprimer de l'affichage.

On supprime également la pastille du tableau, et on joue un son.
Et enfin on décrémente le compteur de pastille et on ajoute 1 au score.

Pour le score, il faut ajouter une variable partagée dans clSharedData.

Déclaration du score dans clSharedData

' Score
Public Score As Long

Suppression d'une pastille dans clScreenGame

' Si le PacMan atteint une case "pleine" => modifie le mouvement en fonction du mouvement suivant demandé
If oPacman.X = Int(oPacman.X) And oPacman.Y = Int(oPacman.Y) Then
' Contenu des cases
lCase = GetValue(oPacman.X, oPacman.Y)
lRight = GetValue(oPacman.X + 1, oPacman.Y)
lLeft = GetValue(oPacman.X - 1, oPacman.Y)
lUp = GetValue(oPacman.X, oPacman.Y - 1)
lDown = GetValue(oPacman.X, oPacman.Y + 1)
' Mange une pastille
If lCase = "o" Or lCase = "O" Then
' Calcule le centre de la pastille
GetCenter oPacman.X, oPacman.Y, lX, lY
' Noirci la case
oGdi.DrawRectangle lX - gTx / 2, lY - gTy / 2, lX + gTx / 2, lY + gTy / 2, vbBlack, vbBlack
' Supprime la pastille du tableau
gTableau(oPacman.X, oPacman.Y) = ""
' Joue un son
oSound.PlaySound "blip"
' Décrémente le compteur de pastille
gNbPastilles = gNbPastilles - 1
' Incrémente le score
oSharedData.Score = oSharedData.Score + 1
' Sauvegarde le contenu "statique"
oGdi.ImageKeep
End If
[...]

Pour visualiser le score, on ajoute un code de dessin à la fin de la fonction ClScreen_UpdateScreen.

Affichage du score à la fin de ClScreen_UpdateScreen

' Affiche le score
oGdi.DrawText "Score : " & oSharedData.Score, 20, , 0, 0, _
oGdi.ImageWidth, oGdi.ImageHeight, 2, 2, vbRed, , _
RGB(255, 255, 100)

Le Pacman peut maintenant manger les pastilles, et on voit le score en bas à droite s'incrémenter à chaque pastille.

XV-H - Gestion des fantômes

Les fantômes sont également des objets de type clSprite.
Mais il y a plusieurs fantômes, dont le nombre est défini dans les options.
Nous allons donc déclarer et initialiser une collection de fantômes.

Déclaration de la collection de fantômes en en-tête du module clScreenGame

' Collection de fantômes
Private oCollFantomes As Collection

Déclarations nécessaires dans Class_Initialize de l'écran de jeu

' Compteur
Dim lCpt As Long
' Objet Fantome
Dim lFantome As clSprite

Initialisation de la collection de fantômes à l'initialisation de l'écran de jeu dans Class_Initialize

' Initialisation des fantômes
Set oCollFantomes = New Collection
If oCollFantomes Is Nothing Then
For lCpt = 1 To oSharedData.NbFantomes
Set lFantome = New clSprite
' Position initiale des fantôme
lFantome.X = Int(11 + Rnd * 6) ' Colonne entre 11 et 16
lFantome.Y = 14 ' Ligne 14
' Image (on tourne sur trois images pour changer la couleur)
lFantome.img = 1 + (lCpt Mod 3)
' Vitesse initiale des fantômes
lFantome.Vitesse = 0.1
' Ajout à la collection
oCollFantomes.Add lFantome
Next
End If

Si on affiche le formulaire on obtient une erreur d'exécution '91' : Variable objet ou variable de bloc With non définie.
Cette erreur est levée lorsqu'on essaye de lire le nombre de fantôme de l'objet oSharedData.
En effet à l'initialisation de l'écran, l'objet oSharedData n'est pas encore affecté.
Et malheureusement VBA ne permet pas de passer des paramètres au constructeur de la classe.
Il faut alors déplacer ce code d'initialisation des fantômes dans la fonction ClScreen_UpdateScreen, au début après les déclarations.
On vérifie que la collection de fantômes n'est pas initialisée afin de ne le faire qu'une seule fois.

Déclarations à déplacer dans la fonction ClScreen_UpdateScreen

' Compteur
Dim lCpt As Long
' Objet Fantome
Dim lFantome As clSprite

Initialisation de la collection de fantômes, à déplacer dans la fonction ClScreen_UpdateScreen

' Initialisation des fantômes
If oCollFantomes Is Nothing Then ' une seule initialisation
Set oCollFantomes = New Collection
For lCpt = 1 To oSharedData.NbFantomes
Set lFantome = New clSprite
' Position initiale des fantôme
lFantome.X = Int(11 + Rnd * 6) ' Colonne entre 11 et 16
lFantome.Y = 14 ' Ligne 14
' Image (on tourne sur trois images)
lFantome.Img = 1 + (lCpt Mod 3)
' Vitesse initiale des fantômes
lFantome.Vitesse = 0.1
' Ajout à la collection
oCollFantomes.Add lFantome
Next
End If

On va ensuite gérer le déplacement et l'affichage des fantômes après le déplacement et l'affichage du Pacman dans la fonction ClScreen_UpdateScreen.
Etant donné qu'il y a plusieurs fantômes, on doit faire une boucle pour gérer chaque fantôme de la collection.

Comme pour le Pacman, on va d'abord déplacer le fantôme en fonction de sa vitesse.
Puis si le fantôme est sur une case « pleine » (positions entières), on recherche le mouvement à affecter au fantôme.
Dans le cas des fantômes, on n'utilise pas le paramètre MvtSuivant qui n'aurait pas d'intérêt.

Le mouvement des fantômes doit répondre aux règles suivantes :

- Le fantôme doit se rapprocher du PacMan => on recherche donc l'axe où la distance entre le fantôme et le PacMan est la plus grande (X ou Y).

- Si le fantôme est sur la porte de sortie (repérée "_"), alors il se déplace vers le haut (afin d'éviter de sortir et rentrer sans cesse).

- Le fantôme ne peut pas rentrer par la porte au centre de l'écran (repérée »_"), il ne peut que sortir.

- Si on n'a pas pû déterminer le déplacement optimal du fantôme, on lui donne un mouvement aléatoire.

- Le fantôme ne peut pas se déplacer sur un mur ("X »), ou sur un des 2 passages ("P").

Et enfin on dessine le fantôme, de la même manière qu'on a dessiné le PacMan.
Pour un fantôme, l'image est ghost1, ghost2 ou ghost3 en fonction du paramètre Img du fantôme.

Déclaration nécessaires au début de ClScreen_UpdateScreen

' Variable pour numéro de fantôme
Dim lCalc as long

Gestion des déplacements des fantômes dans ClScreen_UpdateScreen

' Gestion des fantômes
For Each lFantome In oCollFantomes
' Déplacement des fantômes
Select Case lFantome.Mvt
Case "d"
lFantome.X = lFantome.X + lFantome.Vitesse
Case "g"
lFantome.X = lFantome.X - lFantome.Vitesse
Case "h"
lFantome.Y = lFantome.Y - lFantome.Vitesse
Case "b"
lFantome.Y = lFantome.Y + lFantome.Vitesse
End Select
' Test si le fantôme est sur une case "pleine"
If Int(lFantome.X) = lFantome.X And Int(lFantome.Y) = lFantome.Y Then
' Lecture du contenu des cases aux alentours du fantôme
lRight = GetValue(lFantome.X + 1, lFantome.Y)
lLeft = GetValue(lFantome.X - 1, lFantome.Y)
lUp = GetValue(lFantome.X, lFantome.Y - 1)
lDown = GetValue(lFantome.X, lFantome.Y + 1)
' Pas de mouvement par défaut
lFantome.Mvt = ""
' Recherche du mouvement pour se rapprocher du PacMan
' Si distance X plus grand que distance Y
If Abs(lFantome.X - oPacman.X) > Abs(lFantome.Y - oPacman.Y) Then
If lFantome.X < oPacman.X And lRight <> "X" And lRight <> "P" Then
lFantome.Mvt = "d"
ElseIf lFantome.X >= oPacman.X And lLeft <> "X" And lLeft <> "P" Then
lFantome.Mvt = "g"
End If
End If
' Si distance Y plus grand que distance X ou que le cas précédent n'a pas donné de mouvement possible
If lFantome.Mvt = "" Then
If lFantome.Y < oPacman.Y And lDown <> "X" And lDown <> "_" Then
lFantome.Mvt = "b"
ElseIf lFantome.Y >= oPacman.Y And lUp <> "X" Then
lFantome.Mvt = "h"
End If
End If
' Si on est sur la porte de sortie => déplacement vers le haut
If GetValue(lFantome.X, lFantome.Y) = "_" Then lFantome.Mvt = "h"
' Si fantôme immobile => mouvement aléatoire pour le lancer
If lFantome.Mvt = "" Then
lCalc = 1 + Int(Rnd * 4)
Select Case lCalc
Case 1: lFantome.Mvt = "d"
Case 2: lFantome.Mvt = "g"
Case 3: lFantome.Mvt = "h"
Case 4: lFantome.Mvt = "b"
End Select
End If
' Teste si le mouvement est possible, sinon on l'annule
If lFantome.Mvt = "d" And (lRight = "X" Or lRight = "P") Then
lFantome.Mvt = ""
End If
If lFantome.Mvt = "g" And (lLeft = "X" Or lLeft = "P") Then
lFantome.Mvt = ""
End If
If lFantome.Mvt = "b" And (lDown = "X" Or lDown = "_") Then
lFantome.Mvt = ""
End If
If lFantome.Mvt = "h" And lUp = "X" Then
lFantome.Mvt = ""
End If
End If
' Calcule le centre du fantôme
GetCenter lFantome.X, lFantome.Y, lX, lY
' Dessine le fantôme
oGdi.DrawImg "ghost" & lFantome.Img, lX, lY, , , , GdipSizeModeautosize
Next

Testez l'affichage du jeu, on voit bien les fantômes se déplacer.
Les fantômes sont plutôt stupides dans leur déplacement, mais il ne faudrait pas les rendre trop intelligents si on veut que le jeu soit jouable.

XV-I - Super-pastille : fantômes effrayés

Un élément essentiel du jeu est le changement d'état des fantômes.

Lorsque le Pacman mange une super-pastille (repérée par un « O » majuscule), les fantômes doivent changer d'état :

- ils deviennent bleus ;

- ils se déplacent plus lentement et fuient le PacMan ;

- ils sont mangeables.

Cet état est temporaire, on le fait durer 15 secondes.

Ajoutons un compteur pour l'état effrayé pour les objets fantômes

Ajout de l'état effrayé au module clSprite

' Etat effrayé = temps restant
Public Scared As long

Lorsque le PacMan mangera une super-pastille, on initialise ce compteur.
On fait ce test après le premier test sur l'arrivé sur une pastille.

Initialisation du compteur d'état effrayé dans ClScreen_UpdateScreen

' Mange une pastille
If lCase = "o" Or lCase = "O" Then
[...] ' Test déjà écrit, ajouter le test de super-pastille après celui-ci
End If
' Mange une super-pastille
If lCase = "O" Then
' Pour chaque fantôme
For Each lFantome In oCollFantomes
' Passe le fantôme à l'état effrayé durant 15 secondes
' On multiplie par 40 car 40 images par secondes
lFantome.Scared = 40 * 15
Next
End If

Ensuite dans le code de gestion des fantômes, on décrémente le compteur s'il est supérieur à zéro.

Décrémente le compteur d'état effrayé dans ClScreen_UpdateScreen

' Gestion des fantômes
For Each lFantome In oCollFantomes
' Décrémente le compteur d'état effrayé
If lFantome.Scared > 0 Then lFantome.Scared = lFantome.Scared - 1
[...]

Et on modifie la recherche de mouvement des fantômes pour les éloigner du PacMan s'ils sont effrayés.

Modifie la recherche de mouvement du fantôme dans ClScreen_UpdateScreen

' Pas de mouvement par défaut
lFantome.Mvt = ""
' Recherche du mouvement pour se rapprocher du PacMan
If lFantome.Scared = 0 Then
' Si distance X plus grand que distance Y
If Abs(lFantome.X - oPacman.X) > Abs(lFantome.Y - oPacman.Y) Then
If lFantome.X < oPacman.X And lRight <> "X" And lRight <> "P" Then
lFantome.Mvt = "d"
ElseIf lFantome.X >= oPacman.X And lLeft <> "X" And lLeft <> "P" Then
lFantome.Mvt = "g"
End If
End If
' Si distance Y plus grand que distance X ou que le cas précédent n'a pas donné de mouvement possible
If lFantome.Mvt = "" Then
If lFantome.Y < oPacman.Y And lDown <> "X" And lDown <> "_" Then
lFantome.Mvt = "b"
ElseIf lFantome.Y >= oPacman.Y And lUp <> "X" Then
lFantome.Mvt = "h"
End If
End If
' Ou pour s'éloigner si le fantome est en état effrayé
Else
' Si distance X plus petite que distance Y
If Abs(lFantome.X - oPacman.X) < Abs(lFantome.Y - oPacman.Y) Then
If lFantome.X > oPacman.X And lRight <> "X" And lRight <> "P" Then
lFantome.Mvt = "d"
ElseIf lFantome.X <= oPacman.X And lLeft <> "X" And lLeft <> "P" Then
lFantome.Mvt = "g"
End If
End If
' Si distance Y plus petite que distance X ou que le cas précédent n'a pas donné de mouvement possible
If lFantome.Mvt = "" Then
If lFantome.Y > oPacman.Y And lDown <> "X" And lDown <> "_" Then
lFantome.Mvt = "b"
ElseIf lFantome.Y <= oPacman.Y And lUp <> "X" Then
lFantome.Mvt = "h"
End If
End If
End If

Et enfin modifiez le code de dessin des fantômes pour afficher l'image fantomeeffraye si nécessaire.

Modification du dessin des fantômes ClScreen_UpdateScreen

' Dessine le fantôme
oGdi.DrawImg "ghost" & IIf(lFantome.Scared > 0, "scared", lFantome.Img), lX, lY, , , , GdipSizeModeautosize

Les fantômes sont maintenant bleus durant 15 secondes lorsque le PacMan mange une super-pastille.
Et ils s'éloignent au lieu de se rapprocher.
Par contre leur vitesse n'est pas modifiée.

Lors de la modification de la vitesse il faut bien faire attention.
En effet on teste l'arrivée du sprite sur une case en testant si les positions sont entières.
En modifiant la vitesse, on risque de ne plus arriver à des valeurs entières.

Par exemple :

La position X est égale à 9.95 et on modifie la vitesse de 0.05 à 0.1.
La position suivante vaudra alors 10.05 sans passer par la valeur entière 10.
Il faut donc ajuster la position en fonction de la vitesse définie.
Pour modifier la position lors de la modification de la vitesse, on va transformer la variable publique Vitesse en propriété.

Modification de la déclaration de Vitesse dans clSprite

Private gVitesse As Currency

Propriété pour lecture et écriture de la vitesse dans clSprite

' Propriété pour lecture et écriture de la vitesse
Public Property Get Vitesse() As Currency
Vitesse = gVitesse
End Property
Public Property Let Vitesse(pVitesse As Currency)
gVitesse = pVitesse
' Si vitesse non nulle
If gVitesse <> 0 Then
' Déplace légérement le sprite pour se repositionner sur un multiple de la vitesse
X = Int(X) + Int((X - Int(X)) / gVitesse) * gVitesse
Y = Int(Y) + Int((Y - Int(Y)) / gVitesse) * gVitesse
End If
End Property

Pour que cela fonctionne, la vitesse doit être un dividende de 1 (exemple : 0.2 ou 0.1).
Une vitesse de 0.3 ou de 0.04 ne conviendrait pas.

Le PacMan a une vitesse constante de 0.1.
Les fantômes ont une vitesse de 0.1 en état normal, et de 0.05 en état effrayé.

La modification de la vitesse des fantômes va se faire également dans le module clSprite.
Lors de la modification du compteur Scared, on va modifier la vitesse des fantômes.
Pour cela il faut tranformer la variable publique Scared en propriété.

Modification de la déclaration de Scared dans clSprite

' Etat effrayé
Private gScared As Long

Propriété pour lecture et écriture du compteur d'état effrayé dans clSprite

' Propriété pour lecture et écriture de l'état effrayé
Public Property Get Scared() As Long
Scared = gScared
End Property
Public Property Let Scared(pScared As Long)
If gScared = 0 And pScared <> 0 Then
' Passage à l'état effrayé => on passe la vitesse de 0.1 à 0.05
Vitesse = gVitesse / 2
ElseIf gScared <> 0 And pScared = 0 Then
' Passage à l'état normal => on repasse la vitesse de  à 0.1
Vitesse = gVitesse * 2
End If
gScared = pScared
End Property

Le passage des fantômes de l'état effrayé à l'état normal et vice-versa est terminé.

XV-J - Gestion des collisions entre les sprites

Pour simplifier, il n'y a pas de gestion de collisions entre les fantômes : ils se traversent.
Par contre il faut gérer la collision entre le PacMan et les fantômes.

On simplifie également la détection de collision entre les sprites en détectant la collision entre deux rectangles.
Il faudrait vérifier si un des 4 coins du rectangle encadrant un fantome se trouve dans le rectangle encadrant le PacMan.
Plutôt que de tester toutes ces coordonnées (ce ne serait cependant pas très compliqué), on va utiliser les régions de gdi+.

Lors du dessin des sprites (fantôme et PacMan), on va ajouter une région correspondant au rectangle dans lequel le sprite est dessiné.
Cela se fait simplement en rajoutant un paramètre à l'appel de la fonction DrawImg de l'objet oGdi.
Pour identifier le sprite, on va utiliser le pointeur de l'objet qui est déterminé avec ObjPtr.
Ce pointeur est garanti unique.

Modification du dessin du PacMan pour ajouter une région

' Dessine le PacMan et ajoute une région rectangulaire
oGdi.DrawImg "pac" & IIf(oPacman.img = 0, "", oPacman.Mvt) & oPacman.img, lX, lY, , , , _
GdipSizeModeAutoSize, , , CStr(ObjPtr(oPacman))

Modification du dessin des fantômes pour ajouter une région

' Dessine le fantôme et ajoute une région rectangulaire
oGdi.DrawImg "ghost" & IIf(lFantome.Scared > 0, "scared", lFantome.img), lX, lY, , , , _
GdipSizeModeAutoSize, , , CStr(ObjPtr(lFantome))

On va ensuite rechercher une éventuelle collision après avoir dessiner les sprites.
On profite de la boucle déjà en place sur la collection de fantômes.
Les collisions sont testées à l'aide de la fonction RegionsIntersect de l'objet oGdi.
Pour tester rapidement, on écrit un texte en bas à gauche lorsque le PacMan touche un fantôme.

Test de collision

' Gestion des fantômes
For Each lFantome In oCollFantomes
[...]
' Détection de collision avec le PacMan
If oGdi.RegionsIntersect(ObjPtr(oPacman), ObjPtr(lFantome)) Then
oGdi.DrawText "Collision", 20, , 0, 0, oGdi.ImageWidth, oGdi.ImageHeight, 0, 2, vbRed, , RGB(255, 255, 100), , , , , , True
End If
Next

Affichez le formulaire et allez au contact d'un fantôme pour visualiser la collision.

Que faire en cas de collision ?

Si le fantôme est en état normal => le PacMan est mangé => on affiche l'écran de jeu perdu.
Si le fantôme est en état effrayé => le fantôme est mangé => il doit retourner au centre.

Dans les deux cas, on joue un son.

Pour l'état normal c'est facile :

Le fantôme mange le PacMan

' Détection de collision avec le PacMan
If oGdi.RegionsIntersect(ObjPtr(oPacman), ObjPtr(lFantome)) Then
If lFantome.Scared = 0 Then
' Le fantôme est en état normal => il mange le PacMan
' On joue un son
oSound.PlaySound "aouch"
' On demande le changement d'écran vers l'écran de jeu perdu
gNextScreen = "gameover"
gChangeScreen = True
Else
' Le fantôme est en état effrayé => il est mangé
' Code à suivre
End If
End If

Pour l'état effrayé c'est plus compliqué :
Il faut définir une nouvelle propriété des sprites : un indicateur de retour au centre.
Lorsque cet indicateur est à Vrai :

- les fantômes doivent suivre le chemin pour retourner au centre ;

- ils sont accélérés pour un retour rapide ;

- seuls leurs yeux s'affichent ;

- ils ne doivent pas être remangés ou et ne doivent pas pouvoir manger le PacMan ;

- ils reviennent ensuite à l'état normal.

Déclaration de l'indicateur de retour au centre dans clSprite

' Indicateur pour retour au centre
Private gReturnToCentre As Boolean

Propriétés pour l'indicateur de retour au centre dans clSprite

' Propriété pour lecture et écriture du retour au centre
Public Property Get ReturnToCentre() As Boolean
ReturnToCentre = gReturnToCentre
End Property
Public Property Let ReturnToCentre(pReturnToCentre As Boolean)
If gReturnToCentre = False And pReturnToCentre = True Then
' Met le flag de retour au centre => on passe la vitesse à 0.25
Vitesse = 0.25
ElseIf ReturnToCentre = True And pReturnToCentre = False Then
' Annule le flag de retour au centre => on repasse la vitesse à 0.1
Vitesse = 0.1
End If
gReturnToCentre = pReturnToCentre
End Property

Modifions ensuite la détection des collisions.
Notez qu'on ajoute 50 points au score par fantôme mangé.

Modification de la détection des collisions

' Détection de collision avec le PacMan
' Pas de détection si retour au centre
If Not lFantome.ReturnToCentre And oGdi.RegionsIntersect(ObjPtr(oPacman), ObjPtr(lFantome)) Then
If lFantome.Scared = 0 Then
' Le fantôme est en état normal => il mange le PacMan
' On joue un son
oSound.PlaySound "aouch"
' On demande le changement d'écran vers l'écran de jeu perdu
gNextScreen = "gameover"
gChangeScreen = True
Else
' Le fantôme est en état effrayé => il est mangé
' On joue un son
oSound.PlaySound "aie"
' Incrémente le score de 50
oSharedData.Score = oSharedData.Score + 50
' Fin de l'état effrayé
lFantome.Scared = 0
' Retour au centre
lFantome.ReturnToCentre = True
End If
End If

Occupons maintenant du retour au centre des fantômes.

XV-K - Retour au centre des fantômes

Les déplacements à suivre pour le retour au centre ont été définis dans la feuille Chemin.
De la même manière que pour les données du tableau de la feuille Jeu, nous allons charger ces données dans un tableau.

Déclaration en en-tête de clScreenGame

' Tableau de retour au centre
Private gChemin(0 To 27, 0 To 29) As String

Chargement des déplacements de retour au centre dans Class_Initialize

' A insérer après le chargement de gTableau

' Charge les données du tableau de retour au centre
With ThisWorkbook.Sheets("Chemin")
For lCptX = 1 To 28
For lCptY = 1 To 30
gChemin(lCptX - 1, lCptY - 1) = .Cells(lCptY, lCptX).Value
Next
Next
End With

Ensuite on gère les déplacements dans le cas où ReturnToCentre est Vrai.

Déplacement de retour au centre dans ClScreen_UpdateScreen

' Test si le fantôme est sur une case "pleine"
If Int(lFantome.X) = lFantome.X And Int(lFantome.Y) = lFantome.Y Then
' Pas de retour au centre
If Not lFantome.ReturnToCentre Then
' =====> Ici on trouve le code de déplacement précédemment écrit
Else
' Retour au centre
lFantome.Mvt = gChemin(lFantome.X, lFantome.Y)
' Si plus de déplacement => on est arrivé au centre
If lFantome.Mvt = "" Then lFantome.ReturnToCentre = False
End If
End If

Et enfin on affiche la paire d'yeux lors du retour au centre.

Affichage de la paire d'yeux lors du retour au centre dans ClScreen_UpdateScreen

' Dessine le fantôme et ajoute une région rectangulaire
If lFantome.ReturnToCentre Then
oGdi.DrawImg "yeux", lX, lY, , , , _
GdipSizeModeAutoSize, , , CStr(ObjPtr(lFantome))
Else
oGdi.DrawImg "ghost" & IIf(lFantome.Scared > 0, "scared", lFantome.img), lX, lY, , , , _
GdipSizeModeAutoSize, , , CStr(ObjPtr(lFantome))
End If

On peut alors tester le retour au centre des fantômes :

XV-L - Fin du jeu

Il nous reste une dernière chose : tester si les pastilles ont été toutes mangées.
A la fin de la procédure de mise à jour du jeu (ClScreen_UpdateScreen), on teste si le compteur de pastille est à zéro.
Si c'est le cas, on bascule vers l'écran de jeu gagné.

Test de fin du jeu à la fin de ClScreen_UpdateScreen

' Test de fin du jeu
If gNbPastilles = 0 Then
gNextScreen = "gameend"
gChangeScreen = True
End If

Si on mange toutes les pastilles, le jeu se termine.

Prochaines étapes : la création des écrans de fin de jeu :

- écran de jeu perdu ;

- écran de jeu gagné ;

- écran des scores ;

- écran des crédits.

XVI - Développement de l'écran de jeu perdu

Préparez le module clScreenGameOver de la même manière que le module clScreenMenu au chapitre Préparation du premier écran : le menu clScreenMenu.

Pensez à gérer l'affichage et la minuterie (10 images par seconde suffisent).

Affichage et minuterie dans clScreenGameOver

'------------------------------------------------------------------------
' Affichage de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_DisplayScreen() As Boolean
' Dessine l'image sur le formulaire
oGdi.RepaintNoFormRepaint FormGame.Img
End Function

'------------------------------------------------------------------------
' Minuterie d'attente
'------------------------------------------------------------------------
Private Function ClScreen_ScreenWait() As Boolean
' Attente de 1000 / 10 = 100 ms pour affichage de 10 images/s
oGdi.Wait 100
End Function

Il faut ensuite gérer l'enchaînement de l'écran de jeu.

Dans la fonction GameChangeScreen du module clGame, il faut ajouter l'enchainement vers l'écran de jeu.

Ajoutez un Case au Select.

Enchainement vers écran de jeu perdu dans clGame

' Changement pour le jeu perdu
Case "gameover"
Set oScreen = Nothing
Set oScreen = New ClScreenGameOver

On teste la valeur « gameover » car dans l'écran de jeu c'est cette valeur qu'on utilise lorsque le PacMan est mangé.

A l'initialisation de l'écran, on défini l'écran des scores en écran suivant par défaut.

Ecran par défaut dans clGameOver

' Ecran suivant par défaut
gNextScreen = "scores"

A l'initialisation de l'écran, on écrit le texte de fin de jeu.

Dessin du contenu statique dans Class_Initialize

' Dessin du contenu statique
With oGdi
' Nouveau bitmap pour dessiner
.CreateBitmap .PointsToPixelsX(FormGame.Img.Width), .PointsToPixelsY(FormGame.Img.Height)
' Rempli l'image de gris
.FillColor RGB(150, 150, 150)
' Ecrit le texte jeu perdu en haut
.DrawText "JEU PERDU", 35, "Arial", 0, 20, .ImageWidth, 60, , , _
vbBlue, , vbYellow
' Sauvegarde l'image en mémoire
.ImageKeep
End With

A la mise à jour de l'écran, on écrit le score.
(On n'écrit pas le score à l'initialisation car on n'y a pas accès à l'objet oSharedData)

Affichage du score dans ClScreen_UpdateScreen

With oGdi
' Reprend l'image de la mémoire
.ImageReset
' Ecrit le score du joueur
.DrawText "SCORE : " & oSharedData.Score, 35, "Arial", 20, 80, .ImageWidth, 120, , 0, _
vbBlue, , vbYellow
End With

Il faut ensuite vérifier si on entre dans les meilleurs scores.
Pour l'instant les scores ne sont pas gérés.
On va créer dans le sous-dossier config un fichier scores.ini qui contiendra les 10 meilleurs scores.

fichier scores.ini avec scores initiaux

[1]
name=initial
score=10
[2]
name=initial
score=10
[3]
name=initial
score=10
[4]
name=initial
score=10
[5]
name=initial
score=10
[6]
name=initial
score=10
[7]
name=initial
score=10
[8]
name=initial
score=10
[9]
name=initial
score=10
[10]
name=initial
score=10

Pour chaque score il y a deux valeurs : le nom et le score.

Comme nous l'avions fait pour les options, nous allons créer des fonctions de gestion du fichier scores.ini.

Fonctions de gestion des scores dans le module ModFunctions

'------------------------------------------------------------------------
' Lecture de la valeur du score numéro pNum
' pKey = name ou score
'------------------------------------------------------------------------
Public Function ReadScore(pNum As Long, pKey As String) As String
Dim lRet As Long
Dim lData As String
Dim lSize As Long
lData = Space$(8192)
lSize = 8192
lRet = GetPrivateProfileString(CStr(pNum), pKey, "", lData, lSize, ThisWorkbook.path & "\config\scores.ini")
If lSize > 0 Then
ReadScore = Left$(lData, lRet)
Else
ReadScore = ""
End If
End Function

'------------------------------------------------------------------------
' Ecriture de la valeur du score numéro pNum
'------------------------------------------------------------------------
Public Function WriteScore(pNum As Long, pName As String, pScore As Long) As Boolean
Dim lRet As Long
Dim lSize As Long
lRet = WritePrivateProfileString(pNum, "name", pName, ThisWorkbook.path & "\config\scores.ini")
lRet = WritePrivateProfileString(pNum, "score", CStr(pScore), ThisWorkbook.path & "\config\scores.ini")
WriteScore = (lRet <> 0)
End Function

'---------------------------------------------------------------------------------------
' Vérifie si on entre dans les scores
'---------------------------------------------------------------------------------------
Public Function GetPosHiScore(pScore As Long) As Long
Dim lCpt As Long
' Recherche la ligne où insérer le score si assez élevé
For lCpt = 1 To 10
If pScore > ReadScore(CStr(lCpt), "score") Then
GetPosHiScore = lCpt
Exit For
End If
Next
End Function

'---------------------------------------------------------------------------------------
' Mise à jour des HighScore
'---------------------------------------------------------------------------------------
Public Sub UpdateHiScore(pPosition As Long, pName As String, pScore As Long)
Dim lCpt As Long
' Si entrée dans les scores
' Décale les scores vers le bas
For lCpt = 10 To pPosition + 1 Step -1
WriteScore CStr(lCpt), ReadScore(CStr(lCpt - 1), "name"), ReadScore(CStr(lCpt - 1), "score")
Next
' Ajoute les infos du nouveau score
WriteScore CStr(pPosition), pName, CStr(pScore)
End Sub

- ReadScore lit le nom ou le score numéro pNum.

- WriteScore écrit un score en position pNum.

- GetPosHiScore renvoit la position du score pScore dans les meilleurs scores.

- UpdateHiScore insère un nouveau score en position pNum.

Retour au module clScreenGameOver dans lequel nous avons besoin de déclarer deux nouvelles variables.

Variables en en-tête de clScreenGame

' Position dans les scores
Private gPosHiScore As Long
' Nom pour entrée dans les scores
Private gName As String

A l'ouverture de l'écran, on défini la valeur de gPosHiScore à -1 (la valeur de -1 nous indique qu'on doit déterminer la position, on ne le fera qu'une seule fois).

Dans Class_Initialize, initialise la valeur de gPosHiScore

' Position dans les scores à déterminer
gPosHiScore = -1

Ensuite dans la mise à jour de l'écran, on détermine la position dans les meilleurs scores.

Puis, toujours dans la mise à jour de l'écran, on écrit si nécessaire le texte indiquant au joueur qu'il doit entrer son nom.

Dans ClScreen_UpdateScreen, affiche le texte d'entrée de nom pour les scores

Static sClignote As Long
' Position dans les scores
If gPosHiScore = -1 Then gPosHiScore = GetPosHiScore(oSharedData.Score)
With oGdi
' Reprend l'image de la mémoire
.ImageReset
' Ecrit le score du joueur
.DrawText "SCORE : " & oSharedData.Score, 35, "Arial", 0, 80, .ImageWidth, 120, , 0, _
vbBlue, , vbYellow
' Edition du nom pour entrée dans les scores
If gPosHiScore > 0 Then
oGdi.DrawText "Entrée dans les meilleurs scores", 25, "Arial", _
20, 140, .ImageWidth, 180, 0, , vbRed
oGdi.DrawText "Nom du joueur : " & gName & IIf(sClignote < 5, "_", " "), 25, "Arial", _
20, 200, .ImageWidth, 240, 0, , vbBlack
End If
sClignote = (sClignote + 1) Mod 10
End With

On utilise une variable statique (qui n'est pas remise à zéro à chaque exécution de la fonction) pour faire clignoter un curseur.
Le curseur est un caractère de soulignement affiché la moitié du temps.

Pour terminer, on va modifier la gestion des commandes ClScreen_UpdateCommand.
Les touches acceptées pour l'entrée du nom du joueur sont les touches de lettres (A à Z) et de chiffres (0 à 9).
La touche de retour arrière supprime le dernier caractère.

Dans ClScreen_UpdateCommand, modifie le nom du joueur en fonction des touches appuyées

Dim lKey As Long
' Définition du nom pour les meilleurs scores
If gPosHiScore > 0 Then
' Touche appuyée
lKey = oCommand.KeyPressGetCode
' Test si touche = lettre ou chiffre
If (lKey >= vbKeyA And lKey <= vbKeyZ) Or (lKey >= vbKey0 And lKey <= vbKey9) Then
' Ajoute le caractère au nom
gName = gName & Chr(lKey)
End If
' Test si touche de retour arrière => suppression du dernier caractère
If lKey = vbKeyBack Then
If Len(gName) > 0 Then gName = Left(gName, Len(gName) - 1)
End If
End If

Enfin on bascule vers l'écran suivant sur validation (souris, clavier, ou joystick).
Si le joueur est entré dans les scores, on insère son score dans le fichier ini.

Bascule vers l'écran suivant et insertion du score sur validation dans ClScreen_UpdateCommand

' Test si validation
If oCommand.TestKey(oSharedData.ToucheValid, 500) Or _
oCommand.TestJoypadButton(oSharedData.JoyButton, 500) Then
' Si meilleur score à sauvegarder
If gPosHiScore > 0 Then
Call UpdateHiScore(gPosHiScore, gName, oSharedData.Score)
End If
' Demande le changement d'écran
gChangeScreen = True
End If

Mangez au moins 10 pastilles et allez à la rencontre d'un fantôme pour tester la mise à jour des scores.

XVII - Développement de l'écran de jeu gagné

Pour l'écran de jeu gagné, on va faire simple.
On copie-colle dans clScreenGameEnd le contenu du module clScreenGameOver.
Ensuite on modifie juste le texte du contenu statique :

Modification du texte

' Ecrit le texte jeu gagné en haut
.DrawText "JEU GAGNE", 35, "Arial", 0, 20, .ImageWidth, 60, , , _
vbBlue, , vbYellow

Il faut ensuite gérer l'enchaînement de l'écran.

Dans la fonction GameChangeScreen du module clGame, il faut ajouter l'enchainement vers l'écran de jeu gagné.

Ajoutez un Case au Select.

Enchainement vers écran de jeu gagné dans clGame

' Changement pour le jeu gagné
Case "gameend"
Set oScreen = Nothing
Set oScreen = New ClScreenGameEnd

XVIII - Développement de l'écran des scores

On ne va pas trop détailler la création de cet écran, la méthode est la même que pour les autres écrans.
L'écran suivant par défaut est l'écran des crédits :

Ecran suivant : écran des crédits par défaut

' Ecran suivant par défaut
gNextScreen = "credits"

Il faut ensuite gérer l'enchaînement de l'écran.

Dans la fonction GameChangeScreen du module clGame, il faut ajouter l'enchainement vers l'écran des scores.

Ajoutez un Case au Select.
Cet enchaînement est un peu particulier car si on affiche les scores à partir du menu, on souhaite revenir au menu.
On teste donc le type d'écran en cours avant de basculer vers l'écran suivant, puis on change le nom de l'écran suivant pour retourner au menu le cas échéant.

Enchainement vers écran des scores dans clGame

' Variable pour écran suivant
Dim lNextScreen As String
[...]
' Changement pour les scores
Case "scores"
If TypeOf oScreen Is ClScreenMenu Then
lNextScreen = "menu"
End If
Set oScreen = Nothing
Set oScreen = New clScreenScores
If lNextScreen <> "" Then oScreen.NextScreen = lNextScreen

On pense à changer d'écran sur validation.

Bascule vers l'écran suivant si validation dans clScreenScores

' Test si validation
If oCommand.TestKey(oSharedData.ToucheValid, 500) Or _
oCommand.TestJoypadButton(oSharedData.JoyButton, 500) Then
' Demande le changement d'écran
gChangeScreen = True
End If

Petite différence pour cet écran : il est statique.
Donc il n'est pas nécessaire de mettre à jour l'écran à chaque itération de la minuterie.
On laisse donc vide les fonctions ClScreen_UpdateScreen et ClScreen_DisplayScreen.
Le contenu de l'écran sera dessiné et affiché à l'initialisation dans Class_Initialize.

Ecriture des scores dans Class_Initialize de clScreenScores

Set oGdi = New clGdiplus ' Objet pour graphiques
Set oSound = New clSound ' Objet pour sons
Dim lX1 As Long, lY1 As Long ' Variables pour positionnement des textes
Dim lCpt As Long ' Compteur pour scores
' Ecran suivant par défaut
gNextScreen = "credits"
' Dessin du contenu statique
With oGdi
' Nouveau bitmap pour dessiner
.CreateBitmapForControl FormGame.img
' Rempli l'image de gris
.FillColor RGB(150, 150, 150)
' Ecrit le texte jeu gagné en haut
' Affiche les scores
.DrawText "Meilleurs scores", 40, , 2, 120 + 2, .ImageWidth + 2, 160 + 2, , , vbBlack, 150, , , , True
.DrawText "Meilleurs scores", 40, , 0, 120, .ImageWidth, 160, , , vbRed, , , , , True
.DrawLine 50, 165, .ImageWidth - 50, 165, vbBlack, 3, True, 150
lY1 = 175
lX1 = 50
For lCpt = 1 To 10
.DrawText ReadScore(CStr(lCpt), "name"), 30, , .ImageWidth / 2, lY1, , , 0, 2, vbBlue
.DrawText ReadScore(CStr(lCpt), "score"), 30, , .ImageWidth / 2 + 10, lY1, , , 2, 2, vbRed
lY1 = lY1 + 30
Next
' Dessine l'image sur le formulaire
oGdi.RepaintNoFormRepaint FormGame.img
End With

Testez l'affichage des scores en cliquant sur SCORE dans le menu.

XIX - Développement de l'écran des crédits

On crée l'écran comme on l'a fait pour tous les autres.

code de base de l'écran clScreenCredits

'***************************************************************************************
'*                             OBJET ECRAN DES CREDITS
'***************************************************************************************
Option Explicit

' Implémente l'interface de base des écrans
Implements ClScreen
' Objet pour commandes
Private oCommand As ClCommand
' Objet pour données partagées
Private oSharedData As ClSharedData
' Objet pour sons
Private oSound As ClSound
' Objet pour gdiplus
Private oGdi As ClGdiplus
' Indicateur pour changement d'écran
Private gChangeScreen As Boolean
' Nom de l'écran suivant
Private gNextScreen As String

'------------------------------------------------------------------------
' Initialisation de l'objet
' =>création des objets nécessaire à l'écran
'------------------------------------------------------------------------
Private Sub Class_Initialize()
Set oGdi = New ClGdiplus ' Objet pour graphiques
Set oSound = New ClSound ' Objet pour sons
' Ecran suivant par défaut
gNextScreen = "menu"
' Dessin du contenu statique
With oGdi
' Nouveau bitmap pour dessiner
.CreateBitmap .PointsToPixelsX(FormGame.Img.Width), .PointsToPixelsY(FormGame.Img.Height)
' Rempli l'image de gris
.FillColor RGB(150, 150, 150)
' Sauvegarde l'image en mémoire
.ImageKeep
End With
End Sub

'------------------------------------------------------------------------
' Implémentation des propriétés de l'interface
'------------------------------------------------------------------------
Private Property Get ClScreen_ChangeScreen() As Boolean
ClScreen_ChangeScreen = gChangeScreen
End Property

Private Property Set ClScreen_Command(RHS As ClCommand)
Set oCommand = RHS
End Property

Private Property Let ClScreen_NextScreen(RHS As String)
gNextScreen = RHS
End Property

Private Property Get ClScreen_NextScreen() As String
ClScreen_NextScreen = gNextScreen
End Property

Private Property Set ClScreen_SharedData(RHS As ClSharedData)
Set oSharedData = RHS
End Property

'------------------------------------------------------------------------
' Mise à jour des commandes
'------------------------------------------------------------------------
Private Function ClScreen_UpdateCommand() As Boolean
' Test si validation
If oCommand.TestKey(oSharedData.ToucheValid, 500) Or _
oCommand.TestJoypadButton(oSharedData.JoyButton, 500) Then
' Demande le changement d'écran
gChangeScreen = True
End If
End Function

'------------------------------------------------------------------------
' Mise à jour de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_UpdateScreen() As Boolean
With oGdi
' Reprend l'image de la mémoire
.ImageReset
End With
End Function

'------------------------------------------------------------------------
' Affichage de l'écran
'------------------------------------------------------------------------
Private Function ClScreen_DisplayScreen() As Boolean
' Dessine l'image sur le formulaire
oGdi.RepaintNoFormRepaint FormGame.Img
End Function

'------------------------------------------------------------------------
' Minuterie d'attente
'------------------------------------------------------------------------
Private Function ClScreen_ScreenWait() As Boolean
' Attente de 1000 / 40 = 25 ms pour affichage de 40 images/s
oGdi.Wait 25
End Function

L'écran suivant après les crédits est le menu.
On met à jour l'affichage 40 fois par secondes car on va afficher un texte déroulant.

Pour gérer un scrolling, on va d'abord dessiner le texte des crédits sur une image plus grande que l'image du jeu.
Ensuite on va dessiner cette image sur l'image principale à chaque itération de la minuterie.
En décalant l'image d'un pixel à chaque affichage, on obtiendra un défilement.
Pour que le texte s'enroule sans fin, on utilisera le paramètre gWrapY de l'objet oGdi.

Ecriture des crédits dans Class_Initialize

' Dessin du contenu statique
With oGdi
[...]
' Nouvelle image pour les crédits (2 fois plus haute que l'image principale
With .ImgNew("credits", .ImageWidth, .ImageHeight * 2)
.DrawText "Tutoriel jeu en VBA", 40, , 0, 40, .ImageWidth, 90, , , vbYellow
.DrawText "Ecrit pour developpez.com", 30, , 0, 120, .ImageWidth, 160, , , vbYellow
.DrawText "Dessins des sprites avec Microsoft ® Paint", 30, , 0, 170, .ImageWidth, 210, , , vbYellow
.DrawText "Sons enregistrés avec le magnétophone Windows", 30, , 0, 220, .ImageWidth, 260, , , vbYellow
.DrawText "Auteur : arkham46", 30, , 0, 270, .ImageWidth, 310, , , vbYellow
.DrawText "Site Web : arkham46.developpez.com", 30, , 0, 320, .ImageWidth, 360, , , vbBlue
End With
End With

Défilement des crédits dans ClScreen_UpdateScreen

' Variable pour décalage
Static gDecalage As Long
With oGdi
' Reprend l'image de la mémoire
.ImageReset
' Décalage de 1 pixels
gDecalage = gDecalage + 1
' Affecte le décalage
.WrapY = gDecalage
' On dessine l'image des crédits
.DrawImg "credits", 0, 0, .ImageWidth, .ImageHeight, , GdipSizeModeClip, GdipAlignBottomLeft
' On annule ensuite le décalage
.WrapY = 0
End With

L'image des crédits est dessinée avec un mode GdipSizeModeClip pour ne pas redimensionner l'image.
Elle est alignée en bas (GdipAlignBottomLeft) pour débuter un affichage qui commence en bas et qui remonte.

Puis dans la fonction GameChangeScreen du module clGame, il faut ajouter l'enchainement vers l'écran de crédits.

Ajoutez un Case au Select.

Enchainement vers écran de jeu gagné dans clGame

' Changement pour les crédits
Case "credits"
Set oScreen = Nothing
Set oScreen = New clScreenCredits

Testez l'affichage des crédits en cliquant sur CREDITS dans le menu.

XX - Lancement du jeu à l'ouverture d'Excel

Une toute dernière chose avant de nous quitter.
Il serait intéressant que le jeu se lance à l'ouverture du fichier Excel.
Pour cela on va afficher le formulaire au chargement du classeur.

Dans l'explorateur de projet à gauche, repérez le module ThisWorkbook.
Double-cliquez dessus et sélectionnez WorkBookdans les listes déroulantes en haut.
La procédure Workbook_Open est créée, on y insère le code d'affichage du formulaire.

Affichage du formulaire à l'ouverture du classeur

'---------------------------------------------------------------------------------------
' Procédure exécutée à l'ouverture du classeur Excel
'---------------------------------------------------------------------------------------
Private Sub Workbook_Open()
' Affiche le formulaire de jeu
FormGame.Show
End Sub

Fermez et rouvrez le classeur pour tester.

XXI - Améliorations possibles

Le jeu est complet mais n'est pas exempt de défauts.

Le plus gros défaut à mon goût est que les écrans ont été écrits en fonction de la taille de l'image qui contient le jeu.
Beaucoup de positions des textes sont écrites en durs.
Si on redimensionne l'image, tout est décalé.

D'autres améliorations pourraient être apportées sur l'ergonomie :
Par exemple on pourrait passer les différents écrans de fin de jeu, de scores et de crédits en cliquant à la souris.
L'écran des options n'est pas très ergonomique non plus.

On aurait également pu rendre les écrans plus attrayant graphiquement :
Par exemple le menu est trop statique, on aurait pu ajouter une poursuite entre le PacMan et les fantômes pour le dynamiser.

Mais le tutoriel est déjà bien long, on est allés à l'essentiel et toutes les fonctionnalités sont développées.

Voici un jeu de Pacman programmé avec clGdiplus pour Halloween :

XXII - Conclusion

J'espère que ce tutoriel vous aura aidé.

On a vu qu'il n'était pas si facile de développer un jeu, même un « simple » PacMran.

XXIII - Remerciements

Merci aux équipes Office et Jeux de developpez.com pour leurs relectures, commentaires et encouragements !

XXIV - Téléchargements

Télécharger le classeur Excel de ce tutoriel au format EXCEL 2000.

Téléchargez d'autres jeux créés avec Gdi+.