Manière correcte d'abstraire un contrôleur XBox

12

J'ai un contrôleur XBox360 que j'aimerais utiliser comme entrée pour une application.

Ce que je ne peux pas déterminer, c'est la meilleure façon d'exposer cela via une interface.

Dans les coulisses, la classe qui gère le ou les contrôleurs s'appuie sur l'état du bouton d'interrogation.

J'ai d'abord essayé quelque chose de lien:

Event ButtonPressed() as ButtonEnum

ButtonEnumétait ButtonRed, ButtonStart, etc ...

Ceci est un peu limité en ce qu'il ne prend en charge que les pressions sur les boutons, pas les prises / motifs (appuyez deux fois, etc.)

L'idée suivante était d'exposer simplement l'état du bouton à l'application, par exemple

Property RedPressed as Boolean
Property StartPressed as Boolean
Property Thumb1XAxis as Double

C'est très flexible, mais cela oblige vraiment trop de travail dans l'application et nécessite que l'application interroge - je préférerais si possible les événements.

J'ai envisagé d'ajouter plusieurs événements, par exemple:

Event ButtonPressed(Button as ButtonEnum)
Event ButtonPressedTwice(Button as ButtonEnum)
Event ButtonHeldStart(Button as ButtonEnum)
Event ButtonHeldEnd(Button as ButtonEnum)

mais cela semble un peu maladroit et a été une vraie douleur sur l'écran "Bind button".

Quelqu'un peut-il m'indiquer la manière "correcte" de gérer les entrées des contrôleurs.

NB: J'utilise SlimDX dans la classe qui implémente l'interface. Cela me permet de lire l'état très facilement. Toutes les alternatives qui résoudraient mon problème sont également appréciées

De base
la source

Réponses:

21

Il n'y a pas de mappage parfait qui vous donne une abstraction spécifique à la plate-forme, car la plupart des identifiants qui ont du sens pour un contrôleur 360 sont faux pour un contrôleur PlayStation (A au lieu de X, B au lieu de Circle). Et bien sûr, une manette Wii est tout autre chose.

Le moyen le plus efficace que j'ai trouvé pour y remédier est d'utiliser trois couches d'implémentation. La couche inférieure est entièrement spécifique à la plate-forme / au contrôleur et sait combien de boutons numériques et d'axes analogiques sont disponibles. C'est cette couche qui sait interroger l'état du matériel, et c'est cette couche qui se souvient suffisamment de l'état précédent pour savoir quand un bouton vient d'être pressé, ou s'il a été enfoncé pendant plus d'un tick, ou s'il n'a pas été pressé . En dehors de cela, il est stupide - une classe à l'état pur représentant un seul type de contrôleur. Sa valeur réelle est d'abstraire le détail de l'interrogation de l'état du contrôleur loin de la couche intermédiaire.

La couche intermédiaire est le mappage de contrôle réel des vrais boutons aux concepts de jeu (par exemple A -> Jump). Nous les appelons impulsions au lieu de boutons, car ils ne sont plus liés à un type de contrôleur particulier. C'est à cette couche que vous pouvez remapper les contrôles (soit pendant le développement, soit au moment de l'exécution à la demande de l'utilisateur). Chaque plate-forme a son propre mappage des contrôles aux impulsions virtuelles . Vous ne pouvez pas et ne devez pas essayer de vous en sortir. Chaque contrôleur est unique et a besoin de sa propre cartographie. Les boutons peuvent correspondre à plusieurs impulsions (selon le mode de jeu) et plusieurs boutons peuvent correspondre à la même impulsion (par exemple, A et X accélèrent tous les deux, B et Y décélèrent tous les deux). La cartographie définit tout cela,

La couche supérieure est la couche de jeu. Cela prend des impulsions et peu importe comment elles ont été générées. Peut-être qu'ils venaient d'un contrôleur, ou d'un enregistrement d'un contrôleur, ou peut-être qu'ils venaient d'une IA. À ce niveau, vous ne vous en souciez pas. Ce qui vous importe, c'est qu'il y ait une nouvelle impulsion de saut, ou que l'impulsion d'accélération se soit poursuivie, ou que l'impulsion de plongée ait une valeur de 0,35 ce tick.

Avec ce type de système, vous écrivez la couche inférieure une fois pour chaque contrôleur. La couche supérieure est indépendante de la plateforme. Le code de la couche intermédiaire n'a besoin d'être écrit qu'une seule fois, mais les données (le remappage) doivent être refaites pour chaque plate-forme / contrôleur.

MrCranky
la source
Cela semble être une approche très propre et élégante. Je vous remercie!
Basic
1
Très agréable. Beaucoup mieux que le mien: P
Jordaan Mylonas
3
Très belle abstraction. Mais soyez prudent lors de la mise en œuvre: ne créez pas et ne détruisez pas de nouvel objet d'impulsion pour chaque action de l'utilisateur. Utilisez la mise en commun. Le ramasseur de déchets vous remerciera.
grega g
Absolument. Un tableau statique dimensionné au nombre maximum d'impulsions simultanées est presque toujours la meilleure option; car presque toujours, vous ne voulez qu'une seule instance de chaque impulsion active à la fois. Le plus souvent, ce tableau ne contient que quelques éléments, il est donc rapide d'itérer.
MrCranky
@grega vous remercie tous les deux - je n'y avais pas pensé.
Basic
1

Honnêtement, je dirais que l'interface optimale dépendra fortement de l'utilisation dans le jeu. Cependant, pour un scénario d'utilisation général, je suggérerais une architecture à deux niveaux qui sépare les approches basées sur les événements et les sondages.

Le niveau inférieur peut être considéré comme le niveau "basé sur l'interrogation" et expose l'état actuel d'un bouton. Une telle interface pourrait simplement avoir 2 fonctions, GetAnalogState(InputIdentifier)et GetDigitalState(InputIdentifier)InputIdentifierest une valeur énumérée représentant le bouton, le déclencheur ou le bâton contre lequel vous vérifiez. (L'exécution de GetAnalogState pour un bouton retournerait 1.0 ou 0.0, et l'exécution de GetDigitalState pour un stick analogique retournerait vrai si au-dessus d'un seuil prédéfini, ou faux sinon).

Le deuxième niveau utiliserait ensuite le niveau inférieur pour générer des événements lors d'un changement d'état et permettre aux éléments de s'inscrire aux rappels (les événements C # sont glorieux). Ces rappels incluraient des événements pour la presse, la libération, le tapotement, la longue tenue, etc. Pour un stick analogique, vous pouvez inclure des gestes, par exemple QCF pour un jeu de combat. Le nombre d'événements exposés dépendrait du niveau de détail d'un événement que vous souhaitez envoyer. Vous pouvez simplement tirer un simple ButtonStateChanged(InputIdentifier)si vous souhaitez gérer tout le reste dans une logique distincte.

Donc, si vous avez besoin de vérifier l'état actuel d'un bouton d'entrée ou d'une manette de contrôle, vérifiez le niveau inférieur. Si vous souhaitez simplement déclencher une fonction lors d'un événement d'entrée, inscrivez-vous pour un rappel à partir du deuxième niveau.

Jordaan Mylonas
la source