Tutoriel Gdi+ : développer un jeu de tir II

Thierry GASPERMENT

Comment développer un jeu en VBA avec Gdi+.
Deuxième partie : Boucle de jeu, joystick et sons.
Utilisation des API multimédia.

Commentez cet article : Commentez

- Titre : Tutoriel Gdi+ : développer un jeu de tir II

- Auteur : Thierry GASPERMENT

- Parution : 24 avril 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. La boucle de jeu

- II-A. Mise en œuvre de la boucle

- II-B. Exécution de la boucle

- II-C. Ajout d'une fonction d'attente

- III. Gestions des commandes clavier et joystick

- III-A. Détection du clavier

- III-B. Détection du joystick

- IV. Gestion du son

- V. Conclusion

- VI. Téléchargements

I - Introduction

Lors du premier tutoriel, nous avons commencé le développement d'un petit jeu de tir.

Nous allons, dans cette deuxième partie, apporter quelques améliorations :

- gérer une boucle de jeu (à la place de la minuterie) ;

- ajouter des sons ;

- gérer un joystick ;

Consultez la première partie avant de lire ce tutoriel

II - La boucle de jeu

Nous avons utilisé la minuterie du formulaire pour gérer le jeu du premier tutoriel.
Nous avons également relevé un problème de résolution de cette minuterie, qui est différente selon les configurations, et qui ne permet pas une précision inférieure à 10 ou 15 millisecondes.

Les jeux n'utilisent généralement pas une minuterie de ce type, mais sont gérés par une boucle.
Le terme anglais est Game Loop. Si vous recherchez ces mots sur internet, vous trouverez de nombreux articles et exemples (souvent en anglais).

Nous allons donc mettre en place une boucle, dont voici l’algorithme :

II-A - Mise en œuvre de la boucle

On va créer une boucle simple dans une nouvelle procédure MainLoop.

La boucle étant infinie, on utilise une variable booléenne gStop pour pouvoir commander la sortie de cette boucle.

Déclaration à ajouter en en-tête de formulaire

Private gStop As Boolean ' Variable pour sortie de boucle

Boucle principale

'---------------------------------------------------------------------------------------
' Boucle principale
'---------------------------------------------------------------------------------------
Private Sub MainLoop()
gStop = False
Do
DoEvents ' Traitement des messages
If gStop Then Exit Do ' Sortie de la boucle
UpdateGame ' Mise à jour du jeu
DisplayGame ' Affichage de l'image
Loop
End Sub

Arrêt de la boucle à la fermeture du formulaire

Private Sub Form_Close()
' Arrêt de la boucle
gStop = True
[...]

UpdateGame contient la gestion des objets et la génération de l'image de jeu.
On crée donc une nouvelle procédure dans laquelle on déplace le code qui se trouvait dans la procédure Form_Timer.

Procédure UpdateGame

'---------------------------------------------------------------------------------------
' Mise à jour du jeu
'---------------------------------------------------------------------------------------
Private Sub UpdateGame()
' Déplacez ici tout le contenu de la procédure Form_Timer
End Sub

DisplayGame affiche l'image du jeu à l'écran.
On crée donc une nouvelle procédure dans laquelle on place le code d'affichage de l'image.
Retirez cette même ligne de la procédure UpdateGame pour ne laisser qu'un seul affichage.

Procédure DisplayGame

'---------------------------------------------------------------------------------------
' Affichage du jeu
'---------------------------------------------------------------------------------------
Private Sub DisplayGame()
' Dessine l'image sur le formulaire
oGdi.RepaintFast Me.Img
End Sub

II-B - Exécution de la boucle

L'exécution de la procédure MainLoop fait entrer le programme dans une boucle infinie.
On ne peut donc pas l'exécuter dans l'événement Sur chargement du formulaire car il faut que cette procédure de chargement soit menée à son terme.

La solution la plus efficace que j'ai trouvé est d'exécuter la boucle dans la minuterie.

On définit un intervalle de minuterie de 50 millisecondes par exemple. La boucle de jeu démarrera 50 millisecondes après l'ouverture du formulaire.
N'oublions pas de réinitialiser la minuterie après exécution de la boucle afin de ne la lancer qu'une seule fois.

Procédure Sur Minuterie

'---------------------------------------------------------------------------------------
' Procédure exécutée sur minuterie
'---------------------------------------------------------------------------------------
Private Sub Form_Timer()
' Réinitialise la minuterie
Me.TimerInterval = 0
' Exécute la boucle de jeu
MainLoop
End Sub

Si on exécute le formulaire à ce stade du développement, le jeu se déroule aussi vite que le PC le permet.
Il manque une fonction d'attente pour afficher le jeu à une vitesse déterminée.

II-C - Ajout d'une fonction d'attente

On a vu dans le premier tutoriel que la minuterie avait une résolution de 10 ou 15 ms.
D'autres fonction telles que GetTickCount ont la même résolution.

Pour avoir une résolution de 1 ms, on utilise les Timers Multimedia.
Ces timers ne sont pas très simples à utiliser mais une fonction Wait est programmée dans clGdiplus.

Cette fonction Wait permet d'attendre le déclenchement de la minuterie, tout en traitant les messages reçus par l'application.

Attention, si vous exécutez le code Wait 10 dans une boucle, le programme n'attend pas 10 ms mais le déclenchement d'une minuterie toutes les 10 millisecondes.
On s'assure ainsi que le code qui suit l'instruction Wait est exécuté à intervalle régulier, quel que soit le temps d'exécution du reste du code (pour peu que le code s'exécute assez rapidement bien entendu).

La boucle principale devient :

Boucle principale

'---------------------------------------------------------------------------------------
' Boucle principale
'---------------------------------------------------------------------------------------
Private Sub MainLoop()
' Flag pour sortie de la boucle
gStop = False
' Début de la boucle
Do
oGdi.Wait 1000 / 40 ' Attente et traitement des messages
If gStop Then Exit Do ' Sortie de la boucle
UpdateGame ' Mise à jour du jeu
DisplayGame ' Affichage de l'image
Loop
' Libération de la classe d'attente
End Sub

On attend 1000/40 millisecondes entre chaque affichage, ce qui nous donne un frame rate de 40 images/seconde.

Vous pouvez exécuter le formulaire pour vous en assurer.

Faites ensuite varier ce temps d’attente ; on peut définir le frame rate avec précision.

Vous pouvez également vérifier le taux d'occupation du processeur (avec CTRL+ALT+SUPPR).
L'API utilisée MsgWaitForMultipleObjects a l'avantage de ne pas (ou peu) utiliser le CPU pendant l'attente.

III - Gestions des commandes clavier et joystick

Nous n'avons pas encore modifié le code de gestion du clavier du précédent tutoriel.
Cette gestion du clavier est un peu limitée et ne correspond pas à l'algorithme qu'on s'est fixé.
De plus le joystick ne peut être géré de cette manière car aucun événement n'est envoyé au formulaire.

On insère donc dans la boucle de jeu un appel à une nouvelle procédure GestionCommandes.

Les commandes sont alors testés toutes les 1000/40 = 25 ms, ce qui est bien suffisant.

Appel à la gestion des commandes dans MainLoop

' Début de la boucle
Do
oGdi.Wait 1000 / 40 ' Attente et traitement des messages
If gStop Then Exit Do ' Sortie de la boucle
GestionCommandes ' Gestion des commandes
UpdateGame ' Mise à jour du jeu
DisplayGame ' Affichage de l'image
Loop

On peut supprimer les procédures événementielles Form_KeyDown et Form_KeyUp.

Et ensuite on crée la nouvelle procédure GestionCommandes.

Procédure de gestion des commandes

'---------------------------------------------------------------------------------------
' Procédure de gestion des commandes
'---------------------------------------------------------------------------------------
Private Sub GestionCommandes()
'
End Sub

C'est dans cette procédure que nous allons mettre à jour les variables gKeyDown, gKeyUp, etc...
L'état de ces variables sera déterminé en fonction de l'appui sur les touches du clavier ou du joystick.
On utilise des API pour tester ces commandes.
Ci-dessous les déclarations de ces API.
Copiez-collez ce code dans un nouveau module nommé ModCommand.

Module de déclaration pour les commandes

'***************************************************************************************
'*                             MODULE POUR COMMANDES                                   *
'***************************************************************************************
Option Explicit

'---------------------------------------------------------------------------------------
' Déclarations pour Joystick
'---------------------------------------------------------------------------------------
#If VBA7 Then
Public Declare PtrSafe Function joyGetNumDevs Lib "winmm.dll" Alias "joyGetNumDev" () As Long
Public Declare PtrSafe Function joyGetPos Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFO) As Long
Public Declare PtrSafe Function joyGetDevCaps Lib "winmm.dll" Alias "joyGetDevCapsA" _
(ByVal id As LongPtr, lpCaps As JOYCAPS, ByVal uSize As Long) As Long
Public Declare PtrSafe Function joyGetPosEx Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFOEX) As Long
#Else
Public Declare Function joyGetNumDevs Lib "winmm.dll" () As Long
Public Declare Function joyGetPos Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFO) As Long
Public Declare Function joyGetDevCaps Lib "winmm.dll" Alias "joyGetDevCapsA" _
(ByVal id As Long, lpCaps As JOYCAPS, ByVal uSize As Long) As Long
Public Declare Function joyGetPosEx Lib "winmm.dll" (ByVal uJoyID As Long, pji As JOYINFOEX) As Long
#End If
Public 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
Public 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
Public Type JOYINFO
X As Long
Y As Long
Z As Long
Buttons As Long
End Type
Public Const JOY_BUTTON1 = &H1
Public Const JOY_BUTTON2 = &H2
Public Const JOY_BUTTON3 = &H4
Public Const JOY_BUTTON4 = &H8
Public Const JOYERR_BASE = 160
Public Const JOYERR_NOERROR = (0)
Public Const JOYERR_NOCANDO = (JOYERR_BASE + 6)
Public Const JOYERR_PARMS = (JOYERR_BASE + 5)
Public Const JOYERR_UNPLUGGED = (JOYERR_BASE + 7)
Public Const JOY_RETURNX As Long = &H1&
Public Const JOY_RETURNY As Long = &H2&
Public Const JOY_RETURNZ As Long = &H4&
Public Const JOY_RETURNR As Long = &H8&
Public Const JOY_RETURNU As Long = &H10
Public Const JOY_RETURNV As Long = &H20
Public Const JOY_RETURNPOV As Long = &H40&
Public Const JOY_RETURNBUTTONS As Long = &H80&
Public Const JOY_RETURNCENTERED As Long = &H400&
Public 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
Public Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#Else
Public Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Long
#End If
Public Const VK_LEFT = &H25
Public Const VK_UP = &H26
Public Const VK_RIGHT = &H27
Public Const VK_DOWN = &H28
Public Const VK_SPACE = &H20

III-A - Détection du clavier

La fonction GetAsyncKeyState nous donne l'état d'une touche du clavier.
Les constantes pour chaque touche sont données sur MSDN.

La fonction renvoie un résultat composé de deux entiers dans une variable de type long.
Un seul de ces deux entiers nous intéresse, d'où une opération logique (And &H8000) pour l'extraire.

Procédure de gestion des commandes

'---------------------------------------------------------------------------------------
' Procédure de gestion des commandes
'---------------------------------------------------------------------------------------
Private Sub GestionCommandes()
' Etat des touches
gKeyUp = (GetAsyncKeyState(VK_UP) And &H8000)
gKeyDown = (GetAsyncKeyState(VK_DOWN) And &H8000)
gKeyLeft = (GetAsyncKeyState(VK_LEFT) And &H8000)
gKeyRight = (GetAsyncKeyState(VK_RIGHT) And &H8000)
gKeySpace = (GetAsyncKeyState(VK_SPACE) And &H8000)
End Sub

III-B - Détection du joystick

L'état du joystick sera déterminé grâce aux API multimédia.

Consultez cet article de gRRosminet au sujet du contrôle du joystick sous windows

Pour ce tutoriel, on se limitera à la détection du joystick d'indice 0.
Les axes utilisés pour le déplacement du vaisseau sont les axes X et Y.
Le bouton utilisé pour tirer est le bouton n° 2 (32 boutons possibles).

Il est bien entendu plus intéressant de proposer dans le jeu un choix du joystick et des boutons utilisés.

Procédure de gestion des commandes

'---------------------------------------------------------------------------------------
' Procédure de gestion des commandes
'---------------------------------------------------------------------------------------
Private Sub GestionCommandes()
' Structure pour lecture de l'état du jostick
Dim linfo As JOYINFOEX
' Variable pour test si joystick présent
Dim lJoyPresent As Boolean
' Il est nécessaire d'initialiser cette variable
linfo.dwSize = Len(linfo)
' On lit l'état des axes X et Y, ainsi que des boutons
linfo.dwFlags = JOY_RETURNX Or JOY_RETURNY Or JOY_RETURNBUTTONS
' Lecture des infos et flag si joystick présent
lJoyPresent = (joyGetPosEx(0, linfo) = JOYERR_NOERROR)
' Etat des touches et du joystick
gKeyUp = (lJoyPresent And linfo.dwYpos = 0) Or _
(GetAsyncKeyState(VK_UP) And &H8000)
gKeyDown = (lJoyPresent And linfo.dwYpos = 65535) Or _
(GetAsyncKeyState(VK_DOWN) And &H8000)
gKeyLeft = (lJoyPresent And linfo.dwXpos = 0) Or _
(GetAsyncKeyState(VK_LEFT) And &H8000)
gKeyRight = (lJoyPresent And linfo.dwXpos = 65535) Or _
(GetAsyncKeyState(VK_RIGHT) And &H8000)
gKeySpace = (lJoyPresent And (linfo.dwButtons And 4) = 4) Or _
(GetAsyncKeyState(VK_SPACE) And &H8000)
End Sub

On limite la lecture à ce dont on a besoin avec les flags JOY_RETURNX, JOY_RETURNY et JOY_RETURNBUTTONS.
La fonction joyGetPosEx lit l'état du joystick d'indice 0 et renvoie JOYERR_NOERROR si le joystick est présent.

On n'utilise pas les sticks analogiques pour les axes, uniquement les touches fléchées du joystick.
Leur état varie de 0 à 65535, 32767 étant la position intermédiaire.

L'état de tous les boutons est inscrit dans la variable dwButtons.
Si le bouton 4 est appuyé, cette variable vaut : 2^4 = 16.
Si le bouton 6 est appuyé également, cette variable vaut : 2^4 + 2^6 = 80.

Pour extraire l'état de chaque bouton, il faut faire une opération logique.

Le code permet maintenant de déplacer le vaisseau soit avec les touches fléchées, soit avec la croix directionnelle d'un joystick.
Pour tirer, il faut appuyer soit sur espace, soit sur le bouton 2 du joystick.

IV - Gestion du son

Un jeu sans effet sonore est bien fade.
Pour ajouter du son à notre jeu, nous allons utiliser (encore une fois) une API multimédia : mciSendString.
Cette API nous permet notamment de jouer plusieurs sons simultanément.

Copiez-collez le code suivant dans un module nommé ModSons par exemple.

Module pour gestion du son

'***************************************************************************************
'*                             MODULE 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

Public Sub PlaySound(pPath As String)
Dim lPath As String
Dim lSize As Long
' Récupère le chemin court
lPath = pPath
lPath = Left(lPath, GetShortPathName(pPath, lPath, Len(pPath)))
' Stoppe le son si déjà en cours
mciSendString "Stop " & lPath, vbNullString, 0&, 0&
' Joue le son
mciSendString "Play " & lPath, vbNullString, 0&, 0&
End Sub

Public Sub StopSound(pPath As String)
Dim lPath As String
Dim lSize As Long
' Récupère le chemin court
lPath = pPath
lPath = Left(lPath, GetShortPathName(pPath, lPath, Len(pPath)))
' Stoppe le son
mciSendString "Stop " & lPath, vbNullString, 0&, 0&
End Sub

Ce module ajoute deux fonctions :

- PlaySound pour jouer un son (wav ou midi) ;

- StopSound pour stopper un son.

Remarque : ce module est très simpliste, pour plus d'information sur cette API, visitez le site MSDN
Pour ajouter par exemple un son lors de l'envoi de missile :

Ajout de son à la création de missiles dans la procédure UpdateGame

' Nouveaux missiles
If gKeySpace Then ' Si espace appuyé
If Timer - sLastMissile > 0.2 Then ' Si pas de missile depuis plus de 0.2 secondes
' Joue un son
PlaySound CurrentProject.Path & "\fx\LASERTW.WAV"
[...]

De nombreux sons peuvent être trouvés sur le net.
Beaucoup sont gratuits, certains pour une utilisation personnelle.
Pensez à vérifier la licence avant d'utiliser un son dans votre jeu.

Les sons de ce tutoriel ont été téléchargés sur : http://www.freesoundfiles.tintagel.net/Audio/

V - Conclusion

On a progressé dans ce tutoriel :

- la vitesse du jeu est maitrisée ;

- on peut déplacer le vaisseau au joystick ;

- on a ajouté du son, ce qui rend le jeu beaucoup plus "vivant".

Téléchargez les jeux créés avec Gdi+.

Merci à l'équipe Office de developpez.com pour ses relectures, commentaires et encouragements !

VI - Téléchargements

Télécharger la base Access de ce tutoriel au format ACCESS 2000

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