Date de publication : 24/04/08 , Date de mise à jour : 24/04/08
Par
Thierry GASPERMENT (arkham46.developpez.com/)
Comment développer un jeu en VBA avec Gdi+.
Deuxième partie : Boucle de jeu, joystick et sons.
Utilisation des API multimédia.
I. Introduction
II. La boucle de jeu
II-A. Mise en oeuvre de la boucle
II-B. Exécution de la boucle
II-C. Ajout d'une fonction d'attente
II-D. Limitations techniques
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
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 google, vous trouverez de nombreux articles et exemples (souvent en anglais).
Nous allons donc mettre en place une boucle.
Voici l'algorithme de cette boucle :
II-A. Mise en oeuvre 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 |
|
| Boucle principale |
Private Sub MainLoop()
gStop = False
Do
DoEvents
If gStop Then Exit Do
UpdateGame
DisplayGame
Loop
End Sub
|
| Arrêt de la boucle à la fermeture du formulaire |
Private Sub Form_Close()
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 |
Private Sub UpdateGame()
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 |
Private Sub DisplayGame()
clGdip.FastRepaint 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éfini 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 par 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 |
Private Sub Form_Timer()
Me.TimerInterval = 0
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.
Ci-dessous, le code d'un module de classe pour gérer l'attente.
| Classe pour attente avec timer multimedia |
Option Explicit
Private Const TIME_PERIODIC = 1
Private Const TIME_CALLBACK_FUNCTION = &H0
Private Const TIME_CALLBACK_EVENT_SET = &H10
Private Declare Function timeSetEvent Lib "winmm.dll" _
(ByVal uDelay As Long, ByVal uResolution As Long, ByVal lpFunction As Long, _
ByVal dwUser As Long, ByVal uFlags As Long) As Long
Private Declare Function timeKillEvent Lib "winmm.dll" _
(ByVal uID As Long) As Long
Private Declare Function CreateEvent Lib "Kernel32" Alias "CreateEventA" _
(lpEventAttributes As SECURITY_ATTRIBUTES, ByVal bManualReset As Long, _
ByVal bInitialState As Long, ByVal lpName As String) As Long
Private Declare Function WaitForMultipleObjects Lib "Kernel32" _
(ByVal nCount As Long, lpHandles As Long, ByVal bWaitAll As Long, _
ByVal dwMilliseconds As Long) As Long
Private Declare Function ResetEvent Lib "Kernel32" (ByVal hEvent As Long) As Long
Private Declare Function CloseHandle Lib "Kernel32" ( _
ByVal hObject As Long) As Long
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Long
bInheritHandle As Long
End Type
Private Const WAIT_ABANDONED& = &H80&
Private Const WAIT_ABANDONED_0& = &H80&
Private Const WAIT_FAILED& = -1&
Private Const WAIT_IO_COMPLETION& = &HC0&
Private Const WAIT_OBJECT_0& = 0
Private Const WAIT_OBJECT_1& = 1
Private Const WAIT_TIMEOUT& = &H102&
Private Const INFINITE = &HFFFF
Private Const QS_HOTKEY& = &H80
Private Const QS_KEY& = &H1
Private Const QS_MOUSEBUTTON& = &H4
Private Const QS_MOUSEMOVE& = &H2
Private Const QS_PAINT& = &H20
Private Const QS_POSTMESSAGE& = &H8
Private Const QS_SENDMESSAGE& = &H40
Private Const QS_TIMER& = &H10
Private Const QS_MOUSE& = (QS_MOUSEMOVE _
Or QS_MOUSEBUTTON)
Private Const QS_INPUT& = (QS_MOUSE _
Or QS_KEY)
Private Const QS_ALLEVENTS& = (QS_INPUT _
Or QS_POSTMESSAGE _
Or QS_TIMER _
Or QS_PAINT _
Or QS_HOTKEY)
Private Const QS_ALLINPUT& = (QS_SENDMESSAGE _
Or QS_PAINT _
Or QS_TIMER _
Or QS_POSTMESSAGE _
Or QS_MOUSEBUTTON _
Or QS_MOUSEMOVE _
Or QS_HOTKEY _
Or QS_KEY)
Private Declare Function MsgWaitForMultipleObjects Lib "User32" ( _
ByVal nCount As Long, _
pHandles As Long, _
ByVal fWaitAll As Long, _
ByVal dwMilliseconds As Long, _
ByVal dwWakeMask As Long) As Long
Private gId As Long
Private ghEvent As Long
Private gInterval As Long
Public Sub Wait(ByVal pIntervalMs As Long)
Dim lRet As Long
If pIntervalMs <= 1 Then pIntervalMs = 1
If pIntervalMs <> gInterval Then
gInterval = pIntervalMs
If gId <> 0 Then timeKillEvent gId
gId = timeSetEvent(pIntervalMs, 0, ghEvent, 0, TIME_PERIODIC Or TIME_CALLBACK_EVENT_SET)
End If
Do
lRet = MsgWaitForMultipleObjects(1, ghEvent, False, _
INFINITE, QS_ALLINPUT&)
DoEvents
Loop Until lRet = WAIT_OBJECT_0
End Sub
Private Sub Class_Initialize()
Dim ls As SECURITY_ATTRIBUTES
ls.nLength = Len(ls)
ls.lpSecurityDescriptor = 0
ls.bInheritHandle = 0
ghEvent = CreateEvent(ls, False, False, CStr(ObjPtr(Me)))
End Sub
Private Sub Class_Terminate()
If gId <> 0 Then timeKillEvent gId
If ghEvent <> 0 Then ResetEvent ghEvent
If ghEvent <> 0 Then CloseHandle ghEvent
End Sub
|
Copier-coller ce code dans un nouveau module de classe que vous nommez clWait.
La 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écuter le code Wait 10 dans une boucle, le programme n'attend pas 10 ms mais le déclenchement d'un timer toutes les 10 millisecondes.
On s'assure ainsi que le code qui suit l'instruction Wait est exécuté à intervalle régulier, quelque 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 |
Private Sub MainLoop()
Dim lWait As clWait
Set lWait = New clWait
gStop = False
Do
lWait.Wait 1000 / 40
If gStop Then Exit Do
UpdateGame
DisplayGame
Loop
Set lWait = Nothing
End Sub
|
On attend 1000/40 millisecondes entre chaque affichage, ce qui nous donne un framerate 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 framerate avec précision.
Vous pouvez également vérifier le taux d'occupation du processeur (avec CTRL+ALT+SUPPR).
L'API MsgWaitForMultipleObjects a l'avantage de ne pas (ou peu) utiliser le CPU pendant l'attente.
II-D. Limitations techniques
On peut théoriquement faire varier le temps d'attente jusqu'à 1 ms
On aurait alors 1000 images/secondes.
C'est évidemment beaucoup trop et inutile.
Par contre on serait facilement tenté de mettre par exemple un intervalle de 5 ms afin d'obtenir 100 images/seconde.
J'émets trois avertissements à ce sujet :
- 1 : Il faut alors que tout le code qui s'exécute dans la boucle principale le fasse en moins de 5 millisecondes, sinon l'affichage sera ralenti
- 2 : Il est inutile de demander un affichage si fréquent si l'écran ne le permet pas (pour afficher 100 images/s, il faut une fréquence de rafraîchissement de l'écran de 100Hz).
- 3 : Si vous calibrer votre jeu à 100 images/s sur un PC performant, quelqu'un qui a un PC moins puissant risque de trouver le jeu lent et injouable
Le timer multimedia est surtout utile pour régler la vitesse du jeu de manière précise.
Il vaut peut-être mieux parfois se limiter à 40 images/s et déplacer les objets par pas de 3 ou 4 pixels.
III. Gestions des commandes clavier et joystick
Nous n'avons pas 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 |
Do
lWait.Wait 1000 / 40
If gStop Then Exit Do
GestionCommandes
UpdateGame
DisplayGame
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 |
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ée 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 |
Option Explicit
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
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)
Public Declare Function GetAsyncKeyState Lib "User32" (ByVal vKey As Long) As Integer
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 |
Private Sub GestionCommandes()
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
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 |
Private Sub GestionCommandes()
Dim linfo As JOYINFOEX
Dim lJoyPresent As Boolean
linfo.dwSize = Len(linfo)
linfo.dwFlags = JOY_RETURNX Or JOY_RETURNY Or JOY_RETURNBUTTONS
lJoyPresent = (joyGetPosEx(0, linfo) = JOYERR_NOERROR)
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 multimedia : 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 |
Option Explicit
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
Public Sub PlaySound(pPath As String)
Dim lPath As String
Dim lSize As Long
lPath = pPath
lPath = Left(lPath, GetShortPathName(pPath, lPath, Len(pPath)))
mciSendString "Stop " & lPath, vbNullString, 0&, 0&
mciSendString "Play " & lPath, vbNullString, 0&, 0&
End Sub
Public Sub StopSound(pPath As String)
Dim lPath As String
Dim lSize As Long
lPath = pPath
lPath = Left(lPath, GetShortPathName(pPath, lPath, Len(pPath)))
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 |
If gKeySpace Then
If Timer - sLastMissile > 0.2 Then
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 (au moins sur les ordinateurs assez rapide)
- on peut déplacer le vaisseau au joystick.
- on a ajouté du son, ce qui rend le jeu beaucoup plus "vivant"
Merci à l'équipe Office de developpez.com pour ses relectures, commentaires et encouragements!
VI. Téléchargements


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 oeuvre intellectuelle protégée par les droits d'auteurs. Copyright ©
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'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts. Droits de diffusion permanents accordés à developpez LLC.
Cette page est déposée à la
SACD.