08 janv. 2008

Exploration de XNA : la gestion du clavier

Et voici un nouveau billet tout chaud - l'avant dernier de la mini-série consacrée à la gestion des périphériques d'entrée. Nous allons y apprendre comment gérer le clavier via l'utilisation du framework XNA dans un premier temps puis nous chercherons à faire un peu mieux (sous Windows uniquement). Attention, c'est un peu long...

Logo XNA [Télécharger Visual C# 2005 Express Edition] [Télécharger XNA Game Studio 2.0] [Télécharger le Service Pack 1 pour VC# 2005 EE] [TorqueX, de GarageGames.com] [XNA Resources] [Forum MSDN XNA Game Studio] [GameDev.Net - Plenty of 1s and 0s]

En attendant

Avant de parler de notre sujet principal (le clavier), je voudrais revenir sur une question de RzL trouvée sur le forum geekzone.fr:

Merci pour ces liens, je crois qu'ils m'aideront bien, le XNA et moi on est pas encore très copain. J'en profite pour vous demandez quelque chose: je veux animé un sprite pour un petit jeu 2D à partir d'une planche et le seul tuto que j'ai trouvé c'est celui-ci, le problème étant que dans ce tuto l'auteur se sert d'une petit lib et dans la méthode draw pour les spites animés il n'y a pas le paramètre pour redimensionner le sprite, j'essaie de refaire la fonction à côté mais je galère un peu, si vous avez du temps à perdre pour m'aider ça serait super sympa.

Bon, je dois l'admettre, je ne suis pas très clair à ce niveau. En fait, je n'explique pas la librairie Pawn - parce que ce n'est pas le focus de ces articles. Elle reprends dans les grandes lignes le code des articles, mais en l'organisant d'une autre manière.

Pour répondre à cette question précise : la plupart des effets spéciaux liés au rendu des sprites peuvent être obtenus en modifiant les propriétés d'une instance de la classe Pawn.SpriteModifier (dans ce cas, la propriété Pawn.SpriteModifier.Scale). La ligne 36 du fichier SpriteAnimatedGame.cs (projet Ex_SpriteAnimated dans la dernière archive) donne un exemple d'utilisation de cette propriété. J'ai l'intention d'aborder les effets spéciaux liés aux sprites très rapidement, dès début décembre si tout va bien. A ce moment, je donnerais des informations sur comment ces effets spéciaux sont implémentés dans la librairie Pawn.

Et maintenant, concentrons nous sur notre sujet principal : la gestion du clavier.

Mode d'acquisition[1]

Bien évidemment, vos illusions vont de nouveau s'envoler : la lecture de l'état du clavier se fait en mode immédiat - lorsque vous appelez la méthode adéquate, vous récupérez l'état courant du clavier sans notion d'historique. Cela pose toutefois un problème supplémentaire pour la clavier, car outre le manque d'historique, vous ne récupérez que l'état des touches physiques. Si vous appuyez simultanément sur la touche SHIFT et la touche A pendant que la touche MAJ est active, vous ne récupérez pas un 'a' minuscule - mais bien un appui sur SHIFT et A. Si vous souhaitez récupérer un nom d'utilisateur ou une phrase tapée par le joueur, à vous de faire la translation des touches.

Et oui, c'est principalement la que le bât blesse : sans translation des touches, comment interpréter les entrées utilisateur correctement ? Avant de revenir sur ce passionnant sujet, étudions un peu l'interface que nous propose XNA pour récupérer cet état clavier.

Cette interface est composée d'une classe (Microsoft.Xna.Framework.Input.Keyboard[2]), d'une structure (Input.KeyboardState) et de deux énumérations (Keys et KeyState). La classe Keyboard définit une méthode statique Input.Keyboard.GetState() qui renvoie une structure du type KeyboardState. Cette structure définit les méthodes suivantes :

  • Keys GetPressedKeys () renvoie un tableau qui liste les touches qui sont dans l'état KeyState.Down.
  • bool IsKeyDown(Keys key) renvoie true si la touche key est dans l'état KeyState.Down, false dans le cas contraire.
  • bool IsKeyDown(Keys key) renvoie true si la touche key est dans l'état KeyState.Up, false dans le cas contraire.
  • KeyState (Keys key) renvoie l'état de la touche key.

La liste des valeurs possibles de l'énumération Keys est disponible dans l'aide associée au framework XNA (version 1.0 refresh ici, version 2.0 ici)

Le code suivant montre un appel qui vérifie l'état de la touche "flèche haut".

if (Keyboard.GetState().IsKeyDown(Keys.Up))
{
  // ...
}
else 
{
  // ...
}

Les problèmes se dessinent...

On se rends vite compte des limitations de ce modèle. Dans de très nombreux cas, ce qui nous intéresse est le fait qu'une touche est appuyée ou relâchée - de ces deux informations, on peut déduire si une touche est active ou non. Par contre, en récupérant uniquement l'état courant de la touche clavier, on ne sait pas quand elle a été appuyée ou relâchée.

Si vous vous souvenez du billet concernant la souris, vous vous rappelez certainement de la méthode utilisée pour passer outre ce problème : l'astuce consiste principalement à stocker l'état précédent du clavier. D'autres techniques - dont certaines qui sont plus efficientes en termes de mémoire et de traitement - sont possibles: c'est l'une d'entre elle que nous allons utiliser. En fait, nous n'allons stocker que la liste des touches qui sont dans l'état KeyState.Down. Si nous supprimons une touche de cette liste, c'est qu'elle a été relachée. Si nous ajoutons une touche dans cette liste, c'est qu'elle a été appuyée. Et pour nous aider, plutôt que de tester toutes les touches une à une, nous allons directement utiliser la méthode KeyboardState.GetPressedKeys() qui va nous donner uniquement les touches dont nous avons besoin.

Cette fonction nous renvoie un tableau - donc une référence sur un objet en C# - que nous pouvons stocker pour une future utilisation.

Le code est alors très simple à écrire :

public class Keyboard
{
  public delegate void KeyHandler(Keys key);
public event KeyHandler OnKeyPress; public event KeyHandler OnKeyRelease;
private Keys lastState = null;
private bool IsInArray(Keys key, Keys array) { foreach (Keys k in array) { if (k == key) return true; } return false; }
private void FireOnKeyPress(Keys key) { if (OnKeyPress != null) { OnKeyPress(key); } }
private void FireOnKeyRelease(Keys key) { if (OnKeyRelease != null) { OnKeyRelease(key); } }
public void Update() { Keys currentState = Microsoft.Xna.Framework.Input. Keyboard.GetState().GetPressedKeys();
if (lastState != null) { foreach (Keys k in lastState) { if (!IsInArray(k, currentState)) { FireOnKeyRelease(k); } } foreach (Keys k in currentState) { if (!IsInArray(k, lastState)) { FireOnKeyPress(k); } } }
lastState = currentState; } }

L'utilisation de cette classe est elle aussi relativement simple : il suffit de déclarer une instance de la classe Keyboard définie, de lier des méthodes correspondantes aux event qui sont envoyés, et d'appeler la méthode Keyboard.Update() dans la méthode Game1.Update() de votre application. Vous recevez alors les événements OnKeyPress et OnKeyRelease lorsque l'utilisateur appuie ou libère une touche.

Nous avons toutefois un problème impossible à résoudre en utilisant seulement le framework XNA : puisque nous ne récupérons qu'un différentiel entre deux appel à la méthode Keyboard.Update(), nous n'avons aucune idée de l'ordre dans lequel les touches ont été appuyées ou relâchées et les événements sont générés dans un ordre assez aléatoire. Dans la plupart des cas, cela ne pose aucun problème : le temps séparant l'appui sur deux touches est généralement supérieur au temps s'écoulant entre deux appel à la méthode sus-nommée.

Un autre soucis : nous en sommes toujours à ne récupérer que des appuis de touche non filtrés : SHIFT et A sont toujours des touches dissociées. Mais nous pouvons maintenant écrire une surcouche grossière à notre classe Keyboard qui va filtrer les event pour générer des messages du type "l'utilisateur a appuyé sur tel caractère". Pour ce faire, il nous faut connaître l'état du clavier (quelles sont les touches rémanentes activées) et nous baser sur un layout de clavier.

Bien évidemment, en changeant de langue, le layout change : malheureusement, c'est au programmeur de gérer ce problème. Concrètement, il est préférable de stocker le layout dans un fichier de configuration et de configurer le filtre en fonction de la langue d'entrée de l'utilisateur. C'est une tâche un peu ingrate - mais vu la simplicité de notre modèle courant, il n'y a pas moyen d'y échapper.

Le code ci-dessous est basique : elle présente la classe KeyboardCharacterFilter, construite à partir d'une instance de la classe Keyboard. Elle intercepte les événements OnKeyPress et OnKeyRelease, les traite et génère deux événements selon le cas :

  • si on a pu effectuer la translation vers un caractère, on génère l'événement void OnChar(char c).
  • dans le cas contraire, on génère l'événement void OnSpecialChar(Keys key).
public enum KeyState
{
  On,
  Off
}
public class KeyboardCharacterFilter { private Keyboard keyboard; private KeyState capsState; private KeyState altState;
public delegate void NormalCharHandler(char c); public delegate void SpecialCharHandler(Keys key);
public event NormalCharHandler OnChar; public event SpecialCharHandler OnSpecialChar;
private char[][] translatedChars = new char[3][];
public KeyboardCharacterFilter(Keyboard keyboard, KeyState capsState) { this.keyboard = keyboard; this.capsState = capsState; this.altState = KeyState.Off;
this.keyboard.OnKeyPress += OnKeyPress; this.keyboard.OnKeyRelease += OnKeyRelease;
BuildTranslationArray(); }
private void BuildTranslationArray() { translatedChars[0] = new char[256]; translatedChars[1] = new char[256]; translatedChars[2] = new char[256];
translatedChars[0].Initialize(); translatedChars[1].Initialize(); translatedChars[2].Initialize();
// letters translatedChars[0][(int)Keys.A] = 'A'; translatedChars[1][(int)Keys.A] = 'a'; translatedChars[0][(int)Keys.B] = 'B'; translatedChars[1][(int)Keys.B] = 'b'; // ... translatedChars[0][(int)Keys.Z] = 'Z'; translatedChars[1][(int)Keys.Z] = 'z';
// numbers and accented chars translatedChars[0][(int)Keys.D1] = '1'; translatedChars[1][(int)Keys.D1] = '&'; translatedChars[0][(int)Keys.D2] = '2'; translatedChars[1][(int)Keys.D2] = 'é'; // ... translatedChars[1][(int)Keys.D0] = 'à'; translatedChars[2][(int)Keys.D0] = '@'; translatedChars[1][(int)Keys.OemQuotes] = '²'; translatedChars[0][(int)Keys.OemOpenBrackets] = '°'; translatedChars[1][(int)Keys.OemOpenBrackets] = ')'; translatedChars[2][(int)Keys.OemOpenBrackets] = ']'; translatedChars[0][(int)Keys.OemPlus] = '+'; translatedChars[1][(int)Keys.OemPlus] = '='; translatedChars[2][(int)Keys.OemPlus] = '}';
translatedChars[0][(int)Keys.OemBackslash] = '>'; translatedChars[1][(int)Keys.OemBackslash] = '<';
// ... translatedChars[0][(int)Keys.Space] = ' '; translatedChars[1][(int)Keys.Space] = ' '; translatedChars[2][(int)Keys.Space] = ' '; // and so on... this function is quite big }
private void ToggleCapsState() { if (this.capsState == KeyState.On) { this.capsState = KeyState.Off; } else { this.capsState = KeyState.On; } }
private void ToggleAltState() { if (this.altState == KeyState.On) { this.altState = KeyState.Off; } else { this.altState = KeyState.On; } }
private bool TranslateChar(Keys key, out char output) { char[] translation;
output = (char)0;
if (capsState == KeyState.On) { translation = translatedChars[0]; } else { translation = translatedChars[1]; }
if (altState == KeyState.On) { translation = translatedChars[2]; }
output = translation[(int)key];
if (output != (char)0) { return true; } return false; }
private void OnKeyPress(Keys key) { if (key == Keys.LeftShift || key == Keys.RightShift || key == Keys.CapsLock) { ToggleCapsState(); }
if (key == Keys.RightAlt) { ToggleAltState(); }
char translated;
if (TranslateChar(key, out translated)) { if (OnChar != null) { OnChar(translated); } } else { if (OnSpecialChar != null) { OnSpecialChar(key); } } }
private void OnKeyRelease(Keys key) { if (key == Keys.LeftShift || key == Keys.RightShift) { ToggleCapsState(); }
if (key == Keys.RightAlt) { ToggleAltState(); } } }

L'utilisation de cette classe est simple : il suffit de créer une instance de Keyboard et une instance de KeyboardCharacterFilter, d'initialiser les événements du filtre avec les méthodes ou fonctions correspondantes, et de ne pas oublier l'appel à Keyboard.Update() dans votre méthode Game1.Update().

Le traitement dans OnChar ou OnSpecialChar est simple, mais vous pouvez imaginer n'importe quel traitement possible en fait. Par exemple:

string toWrite = "";
private void OnChar(char c) { toWrite = String.Format("{0}{1}", toWrite, c); }
private void OnSpecialChar(Keys key) { if (key == Keys.Back && toWrite.Length > 0) { toWrite = write.Substring(0, toWrite.Length - 1); } }

On peut améliorer cet exemple en gérant les touches Keys.Left, Keys.Right, Keys.Home, Keys.End de manière à pouvoir naviguer dans la chaîne de caractère.

Bonus : et si je suis sous Windows...

Attention: Cette section est réservée à l'utilisation du framework XNA sous Windows : le truc qui y est mentionné ne fonctionnera pas sur XBox360, en raison de l'architecture sur laquelle XNA est construit sur cette plateforme[3].

Sous Windows, il est possible de récupérer les événements claviers et souris sans passer par XNA. Cette possibilité permet d'assurer une meilleure gestion de ces deux périphériques : en effet, on intercepte directement les événements générés par Windows, ce qui nous permet par exemple de supporter n'importe quel langage actif à peu de frais, y compris celles qui nécessitent un Input Method Editor (par exemple, le japonais). promit, modérateur sur gamedev.net et grand amateur de C# devant l'éternel, a ainsi écrit ce code, et Nypyren l'a ensuite modifié pour supporter la souris. Vous pouvez retrouver le code final ici.

Conclusion

Voilà, j'espère que cet article vous aura donné de bonnes bases concernant l'utilisation du clavier dans un projet XNA. Le prochain article approfondira encore un peu plus la gestion des différents périphériques d'entrée, puis nous retournerons à des sujets plus graphiques.

Amusez vous bien !

Notes

[1] Oui, je vous refais le coup à chaque fois.

[2] par la suite, on ométtra la référence à Microsoft.Xna.Framework; elle est implicite dans le reste du texte de ce billet.

[3] Sur XBox360, XNA est basé sur une partie réduite du framework .NET nommé Compact .Net qui n'offre pas l'accès au système sous-jacent.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet