Réalisation d'une gestion des entrées
utilisateur clavier / GamePad
par Josselin Muller
Le framework XNA permet aux développeurs de créer des jeux multiplateforme (PC, Xbox 360, WIndows Mobile) simplement. Grâce à lui, le développeur peut se concentrer sur son projet sans trop réfléchir aux spécificités de chaque plateforme. Toutefois, certaines composantes d'un jeu multiplateforme peuvent se révéler délicates : c'est le cas des entrées utilisateur.
Vous découvrirez tout au long de ce tutoriel un moyen simple pour gérer les entrées utilisateur sans se soucier de leur provenance : clavier ou gamepad Xbox 360 par exemple.
Attention, cet article est destiné aux développeurs maîtrisant déjà le langage C# et ayant des bases dans le développement XNA.
Vous découvrirez tout au long de ce tutoriel un moyen simple pour gérer les entrées utilisateur sans se soucier de leur provenance : clavier ou gamepad Xbox 360 par exemple.
Attention, cet article est destiné aux développeurs maîtrisant déjà le langage C# et ayant des bases dans le développement XNA.
Introduction
Le framework XNA intègre de base un certain nombre de classes permettant une gestion basique des entrées utilisateur :
Malheureusement, elles ne donnent pas la possibilité de savoir si la touche en question vient juste d'être "appuyée" ou bien si celle-ci l'est depuis longtemps. Pour pallier ces désagréments majeurs, nous allons développer notre propre composant XNA chargé de gérer les entrées utilisateur et ainsi de nous offrir une plus grande flexibilité.
Pour commencer, créons une nouvelle classe que nous appellerons "Inputs". Cette classe héritera de "GameComponent" afin d'être compatible avec le système de composants fournis par le framework XNA :
- Mouse
- Keyboard
- GamePad
Malheureusement, elles ne donnent pas la possibilité de savoir si la touche en question vient juste d'être "appuyée" ou bien si celle-ci l'est depuis longtemps. Pour pallier ces désagréments majeurs, nous allons développer notre propre composant XNA chargé de gérer les entrées utilisateur et ainsi de nous offrir une plus grande flexibilité.
Pour commencer, créons une nouvelle classe que nous appellerons "Inputs". Cette classe héritera de "GameComponent" afin d'être compatible avec le système de composants fournis par le framework XNA :
class Inputs : GameComponent { public Inputs(Game game) : base(game) { } public override void Update(GameTime gameTime) { base.Update(gameTime); } } |
Ajoutons ensuite la ligne suivante dans la méthode "Initialize" de notre classe Game, afin d'intégrer notre composant dans la liste interne de notre jeu :
Components.Add(new Inputs(this)); |
Bonne nouvelle, notre composant est désormais intégré au jeu et restera toujours à jour grâce à la méthode "Update". Il ne nous reste plus qu'à lui ajouter nos fonctionnalités souhaitées !
Gestion du clavier
Commençons par la gestion des touches du clavier. Pour cela, nous allons constamment garder à jour deux "KeyboardState" qui représentent un état du clavier à un instant donné. Pourquoi deux états et pas qu'un seul ? En stockant l'état actuel mais aussi le précédent, nous avons la possibilité de savoir si une touche vient juste d'être appuyée, afin de ne pas déclencher une action à chaque tour de boucle de jeu sous prétexte que le joueur a laissé son doigt trop longtemps sur une touche.
class Inputs : GameComponent { private static KeyboardState _kbState; private static KeyboardState _prevKbState; public Inputs(Game game) : base(game) { _kbState = Keyboard.GetState(); _prevKbState = Keyboard.GetState(); } public override void Update(GameTime gameTime) { _prevKbState = _kbState; _kbState = Keyboard.GetState(); base.Update(gameTime); } } |
Nous pouvons désormais ajouter nos premières véritables fonctionnalités sous forme de méthode statique :
public static bool IsPressedKey(Keys k) { return _kbState.IsKeyDown(k); } public static bool IsPressedKeyOnce(Keys k) { return _kbState.IsKeyDown(k) && _prevKbState.IsKeyUp(k); } |
Gestion du GamePad
Maintenant que nous avons une gestion des touches du clavier, nous pouvons attaquer la gestion du Gamepad Xbox 360. Même principe que pour le clavier, à ceci près que nous stockerons des "GamePadState". Nous obtenons le code suivant :
class Inputs : GameComponent { private static KeyboardState _kbState; private static KeyboardState _prevKbState; private static GamePadState _gamePadState; private static GamePadState _prevGamePadState; public Inputs(Game game) : base(game) { _kbState = Keyboard.GetState(); _prevKbState = Keyboard.GetState(); _gamePadState = GamePad.GetState(0); _prevGamePadState = GamePad.GetState(0); } public override void Update(GameTime gameTime) { _prevKbState = _kbState; _kbState = Keyboard.GetState(); base.Update(gameTime); } public static bool IsPressedKey(Keys k) { return _kbState.IsKeyDown(k); } public static bool IsPressedKeyOnce(Keys k) { return _kbState.IsKeyDown(k) && _prevKbState.IsKeyUp(k); } public static bool IsPressedButton(Buttons b) { return _gamePadState.IsButtonDown(b); } public static bool IsPressedButtonOnce(Buttons b) { return _gamePadState.IsButtonDown(b) && _prevGamePadState.IsButtonUp(b); } } |
Gestion des données multi-plateformes
Notre système nous permet désormais de gérer à la fois les entrées clavier mais aussi le gamepad. Seulement, son utilisation est plutôt ennuyeuse. En effet, nous devrons constamment faire appel à deux méthodes différentes pour distinguer les deux périphériques (voir le code d'exemple ci-dessous) :
// Touche A du clavier ou Bouton A du gamepad if (Inputs.IsPressedKeyOnce(Keys.A) || Inputs.IsPressedButtonOnce(Buttons.A)) doSomething(); |
Cette méthode fonctionne, mais va vite devenir un fardeau pour les développeurs que nous sommes. Pour remplacer cette façon de faire, nous allons repenser le système sous forme d'actions plutôt que de touches. Ainsi, le code ci-dessus se transformerait en :
// Touche A du clavier ou Bouton A du gamepad if (Inputs.IsPressedOnce(InputActions.LaunchGame)) doSomething(); |
Liste d'actions
La première étape de cette transformation consiste à créer une liste d'actions disponibles dans notre jeu sous forme d'énumérateur. Pour faciliter son utilisation, il est conseillé de placer celui-ci à l'extérieur de la classe Inputs.
public enum InputActions { Accept, Cancel, Launch // etc } |
Mapping
Pour que notre système fonctionne, nous devons être en mesure d'associer des touches de clavier/gamepad à des actions. Pour cela, nous allons créer une classe de "Mapping" contenant les éléments suivants :
- Action concernée
- Périphérique cible
- Touche souhaitée
public enum Device { Keyboard, Gamepad } |
2ème étape : créer la classe de Mapping en elle-même :
public class Mapping { // Action liée public InputActions Action { get; set; } // Périphérique utilisé public Device Device { get; set; } // Touche associée public int Key { get; set; } // Constructeurs public Mapping(InputActions action, Device device, int key) { Action = action; Device = device; Key = key; } public Mapping(InputActions action, Keys k) : this(action, Device.Keyboard, (int)k) { } public Mapping(InputActions action, Buttons k) : this(action, Device.Gamepad, (int)k) { } // Méthodes Pressed / PressedOnce public bool Pressed { get { switch (Device) { case Device.Keyboard: return Inputs.IsPressedKey((Keys)Key); case Device.Gamepad: return Inputs.IsPressedButton((Buttons)Key); } return false; } } public bool PressedOnce { get { switch (Device) { case Device.Keyboard: return Inputs.IsPressedKeyOnce(((Keys)Key)); case Device.Gamepad: return Inputs.IsPressedButtonOnce(((Buttons)Key)); } return false; } } } |
La seule subtilité de cette classe se situe dans les méthodes Pressed et PressedOnce. Puisque nous ne stockons qu'un entier pour représenter la touche du Mapping, nous ne pouvons pas l'utiliser tel quel. Nous effectuons donc un "cast" de l'entier dans l'enum souhaité. Rappellons en effet que les enums ne sont que des entiers.
Pour terminer cette classe de mapping, nous allons lui rajouter une méthode statique facilitant la création d'une liste de mapping à partir d'une liste de couples Périphérique / Touche :
Pour terminer cette classe de mapping, nous allons lui rajouter une méthode statique facilitant la création d'une liste de mapping à partir d'une liste de couples Périphérique / Touche :
public static IEnumerable Make(InputActions action, params KeyValuePair[] keys) { return keys.Select(pair => new Mapping(action, pair.Key, pair.Value)); } |
Mise en place
Notre système de mappings étant achevé, nous pouvons l'intégrer à notre gestion des Inputs. Commençons par stocker et initialiser nos mappings.
Champ privé :
Champ privé :
private static Dictionary<InputActions, IEnumerable<Mapping>> _mappings = new Dictionary<InputActions, IEnumerable<Mapping>>(); |
Constructeur :
InitializeMappings(); |
Nouvelles méthodes :
private static void AddMapping(InputActions action, params KeyValuePair[] keys) { _mappings.Add(action, Mapping.Make(action, keys)); } private static void InitializeMappings() { AddMapping(InputActions.Accept, new KeyValuePair(Device.Keyboard,(int)Keys.Enter), new KeyValuePair(Device.Gamepad, (int)Buttons.A)); AddMapping(InputActions.Cancel, new KeyValuePair(Device.Keyboard, (int)Keys.Escape), new KeyValuePair(Device.Gamepad, (int)Buttons.B)); } |
Pour finir, nous devons ajouter deux méthodes pour utiliser nos mappings de façon invisible : IsPressed et IsPressedOnce :
public static bool IsPressed(InputActions action) { IEnumerable keys; return _mappings.TryGetValue(action, out keys) && keys.Any(k => k.Pressed); } public static bool IsPressedOnce(InputActions action) { IEnumerable keys; return _mappings.TryGetValue(action, out keys) && keys.Any(k => k.PressedOnce); } |
Conclusion
Notre système est désormais terminé et prêt à l'usage. L'ajout de nouveaux mappings se fait simplement, de même pour la vérification du lancement d'une action. Grâce à cette nouvelle version, vous pouvez créer votre jeu multiplateforme sans vous soucier de quel contrôleur de jeu le joueur utilisera.