Créez votre propre contrôleur de jeu

Source d'inspiration


Lors d'expositions de jeux, les développeurs d' Objects in Space ont montré une démonstration de leur jeu avec un contrôleur sur le cockpit d'un énorme vaisseau spatial. Il a été complété par des boutons lumineux, des appareils analogiques, des voyants d'état, des interrupteurs, etc ... Cela affecte grandement l'immersion dans le jeu:


Un didacticiel Arduino a été publié sur le site Web du jeu avec une description du protocole de communication pour ces contrôleurs.

Je veux créer la même chose pour mon jeu

Dans cet exemple, je dépenserai environ 40 $ pour ajouter de beaux, gros et lourds commutateurs au cockpit d'un simulateur de course. Les principaux coûts sont associés à ces commutateurs - si j'utilisais de simples commutateurs / boutons, le prix serait deux fois moins élevé! Il s'agit d'un véritable équipement qui peut supporter 240 watts de puissance, et je n'en laisserai sortir que 0,03 watts environ.

Avertissement: j'ai décidé d'économiser de l'argent, je laisse donc un lien vers un site chinois bon marché où j'achète un tas de composants / outils différents. L'un des inconvénients de l'achat de composants bon marché est qu'ils n'ont souvent pas de documentation, donc dans cet article, je vais résoudre ce problème.

Composants principaux






Outils en vedette



Logiciels


  • Arduino IDE pour programmer le processeur Arduino
  • Pour créer un contrôleur qui apparaît comme un véritable contrôleur / joystick USB matériel:
    • FLIP pour flasher le nouveau firmware du contrôleur USB Arduino
    • Bibliothèque Arduino-USB sur Github
  • Pour créer un contrôleur avec lequel le jeu communique directement ( ou qui apparaît comme un contrôleur / joystick USB virtuel )
    • Ma bibliothèque ois_protocol sur github
    • Pilote VJoy si vous souhaitez utiliser le contrôleur comme contrôleur / joystick USB virtuel.

Avertissement


J'ai étudié l'électronique au lycée, appris à utiliser un fer à souder, appris que les fils rouges doivent être connectés au rouge et le noir au noir ... Volts, ampères, résistance et les équations qui les connectent - c'est tout ce que ma formation formelle en électronique a été épuisée.

Pour moi, c'était un projet de formation, donc ça pourrait avoir de mauvais conseils ou des erreurs!

Partie 1. Assembler le contrôleur!


Nous travaillons avec des commutateurs sans documentation ...


Comme indiqué ci-dessus, j'achète des pièces bon marché chez un détaillant à faible marge, donc la première chose à faire est de comprendre comment ces commutateurs / boutons fonctionnent.

Bouton-poussoir / interrupteur simple


Avec le bouton, tout est simple - il n'y a pas de LED et seulement deux contacts. Basculez le multimètre en mode continuité / numérotation ( ) et toucher les sondes des différents contacts - OL (boucle ouverte, circuit ouvert) s'affichera à l'écran: cela signifie qu'il n'y a pas de connexion entre les deux sondes. Ensuite, nous appuyons sur le bouton, en touchant toujours les sondes de contact - quelque chose comme 0,1Ω devrait maintenant être affiché à l'écran et le multimètre émettra un bip ( indiquant qu'il y a une très faible résistance entre les sondes - un circuit fermé ).

Nous savons maintenant que lorsque le bouton est enfoncé, le circuit se ferme et lorsqu'il est enfoncé, il s'ouvre. Dans le diagramme, cela peut être décrit comme un simple interrupteur:

Nous connectons le commutateur à Arduino


Trouvez deux broches sur la carte Arduino: étiquetées GND et étiquetées «2» (ou tout autre nombre arbitraire - ce sont des broches d'E / S à usage général que nous pouvons contrôler via un logiciel).

Si nous connectons le commutateur de cette manière, puis nous ordonnons à Arduino de configurer la broche 2 en tant que broche INPUT, nous obtenons le circuit illustré à gauche (dans la figure ci-dessous). Lorsque le bouton est enfoncé, la broche 2 sera directement connectée à la masse / 0V, et lorsqu'elle est enfoncée, la broche 2 ne sera connectée à rien. Cet état ( non connecté à quoi que ce soit ) est appelé "flottant" (état de haute impédance) et, malheureusement, ce n'est pas un très bon état pour nos besoins. Lorsque nous lisons les données d'un contact dans le logiciel (en utilisant digitalRead (2) ), nous obtenons LOW si le contact est mis à la terre, et un résultat imprévisible (LOW ou HIGH) si le contact flotte!

Pour résoudre ce problème, nous pouvons configurer le contact pour qu'il soit en mode INPUT_PULLUP, qui se connecte à la résistance à l'intérieur du processeur et crée le circuit illustré à droite. Dans ce circuit, avec l'interrupteur ouvert, la broche 2 a un chemin de + 5V, donc quand elle est lue, le résultat sera toujours ÉLEVÉ. Lorsque l'interrupteur est fermé, le contact aura toujours un chemin avec une résistance élevée à + 5V, ainsi qu'un chemin sans résistance à la terre / 0V, qui "gagne", donc quand nous lisons le contact, nous obtenons un BAS.


Pour les développeurs de logiciels, l'ordre peut sembler inversé - lorsque nous cliquons sur le bouton, nous lisons false / LOW, et lorsqu'ils sont déprimés, nous lisons true / HIGH.

Vous pouvez faire le contraire, mais le processeur n'a que des résistances de pull-up intégrées et il n'y a pas de résistances de pull-down, nous allons donc nous en tenir à ce modèle.

Le programme le plus simple pour Arduino, qui lit l'état du commutateur et informe le PC de son état, ressemble à celui illustré ci-dessous. Vous pouvez cliquer sur le bouton de téléchargement dans l'IDE Arduino puis ouvrir le moniteur série (dans le menu Outils) pour voir les résultats.

void setup() { Serial.begin(9600); pinMode(2, INPUT_PULLUP); } void loop() { int state = digitalRead(pin); Serial.println( state == HIGH ? "Released" : "Pressed" ); delay(100);//artifically reduce the loop rate so the output is at a human readable rate... } 

D'autres commutateurs avec presque aucune documentation ...


Interrupteur LED à trois broches


Heureusement, sur les interrupteurs principaux de mon panneau, il y a des marques de trois contacts:


Je ne sais pas trop comment cela fonctionne, nous allons donc remettre le multimètre en mode continu et toucher toutes les paires de contacts lorsque l'interrupteur est activé et désactivé ... cependant, cette fois, le multimètre n'émet aucun bip lorsque nous touchons les sondes [GND] et [+] avec " allumez! La seule configuration dans laquelle le multimètre émet un bip ( détecte une connexion ) est lorsque le commutateur est sur «on» et que les sondes sont sur [+] et [lampe].

La LED à l'intérieur du commutateur bloque les mesures de continuité, donc à partir des tests ci-dessus, nous pouvons supposer que la LED est connectée directement à la broche [GND], et non aux contacts [+] et [lampe]. Ensuite, nous passerons le multimètre en mode de test de diode (symbole ) et vérifiez à nouveau la paire de contacts, mais cette fois la polarité est importante ( sonde rouge et noire ). Maintenant, si nous connectons la sonde rouge à [lampe] et la noire à [GND], alors la LED s'allumera et 2.25V sera affiché sur le multimètre. Il s'agit de la tension continue de la diode ou de la tension minimale requise pour l'allumer. Quelle que soit la position du commutateur, 2,25 V de [lampe] à [GND] fait s'allumer la LED. Si nous connectons la sonde rouge à [+] et la noire à [GND], la LED ne s'allumera que lorsque l'interrupteur est activé.

D'après ces lectures, nous pouvons supposer que l'intérieur de ce commutateur ressemble à quelque chose comme le diagramme ci-dessous:

  1. [+] et [lampe] sont court-circuités lorsque l'interrupteur est activé / fermé.
  2. Une tension positive de [lampe] à [GND] allume toujours la LED.
  3. Une tension positive de [+] à [GND] n'allume la LED que lorsque l'interrupteur est activé / fermé.



Honnêtement, nous ne pouvons que deviner la présence d'une résistance. La LED doit être connectée à la résistance appropriée afin de limiter le courant qui lui est fourni, sinon elle s'éteindra. Le mien n'a pas brûlé et il semble qu'il fonctionne correctement. Sur le forum du site Web du vendeur, j'ai trouvé un article qui parle d'une résistance installée prenant en charge jusqu'à 12 V, ce qui m'a fait gagner du temps pour vérifier / calculer une résistance appropriée.

Nous connectons le commutateur à Arduino


Le moyen le plus simple consiste à utiliser le commutateur avec Arduino, en ignorant la broche [lampe]: connectez [GND] au GND dans Arduino et connectez [+] à l'un des contacts Arduino numérotés, par exemple 3.

Si nous configurons la broche 3 comme INPUT_PULLUP ( comme pour le bouton précédent ), nous obtiendrons le résultat ci-dessous. La partie supérieure gauche montre la valeur que nous recevrons en exécutant «digitalRead (3)» dans le code Arduino.

Lorsque l'interrupteur est allumé / fermé, nous lisons le BAS et la LED s'allume! Pour utiliser un tel commutateur dans cette configuration, nous pouvons utiliser le même code Arduino que dans l'exemple de bouton.


Problèmes de cette solution


Après la connexion à l'Arduino, le circuit complet ressemble à ceci:


Cependant, ici, nous pouvons voir que lorsque l'interrupteur est fermé, en plus de la petite résistance de limitation de courant devant la LED (je suppose que sa résistance est de 100 Ohms), il y a aussi une résistance de rappel de 20 kOhm, ce qui réduit encore la quantité de courant traversant la LED. Cela signifie que bien que le circuit fonctionne, la LED ne sera pas très lumineuse.

Un autre inconvénient de ce schéma est que nous n'avons pas de contrôle logiciel sur la LED - elle est allumée lorsque l'interrupteur est allumé et désactivée dans le cas contraire.

Vous pouvez voir ce qui se passe si nous connectons la broche [lampe] à 0V ou + 5V.

Si [la lampe] est connectée à 0V, alors la LED est constamment éteinte ( quelle que soit la position du commutateur ), et la reconnaissance de position Arduino est toujours effectuée. Cela nous permet de désactiver par programmation la LED!


Si [la lampe] est connectée à + 5V, alors la LED est constamment allumée ( quelle que soit la position de l'interrupteur ), cependant, la reconnaissance de la position Arduino est cassée - HIGH sera toujours lu à partir du contact.


Nous connectons correctement ce commutateur à Arduino


Nous pouvons surmonter les limitations décrites ci-dessus ( faible courant / luminosité de la LED et le manque de contrôle de programme sur la LED ) en écrivant plus de code! Pour résoudre le conflit entre la capacité de contrôler la LED et la reconnaissance de position qui a été interrompue à cause de cela, nous pouvons séparer les deux tâches à temps, c'est-à-dire éteindre temporairement la LED lors de la lecture du contact du capteur (3).

Tout d'abord, connectez la broche [lampe] à une autre broche Arduino à usage général, par exemple à 4, afin de pouvoir contrôler la lampe.

Pour créer un programme qui lira correctement la position de l'interrupteur et contrôlera la LED (nous la ferons clignoter), il suffit d'éteindre la LED avant de lire l'état de l'interrupteur. La LED s'éteindra juste une fraction de milliseconde, donc le scintillement ne devrait pas être perceptible:

 int pinSwitch = 3; int pinLed = 4; void setup() { //connect to the PC Serial.begin(9600); //connect our switch's [+] connector to a digital sensor, and to +5V through a large resistor pinMode(pinSwitch, INPUT_PULLUP); //connect our switch's [lamp] connector to 0V or +5V directly pinMode(pinLed, OUTPUT); } void loop() { int lampOn = (millis()>>8)&1;//make a variable that alternates between 0 and 1 over time digitalWrite(pinLed, LOW);//connect our [lamp] to +0V so the read is clean int state = digitalRead(pinSwitch); if( lampOn ) digitalWrite(pinLed, HIGH);//connect our [lamp] to +5V Serial.println(state);//report the switch state to the PC } 

Dans Arduino Mega, les broches 2-13 et 44-46 peuvent utiliser la fonction analogWrite, qui ne génère pas réellement de tension de 0V à + 5V, mais l'approche en utilisant une onde carrée. Si vous le souhaitez, vous pouvez l'utiliser pour contrôler la luminosité de la LED! Ce code fera pulser la lumière, pas seulement le scintillement:

 void loop() { int lampState = (millis()>>1)&0xFF;//make a variable that alternates between 0 and 255 over time digitalWrite(pinLed, LOW);//connect our [lamp] to +0V so the read is clean int state = digitalRead(pinSwitch); if( lampState > 0 ) analogWrite(pinLed, lampState); } 

Conseils d'assemblage


Le message est déjà assez volumineux, donc je n'ajouterai pas le tutoriel sur la soudure, vous pouvez le chercher sur Google!

Cependant, je donnerai les conseils les plus élémentaires:

  • Lorsque vous connectez des fils avec de grands contacts métalliques, assurez-vous d'abord que le fer à souder est chaud et chauffez le contact métallique pendant un certain temps. Le sens de la soudure est de former un joint permanent en créant un alliage, mais si une seule partie du joint est chaude, vous pouvez facilement obtenir un «joint froid» qui ressemble à un joint, mais qui n'est pas réellement connecté.
  • Lors de la connexion des deux fils, mettez d' abord sur l'un d'eux un morceau de tube thermorétractable - après la connexion, le tube ne peut pas être mis. Cela semble évident, mais je l'oublie constamment et je dois utiliser du ruban électrique à la place du tube ... Retirez le tube rétractable de la connexion afin qu'il ne chauffe pas à l'avance. Après avoir vérifié la connexion soudée, faites glisser le tube dessus et chauffez-le.
  • Les petits fils de connexion minces que j'ai mentionnés au début conviennent bien aux connexions sans soudure (par exemple, lorsqu'ils sont connectés à un Arduino!), Mais plutôt fragiles. Après le soudage, utilisez un pistolet à colle pour les fixer et éliminer toutes les contraintes de la connexion elle-même. Par exemple, les fils rouges dans l'image ci-dessous peuvent être accidentellement tirés pendant le fonctionnement, donc après la soudure, je les ai fixés avec une goutte de colle chaude:


Partie 2. Nous transformons l'appareil en contrôleur de jeu!


Pour que le système d'exploitation reconnaisse l'appareil en tant que contrôleur de jeu USB, vous avez besoin d'un code assez simple, mais, malheureusement, vous devez également remplacer le micrologiciel de la puce USB Arduino par un autre, qui peut être pris ici: https://github.com/harlequin-tech/arduino-usb .

Mais après avoir téléchargé ce firmware sur Arduino, l'appareil devient un joystick USB et cesse d'être Arduino. Par conséquent, pour le reprogrammer, vous devez re-flasher le firmware Arduino d'origine. Ces itérations sont assez douloureuses - chargez le code Arduino, flashez le firmware du joystick, testez, flashez le firmware Arduino, répétez ...

Un exemple de programme pour Arduino pouvant être utilisé avec ce micrologiciel est illustré ci-dessous - il configure les trois boutons en tant qu'entrées, lit leurs valeurs, copie les valeurs dans la structure de données attendue par ce micrologiciel, puis envoie les données. Laver, savon, répéter.

 // define DEBUG if you want to inspect the output in the Serial Monitor // don't define DEBUG if you're ready to use the custom firmware #define DEBUG //Say we've got three buttons, connected to GND and pins 2/3/4 int pinButton1 = 2; int pinButton2 = 3; int pinButton3 = 4; void setup() { //configure our button's pins properly pinMode(pinButton1, INPUT_PULLUP); pinMode(pinButton2, INPUT_PULLUP); pinMode(pinButton3, INPUT_PULLUP); #if defined DEBUG Serial.begin(9600); #else Serial.begin(115200);//The data rate expected by the custom USB firmware delay(200); #endif } //The structure expected by the custom USB firmware #define NUM_BUTTONS 40 #define NUM_AXES 8 // 8 axes, X, Y, Z, etc typedef struct joyReport_t { int16_t axis[NUM_AXES]; uint8_t button[(NUM_BUTTONS+7)/8]; // 8 buttons per byte } joyReport_t; void sendJoyReport(struct joyReport_t *report) { #ifndef DEBUG Serial.write((uint8_t *)report, sizeof(joyReport_t));//send our data to the custom USB firmware #else // dump human readable output for debugging for (uint8_t ind=0; ind<NUM_AXES; ind++) { Serial.print("axis["); Serial.print(ind); Serial.print("]= "); Serial.print(report->axis[ind]); Serial.print(" "); } Serial.println(); for (uint8_t ind=0; ind<NUM_BUTTONS/8; ind++) { Serial.print("button["); Serial.print(ind); Serial.print("]= "); Serial.print(report->button[ind], HEX); Serial.print(" "); } Serial.println(); #endif } joyReport_t joyReport = {}; void loop() { //check if our buttons are pressed: bool button1 = LOW == digitalRead( pinButton1 ); bool button2 = LOW == digitalRead( pinButton2 ); bool button3 = LOW == digitalRead( pinButton3 ); //write the data into the structure joyReport.button[0] = (button1?0x01:0) | (button2?0x02:0) | (button3?0x03:0); //send it to the firmware sendJoyReport(joyReport) } 

Partie 3. Nous intégrons l'appareil à notre propre jeu!


Si vous avez le contrôle du jeu avec lequel l'appareil doit interagir, vous pouvez également communiquer directement avec le contrôleur - il n'est pas nécessaire de le rendre visible pour le système d'exploitation comme un joystick! Au début de l'article, j'ai mentionné les objets dans l'espace; c'est l'approche utilisée par ses développeurs. Ils ont créé un simple protocole de communication ASCII qui permet au contrôleur et au jeu de communiquer entre eux. Énumérez simplement les ports série du système ( ce sont des ports COM sur Windows; au fait, regardez à quel point il a l'air horrible en C ), trouvez le port auquel le périphérique appelé «Arduino» est connecté, et commencez à lire / écrire ASCII à partir de ce lien.

Du côté Arduino, nous utilisons simplement les fonctions Serial.print qui ont été utilisées dans les exemples ci-dessus.

Au début de cet article, j'ai également mentionné ma bibliothèque pour résoudre ce problème: https://github.com/hodgman/ois_protocol .

Il contient du code C ++ qui peut être intégré dans le jeu et utilisé comme «serveur», et du code Arduino qui peut être exécuté dans le contrôleur pour l'utiliser comme «client».

Personnaliser Arduino


Dans example_hardware.h, j'ai créé des classes pour abstraire des boutons / boutons radio individuels; par exemple, "Switch" est un simple bouton du premier exemple., et "LedSwitch2Pin" est un commutateur avec une LED contrôlée du deuxième exemple.

L'exemple de code pour ma barre de boutons est dans example.ino .

À titre d'exemple, disons que nous avons un seul bouton qui doit être envoyé au jeu et une LED contrôlée par le jeu. Le code Arduino requis ressemble à ceci:

 #include "ois_protocol.h" //instantiate the library OisState ois; //inputs are values that the game will send to the controller struct { OisNumericInput myLedInput{"Lamp", Number}; } inputs; //outputs are values the controller will send to the game struct { OisNumericOutput myButtonOutput{"Button", Boolean}; } outputs; //commands are named events that the controller will send to the game struct { OisCommand quitCommand{"Quit"}; } commands; int pinButton = 2; int pinLed = 3; void setup() { ois_setup_structs(ois, "My Controller", 1337, 42, commands, inputs, outputs); pinMode(pinButton, INPUT_PULLUP); pinMode(pinLed, OUTPUT); } void loop() { //read our button, send it to the game: bool buttonPressed = LOW == digitalRead(pin); ois_set(ois, outputs.myButtonOutput, buttonPressed); //read the LED value from the game, write it to the LED pin: analogWrite(pinLed, inputs.myLedInput.value); //example command / event: if( millis() > 60 * 1000 )//if 60 seconds has passed, tell the game to quit ois_execute(ois, commands.quitCommand); //run the library code (communicates with the game) ois_loop(ois); } 

Personnalisez le jeu


Le code du jeu est écrit dans le style "en-tête unique". Pour importer la bibliothèque, incluez oisdevice.h dans le jeu.

Dans un seul fichier CPP, avant d'exécuter l'en-tête #include, écrivez #define OIS_DEVICE_IMPL et #define OIS_SERIALPORT_IMPL - cela ajoutera le code source des classes au fichier CPP. Si vous avez vos propres instructions, journaux, chaînes ou vecteurs, il existe plusieurs autres macros OIS_ * que vous pouvez définir avant d'importer l'en-tête pour tirer parti des capacités du moteur.

Pour répertorier les ports COM et créer une connexion avec un périphérique spécifique, vous pouvez utiliser le code suivant:

 OIS_PORT_LIST portList; OIS_STRING_BUILDER sb; SerialPort::EnumerateSerialPorts(portList, sb, -1); for( auto it = portList.begin(); it != portList.end(); ++it ) { std::string label = it->name + '(' + it->path + ')'; if( /*device selection choice*/ ) { int gameVersion = 1; OisDevice* device = new OisDevice(it->id, it->path, it->name, gameVersion, "Game Title"); ... } } 

Après avoir reçu une instance OisDevice, vous devez appeler régulièrement sa fonction membre Poll (par exemple, dans chaque trame), vous pouvez obtenir l'état actuel de la sortie du contrôleur à l'aide de DeviceOutputs (), utiliser les événements de périphérique à l'aide de PopEvents () et envoyer des valeurs au périphérique à l'aide de SetInput ().

Un exemple d'application faisant tout cela peut être trouvé ici: example_ois2vjoy / main.cpp .

Partie 4. Et si je veux les parties 2 et 3 en même temps?


Pour que le contrôleur fonctionne dans d'autres jeux (partie 2), vous devez installer votre propre firmware et un programme Arduino, mais pour que le contrôleur soit entièrement programmé par le jeu, nous avons utilisé le firmware Arduino standard et un autre programme Arduino. Mais que se passe-t-il si nous voulons avoir les deux possibilités en même temps?

L'exemple d'application auquel j'ai donné le lien ci-dessus ( ois2vjoy ) résout ce problème.

Cette application communique avec le périphérique OIS (le programme de la partie 3), puis sur le PC convertit ces données en données régulières de contrôleur / joystick, qui sont ensuite transférées vers le contrôleur virtuel / périphérique de joystick. Cela signifie que vous pouvez autoriser votre contrôleur à utiliser en permanence la bibliothèque OIS (aucun autre micrologiciel n'est requis), et si nous voulons l'utiliser comme un contrôleur / joystick normal, il suffit d'exécuter l'application ois2vjoy sur le PC, qui effectue la conversion.

Partie 5. Achèvement


J'espère que quelqu'un a trouvé cet article utile ou intéressant. Merci d'avoir lu jusqu'au bout!

Si vous êtes curieux, je vous invite à participer au développement de la bibliothèque ois_protocol ! Je pense que ce sera formidable de développer un protocole unique pour prendre en charge toutes sortes de contrôleurs faits maison dans les jeux et encourager les jeux à prendre directement en charge les contrôleurs faits maison!

Source: https://habr.com/ru/post/fr442816/


All Articles