V. Les entrées utilisateurs▲
V-A. Introduction▲
Maintenant que vous savez afficher des vues 2D et 3D à couper le souffle (allons, pas de fausse modestie), il reste une chose importante: pouvoir faire interagir la scène avec le joueur.
Pour cela, il existe trois interfaces principales disponibles avec DirectX :
- le clavier ;
- la souris ;
- les manettes (joystick, joypad, etc.).
Comme d'habitude, nous allons voir leur fonctionnement tout au long de l'écriture d'un programme simple. Celui-ci devra afficher un carré blanc. L'utilisateur pourra déplacer ce carré à l'aide du clavier et d'une des manettes. Évidemment, il pourra sélectionner la manette directement pendant le « jeu ».
Afin d'aller plus, loin, on affichera quelques données sur les manettes afin d'identifier plus facilement la manette courante.
Maintenant que vous êtes familier avec l'affichage avec Asphyre, nous ne rentrerons pas dans les détails de l'affichage.
Le projet de départ que nous utiliserons ne fait pour l'instant qu'afficher un fond noir ainsi que le nombre de FPS.
V-B. Les entrées utilisateur dans Asphyre▲
Comme toujours, Asphyre propose des classes déjà toutes faites. Il nous incombe tout de même de les initialiser. Tout se passe dans les unités suivantes :
- AsphyreInputs.pas : déclare la variable AsphyreInput de type TAsphyreInput qui centralise les entrées utilisateurs ;
- AsphyreKeyboard.pas : unité relative au clavier ;
- AsphyreJoystick.pas : unité relative aux manettes ;
- AsphyreMouse.pas : unité relative à la souris ;
- DirectInput.pas : unité contenant toutes les constantes ainsi que les implémentations de DirectX.
Ajoutons donc ces unités à la clause uses de notre projet.
Pour commencer, ajoutons également ces variables privées nécessaires à la suite du programme :
- OriginRectPos : TPoint : coordonnées de l'origine du carré ;
- DispRectPos : TPoint : coordonnées finales du carré.
Voyons maintenant l'initialisation :
//Initialisation de AsphyreInput
AsphyreInput.Initialize;
AsphyreInput.WindowHandle := Handle;
//Initialisation des interfaces
AsphyreInput.Joysticks.Initialize;
AsphyreInput.Keyboard.Initialize;
N'oubliez surtout pas d'assigner la propriété WindowHandle !
La finalisation se fait aussi simplement :
AsphyreInput.Finalise;
à noter que la finalisation de AsphyreInput déclenche aussi la finalisation de Joysticks, Keyboard et Mouse. Cette simple ligne suffit donc.
L'affichage du carré se fera de cette façon dans le callback de rendu :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
RectSize = 40
;
var
p: TPoint4;
cl: TColor4;
begin
//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'
].TextOut('FPS : '
+ IntToStr(Timer.FrameRate),
10
, 10
, $99009900
);
with
Sender.Canvas do
begin
//Spécification des coordonnées du carré
p := Point4(DispRectPos.X, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y + RectSize,
DispRectPos.X, DispRectPos.Y + RectSize);
//Affichage du carré
FillQuad(p, clWhite4, fxuBlend);
end
;
end
;
Vous devriez avoir ceci :
V-C. Le clavier▲
Tachons maintenant de déplacer ce carré blanc lorsque l'on appuie sur les flèches du pavé directionnel.
L'idéal est de tout regrouper ces opérations dans OnProcessEvent.
La première chose à faire est de mettre à jour les informations en appelant Update :
if
AsphyreInput.Keyboard.Update then
Si l'Update s'est correctement déroulé, on peut passer à la suite.
La détection des touches sous DirectX ne se passe pas de la même façon que la VCL. En effet, dans la VCL, vous avez un événement OnKeyPress (ou Down et Up) qui passe la touche appuyée en paramètre. Sous DirectX, c'est à vous de tester si chaque touche qui vous intéresse est pressée ou non.
Pour cela, la propriété Key de AsphyreInput.Keyboard nous permet de savoir facilement si la touche spécifiée est pressée ou non de cette façon :
procedure
TMainFrm.ProcessEvent(Sender: TObject);
const
DecValue = 40
;
var
Decalage: TPoint;
begin
//Mise à zéro du décalage
Decalage := Point(0
, 0
);
with
AsphyreInput.Keyboard do
begin
//Appui sur droite ou gauche
if
Key[DIK_RIGHT] then
Decalage.X := DecValue else
if
Key[DIK_LEFT] then
Decalage.X := -DecValue;
//Appui sur bas ou haut
if
Key[DIK_DOWN] then
Decalage.Y := DecValue else
if
Key[DIK_UP] then
Decalage.Y := -DecValue;
//Appui sur Echap pour fermer l'application
if
Key[DIK_ESCAPE] then
Close;
end
;
//Assignation de la position du carré
DispRectPos.X := OriginRectPos.X + Decalage.X;
DispRectPos.Y := OriginRectPos.Y + Decalage.Y;
end
;
Les constantes DIK_[Touche] se trouvent dans l'unité DirectInput.pas. Ce code est simple : suivant la direction pressée, le décalage est assigné. Vous aurez aussi remarqué que nous vérifions l'état de la touche Echap afin de fermer l'application. Son interception dans OnKeyPress est donc inutile ici!
Les deux dernières lignes calculent la position finale du carré.
Et c'est tout !
Exécutez votre programme et essayez d'appuyer sur les touches du pavé directionnel, le carré se déplacera et reviendra à sa place lorsque vous lâchez la touche.
Il existe également des propriétés KeyPressed et KeyReleased, mais comme stipulé en commentaire dans l'unité AsphyreKeyboard.pas, elles sont tributaires des appels à Update. Donc si vous utilisez ces méthodes, votre programme peut perdre des événements du clavier.
Il vaut donc mieux utiliser la propriété Key.
V-D. Les joysticks▲
Afin d'afficher les données de chaque joystick (pour peu que vous en ayez plusieurs), nous avons besoin d'un record ainsi que d'un tableau dynamique contenant ces records.
Voici la structure :
TJoyInfos = record
Axis: integer
;
Buttons: integer
;
POVs: integer
;
end
;
Cette structure contiendra les capacités de chacun des joystick branché.
Nous aurons également des variables privées suivantes :
- CurrentJoystick : integer : indice du joystick courant ;
- JoyPos : TPoint : amplitude du déplacement du joystick pour l'affichage ;
- Joys : array or TJoyInfos : contient les données de chaque joystick ;
- JoyDown : boolean : si le bouton est appuyé ou non.
Ajoutons donc une fonction qui va récupérer les informations des joysticks :
function
TMainFrm.GetJoyInfos(const
JoyIndex: integer
): TJoyInfos;
begin
with
AsphyreInput.Joysticks[JoyIndex].DeviceCaps do
begin
//Nombre d'axes
Result.Axis := dwAxes;
//Nombre de boutons
Result.Buttons := dwButtons;
//Nombre de POVs
Result.POVs := dwPOVs;
end
;
end
;
Les capacités du joystick se trouvent dans la propriété DeviceCaps.
Vous pouvez également afficher le nom du joystick en utilisant la méthode GetDeviceInfo de AsphyreJoystick.InputDevice, mais je n'ai pas réussi à la faire fonctionner: soit elle ne marche pas soit je m'y suis pris comme un pied.
Bref, si vous avez la solution à ce problème, je suis preneur.
Étoffons un peu l'initialisation afin de récupérer toutes ces données :
//Initialisation de AsphyreInput
AsphyreInput.Initialize;
AsphyreInput.WindowHandle := Handle;
//Initialisation des interfaces
AsphyreInput.Joysticks.Initialize;
AsphyreInput.Keyboard.Initialize;
//Initialise le joystick courant
CurrentJoystick := 0
;
//Spécifie la longueur du tableau
SetLength(Joys, AsphyreInput.Joysticks.Count);
//Initialise chaque information de joystick
for
I := 0
to
AsphyreInput.Joysticks.Count - 1
do
Joys[I] := GetJoyInfos(i);
//Initialisation de l'état appuyé à faux
JoyDown := false
;
Ajoutons également la libération du tableau dans OnDestroy :
procedure
TMainFrm.FormDestroy(Sender: TObject);
begin
//Vidage du tableau
SetLength(Joys, 0
);
//Finalisation de AsphyreInput
AsphyreInput.Finalize;
//Finalisation de DirectX
Devices.Finalize;
end
;
Afin de faciliter l'écriture du code, nous allons également séparer l'affichage des informations des joysticks :
procedure
TMainFrm.DrawJoyInfos(Sender: TAsphyreDevice);
var
i: integer
;
Color: Cardinal
;
begin
//Y a-t-il des joysticks?
if
AsphyreInput.Joysticks.Count > 0
then
begin
//Affichage du nombre de joystick(s) et des amplitudes de déplacement
Sender.SysFonts.Font['s/arial'
].TextOut(
Format('Joysticks : %u (X=%d, Y=%d)'
,
[AsphyreInput.Joysticks.Count, JoyPos.X, JoyPos.Y]),
10
, 25
, $FF777777
);
//Affichage des données de chaque joystick
with
Sender.Canvas do
for
i := 0
to
AsphyreInput.Joysticks.Count - 1
do
begin
//Si le joystick est le joystick courant
if
i = CurrentJoystick then
begin
//Aficher un cercle rouge en face du texte
FillCircle(10
, 51
+ i * 20
, 5
, 10
, clRed4, fxuBlend);
//Couleur du texte en blanc
Color := $FFFFFFFF
;
end
else
//Couleur du texte en gris
Color := $FF777777
;
//Affichage du texte
Sender.SysFonts.Font['s/arial'
].TextOut(
Format('%d : %d axe(s), %d bouton(s), %d POV(s)'
,
[i + 1
, Joys[i].Axis, Joys[i].Buttons, Joys[i].POVs]),
20
, 45
+ i * 20
, Color);
end
;
end
else
//S'il n'y a pas de joystick
Sender.SysFonts.Font['s/arial'
].TextOut('Aucun joystick détecté.'
,
10
, 25
, $FF777777
);
end
;
Ajoutons cette procédure dans le callback de rendu :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
RectSize = 40
;
var
p: TPoint4;
cl: TColor4;
begin
//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'
].TextOut('FPS : '
+ IntToStr(Timer.FrameRate),
10
, 10
, $99009900
);
//Affichage des données des joysticks
DrawJoyInfos(Sender);
with
Sender.Canvas do
begin
//Spécification des coordonnées du carré
p := Point4(DispRectPos.X, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y + RectSize,
DispRectPos.X, DispRectPos.Y + RectSize);
//Spécification de la couleur suivant que l'état appuyé soit vrai ou faux
if
JoyDown then
cl := clRed4
else
cl := clWhite4;
//Affichage du carré
FillQuad(p, cl, fxuBlend);
end
;
end
;
Vous devriez avoir quelque chose comme ceci :
Écrivons maintenant le code pour utiliser les données des joysticks.
Contrairement aux souris et clavier, il est courant d'avoir plusieurs joysticks/pads branchés en même temps sur l'ordinateur. De plus, le joystick que l'utilisateur voudrait utiliser n'est pas forcément le premier branché !
Vous devez donc prendre compte de tous ces paramètres lorsque que vous codez un jeu.
Heureusement, Asphyre gère ce problème de façon très simple. En effet, chaque joystick a un indice différent (de 0 à n-1); il est donc très facile d'utiliser le bon joystick.
Regardons le code pour notre exemple :
const
JoyDeadZone = 3000
;
...
//Vérification de la présence d'un joystick
if
AsphyreInput.Joysticks.Count > 0
then
begin
//Mise à jour des données des joysticks
if
AsphyreInput.Joysticks.Update then
with
AsphyreInput.Joysticks[CurrentJoystick].JoyState do
begin
//Si l'amplitude est supérieure à la deadzone alors effectuer le déplacement
//en X
if
Abs(lX) > JoyDeadZone then
Decalage.X := lX div
200
;
//en Y
if
Abs(lY) > JoyDeadZone then
Decalage.Y := lY div
200
;
//Pour l'affichage des données
JoyPos := Point(lX, lY);
end
;
end
;
Vous aurez remarqué cette constante JoyDeadZone. C'est une constante assez importante puisque c'est elle qui définit la zone morte (dead zone) de votre joystick. En effet, si le joystick est sensible, à peine vous aurez bougé le manche que l'affichage réagira. C'est une bonne chose me direz-vous. Je vous répondrais oui, sauf qu'un joystick est rarement centré pile au centre lorsqu'il est au repos. Vous aurez donc des mouvements parasites que vous pourrez supprimer avec cette zone morte. Elle définit l'amplitude minimale à prendre en compte.
Par exemple, pour éviter les mouvements parasites avec mon pad, j'ai dû paramétrer la deadzone à 3000…
Nous testerons donc pour chaque direction si l'amplitude est supérieure à la deadzone. Nous diviserons l'amplitude par 200 afin d'éviter que le carré sorte complètement de la zone d'affichage (l'amplitude d'un joystick allant de -32768 à 32767).
La dernière ligne ne sert qu'à assigner JoyPos à l'amplitude actuelle afin de l'afficher.
Vous pouvez d'ores et déjà exécuter votre code. Vous verrez que si vous agissez sur votre premier joystick (dans l'ordre de la liste des Contrôleurs de jeu du Panneau de configuration de Windows) le carré bougera dans le sens que vous lui donnez.
La dernière chose à faire est de permettre de spécifier le joystick utilisé. Pour cela, l'utilisateur devra taper sur le pavé numérique du clavier le numéro du joystick.
Nous devons donc ajouter les tests des touches 1 à 9 (oui voyons grand !) afin de donner à CurrentJoystick l'indice du joystick :
if
AsphyreInput.Keyboard.Update then
with
AsphyreInput.Keyboard do
begin
//Appui de 1 à 9 sur le pavé numérique pour spécifier le joystick utilisé
if
Key[DIK_NUMPAD1] then
CurrentJoystick :=
Min(0
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD2] then
CurrentJoystick :=
Min(1
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD3] then
CurrentJoystick :=
Min(2
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD4] then
CurrentJoystick :=
Min(3
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD5] then
CurrentJoystick :=
Min(4
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD6] then
CurrentJoystick :=
Min(5
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD7] then
CurrentJoystick :=
Min(6
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD8] then
CurrentJoystick :=
Min(7
, AsphyreInput.Joysticks.Count - 1
) else
if
Key[DIK_NUMPAD9] then
CurrentJoystick :=
Min(8
, AsphyreInput.Joysticks.Count - 1
);
end
;
C'est long, mais c'est indispensable. Notez que nous testons à chaque fois si l'indice ne dépasse pas le nombre de joysticks ! Mais pourquoi le faire à chaque fois et pas une bonne fois pour toutes à la fin ? La raison en est très simple : on utilise CurrentJoystick dans le callback de rendu afin de mettre en valeur le joystick utilisé. Or, si on ne paramètre cette variable qu'à la fin, pendant un court instant, elle aura une valeur potentiellement hors de l'étendue (supérieure au nombre de joysticks). Eh bien ce court instant a suffi à faire planter systématiquement lorsque j'ai voulu faire comme ceci : le callback de rendu faisait appel à cette variable au moment où elle n'était pas encore vérifiée.
C'est pour cette raison que j'ai volontairement utilisé cette méthode qui n'assignera jamais CurrentJoystick à autre chose qu'un indice valable.
Ainsi, l'utilisateur pourra choisir le joystick utilisé :
Il ne reste qu'une chose : Détecter l'appui du bouton.
Pour cela, une ligne suffit :
if
AsphyreInput.Joysticks.Update then
with
AsphyreInput.Joysticks[CurrentJoystick].JoyState do
begin
//Etat baissé si le bouton 0 est pressé
JoyDown := rgbButtons[0
] > 0
;
end
;
La propriété rgbButtons recense tous les boutons du contrôleur. Ils sont identifiés par leur indice.
Vous pouvez trouver très facilement l'indice de chacun des boutons de votre contrôleur. Pour cela, rendez-vous dans la boite de dialogue Contrôleur de jeu du Panneau de configuration de Windows. Sélectionnez le contrôleur et cliquez sur Propriétés. Vous devrez avoir la liste de tous les boutons numérotés. Cliquez sur le bouton que vous voulez utiliser, repérez son numéro et retranchez 1 à ce numéro : vous avez l'indice !
Vous remarquerez aussi une particularité : la propriété rgbButtons[Index] n'est pas de type booléen. Il est donc tout à fait possible d'utiliser des boutons avec une amplitude. Dans notre cas, le bouton est du type on/off, nous testerons donc simplement si sa valeur est 0 (relâché) ou supérieur à 0 (appuyé).
Vous pouvez exécuter votre programme, lorsque vous appuyez sur le bouton, le carré deviendra rouge :
V-E. La souris▲
Passons maintenant à la dernière interface disponible: la souris. Son utilisation sous Asphyre ressemble beaucoup à celle du joystick. À ceci près, que l'on n'a pas une valeur d'amplitude pour les déplacements, mais une différence (delta).
Premièrement, il faut initialiser le gestionnaire de souris. Nous avons maintenant l'habitude, cela se fait comme ceci :
//Initialisation de AsphyreInput
AsphyreInput.Initialize;
AsphyreInput.WindowHandle := Handle;
//Initialisation des interfaces
AsphyreInput.Joysticks.Initialize;
AsphyreInput.Keyboard.Initialize;
AsphyreInput.Mouse.Initialize;
Comme son nom l'indique, la dernière ligne initialise le gestionnaire de souris. Nous pouvons dès lors utiliser la souris avec Asphyre.
Lorsque l'utilisateur appuiera sur le bouton gauche de la souris, cela aura le même effet que le bouton du joystick : le carré devient rouge et redevient blanc lorsque le bouton est lâché. Ajoutons une variable privée MouseDown : boolean afin de ne pas interférer avec la variable JoyDown pour le joystick. Cette variable MouseDown sera initialisée à false dans OnCreate
Modifions donc notre procédure ProcessEvent afin d'utiliser la souris :
//Mise à jour des données de la souris
if
AsphyreInput.Mouse.Update then
with
AsphyreInput.Mouse do
begin
//Assignation des valeurs des mouvements
Decalage.X := DeltaX * 2
;
Decalage.Y := DeltaY * 2
;
//Si le bouton gauche est enfoncé
if
Pressed[0
] then
MouseDown := true
;
//S'il est relâché
if
Released[0
] then
MouseDown := false
;
end
;
La question qui se pose ici est de savoir où placer cette portion de code par rapport aux autres gestionnaires que nous avons écrits précédemment (clavier et joysticks). Tel que nous l'avons écrit, le dernier des périphériques donnera sa valeur à Decalage.X et Decalage.Y. C'est donc à vous de savoir quel périphérique sera prioritaire par rapport aux autres. En général, les contrôleurs d'un jeu ne sont pas aussi redondants et on doit choisir entre le couple clavier/souris ou le joystick par exemple pour les jeux tirés des consoles. Ce problème se pose donc uniquement dans notre exemple. J'ai personnellement placé cette gestion de la souris en premier.
Rien ne vous empêche également de cumuler tous ces contrôleurs en ajoutant à chaque fois la valeur de Decalage.X et Decalage.Y. Bref, vous l'aurez compris, il est très facile de gérer les contrôleurs de façon adaptée à vos besoins.
Regardons le code plus en détail. On commence comme d'habitude par faire un Update. Cette opération est indispensable sinon, les buffers ne sont pas mis à jour et aucun mouvement n'est détecté !
Les lignes suivantes utilisent DeltaX et DeltaY qui sont comme je vous le disais les deux différences (et non amplitudes) de déplacement de la souris. Dans notre exemple volontairement simple, nous utiliserons directement la valeur des deltas pour notre position.
La multiplication par 2 ne sert qu'à avoir une amplitude de mouvement plus grand.
Passons aux lignes du dessous. Elles gèrent l'état du bouton 0 (le bouton gauche) appuyé ou relâché.
Comme vous le constatez, le système est différent du joystick. Pour celui-ci, nous devions tester si la valeur d'un bouton est supérieure à zéro. Si oui, le bouton est pressé et dès que la valeur repasse à 0, le bouton vient d'être relâché.
Ici, il faut tester lorsque le bouton est appuyé (Pressed) et lorsqu'il est relâché (Released). C'est pour cette raison que je n'ai pas écrit :
MouseDown := Pressed[0
];
Si vous essayez ce code, vous verrez le cube rouge qu'une fraction de seconde : Simplement au moment où le bouton est pressé et non pendant que le bouton est pressé ! La nuance est subtile.
À noter qu'il existe également la propriété DeltaWheel de Mouse qui, comme son nom l'indique permet d'utiliser la molette.
Pour finir, ajoutons la prise en compte de la nouvelle variable MouseDown dans le callback de rendu :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
RectSize = 40
;
var
p: TPoint4;
cl: TColor4;
begin
//Affichage du nombre de FPS
Sender.SysFonts.Font['s/arial'
].TextOut('FPS : '
+ IntToStr(Timer.FrameRate),
10
, 10
, $99009900
);
//Affichage des données des joysticks
DrawJoyInfos(Sender);
with
Sender.Canvas do
begin
//Spécification des coordonnées du carré
p := Point4(DispRectPos.X, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y,
DispRectPos.X + RectSize, DispRectPos.Y + RectSize,
DispRectPos.X, DispRectPos.Y + RectSize);
//Spécification de la couleur suivant que l'état appuyé soit vrai ou faux
if
JoyDown or
MouseDown then
cl := clRed4
else
cl := clWhite4;
//Affichage du carré
FillQuad(p, cl, fxuBlend);
end
;
end
;
Vous pouvez exécuter votre programme et voir votre carré blanc trembler suivant la vitesse de déplacement de votre souris.
V-F. Téléchargement du programme d'exemple▲
Vous pouvez télécharger le programme d'exemple ici: Exemple2D.zip
V-G. Conclusion▲
Voilà, dorénavant, vous êtes prêt à vous lancer dans la création d'un jeu avec votre EDI préféré ! N'hésitez pas à me faire part de vos créations, je me ferai une joie de les ajouter en téléchargement à la suite de ce tutoriel !
Bon dev à tous !