Crea tu propio controlador de juego

Fuente de inspiración


En las exhibiciones de juegos, los desarrolladores de Objects in Space mostraron una demostración de su juego con un controlador en la cabina de una enorme nave espacial. Se complementó con botones iluminadores, dispositivos analógicos, indicadores luminosos de estado, interruptores, etc. ... Esto afecta en gran medida la inmersión en el juego:


Se ha publicado un tutorial de Arduino en el sitio web del juego con una descripción del protocolo de comunicación para dichos controladores.

Quiero crear lo mismo para mi juego

En este ejemplo, gastaré alrededor de $ 40 para agregar interruptores hermosos, grandes y pesados ​​a la cabina de un simulador de carreras. Los costos principales están asociados con estos interruptores: si usara interruptores / botones simples, ¡el precio sería la mitad! Este es un equipo real que puede soportar 240 vatios de potencia, y solo dejaré salir aproximadamente 0.03 vatios.

Advertencia: decidí ahorrar dinero, así que dejo un enlace a un sitio web chino barato donde compro un montón de diferentes componentes / herramientas. Uno de los inconvenientes de comprar componentes baratos es que a menudo no tienen documentación, por lo que en este artículo resolveré este problema.

Componentes principales






Herramientas destacadas



Software


  • Arduino IDE para programar el procesador Arduino
  • Para crear un controlador que aparezca como un controlador / joystick USB de hardware real:
    • FLIP para flashear nuevo firmware en el controlador USB Arduino
    • Biblioteca Arduino-usb en github
  • Para crear un controlador con el que el juego se comunica directamente ( o que aparece como un controlador / joystick virtual USB )
    • Mi biblioteca ois_protocol en github
    • Controlador VJoy si desea utilizar el controlador como un controlador / joystick virtual USB.

Advertencia


Estudié electrónica en la escuela secundaria, aprendí a usar un soldador, aprendí que los cables rojos deben conectarse al rojo y el negro al negro ... Voltios, amperios, resistencia y las ecuaciones que los conectan, eso es todo lo que agotó mi entrenamiento formal en electrónica.

Para mí fue un proyecto de capacitación, por lo que podría tener malos consejos o errores.

Parte 1. ¡Poner el controlador junto!


Trabajamos con interruptores sin documentación ...


Como se indicó anteriormente, compro piezas baratas de un minorista de bajo margen, por lo que lo primero que debe hacer es descubrir cómo funcionan estos interruptores / botones.

Pulsador / interruptor simple


Con el botón, todo es simple: no hay LED y solo dos contactos. Cambie el multímetro al modo de continuidad / marcación ( ) y toque las sondas de diferentes contactos: OL (bucle abierto, circuito abierto) se mostrará en la pantalla: esto significa que no hay conexión entre las dos sondas. Luego presionamos el botón, aún tocando las sondas de contacto: ahora debería aparecer algo como 0.1Ω en la pantalla y el multímetro comenzará a emitir un pitido ( lo que indica que hay una resistencia muy baja entre las sondas, un circuito cerrado ).

Ahora sabemos que cuando se presiona el botón, el circuito se cierra y, cuando se presiona, se abre. En el diagrama, esto se puede describir como un simple interruptor:

Conectamos el interruptor a Arduino


Encuentre dos pines en la placa Arduino: etiquetados GND y etiquetados "2" (o cualquier otro número arbitrario; estos son pines de E / S de propósito general que podemos controlar a través del software).

Si conectamos el interruptor de esta manera, y luego le ordenamos a Arduino que configure el pin 2 como un pin de ENTRADA, obtenemos el circuito que se muestra a la izquierda (en la figura a continuación). Cuando se presiona el botón, el pin 2 se conectará directamente a tierra / 0 V, y cuando se presione, el pin 2 no se conectará a nada. Este estado ( no conectado a nada ) se llama "flotante" (estado de alta impedancia) y, desafortunadamente, esta no es una muy buena condición para nuestros propósitos. Cuando leemos datos de un contacto en el software ( usando digitalRead (2) ), obtenemos BAJO si el contacto está conectado a tierra, y un resultado impredecible (BAJO o ALTO) si el contacto está flotando.

Para solucionar esto, podemos configurar el contacto para que esté en modo INPUT_PULLUP, que se conecta a la resistencia dentro del procesador y crea el circuito que se muestra a la derecha. En este circuito, con el interruptor abierto, el pin 2 tiene una ruta de + 5V, por lo que cuando se lee, el resultado siempre será ALTO. Cuando el interruptor está cerrado, el contacto seguirá teniendo una ruta con alta resistencia a + 5V, así como una ruta sin resistencia a tierra / 0V, que "gana", por lo que cuando leemos el contacto, obtenemos un BAJO.


Para los desarrolladores de software, el orden puede parecer invertido: cuando hacemos clic en el botón, leemos falso / BAJO, y cuando estamos deprimidos, leemos verdadero / ALTO.

Puede hacer lo contrario, pero el procesador solo tiene resistencias pull-up incorporadas y no hay resistencias pull-down, por lo que nos quedaremos con este modelo.

El programa más simple para Arduino, que lee el estado del interruptor y le dice a la PC sobre su estado, se parece a lo que se muestra a continuación. Puede hacer clic en el botón de descarga en el IDE de Arduino y luego abrir el Monitor de serie (en el menú Herramientas) para ver los resultados.

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... } 

Otros interruptores casi sin documentación ...


Interruptor LED de tres pines


Afortunadamente, en los interruptores principales de mi panel hay marcas de tres contactos:


No estoy completamente seguro de cómo funciona, por lo que cambiaremos el multímetro a modo continuo y tocaremos todos los pares de contactos cuando el interruptor esté encendido y apagado ... sin embargo, esta vez el multímetro no emite ningún pitido cuando tocamos las sondas [GND] y [+] con " encender La única configuración en la que el multímetro emite un pitido ( detecta una conexión ) es cuando el interruptor está "encendido" y las sondas están en [+] y [lámpara].

El LED dentro del interruptor bloquea las mediciones de continuidad, por lo que de las pruebas anteriores podemos suponer que el LED está conectado directamente al pin [GND], y no a los contactos [+] y [lámpara]. A continuación, cambiaremos el multímetro al modo de prueba de diodos (símbolo ) y verifique nuevamente el par de contactos, pero esta vez la polaridad es importante ( sonda roja y negra ). Ahora, si conectamos la sonda roja a [lámpara] y la negra a [GND], el LED se iluminará y se mostrarán 2.25V en el multímetro. Este es el voltaje directo del diodo, o el voltaje mínimo requerido para encenderlo. Independientemente de la posición del interruptor, 2.25V desde [lámpara] a [GND] hace que el LED se ilumine. Si conectamos la sonda roja a [+] y la negra a [GND], el LED se encenderá solo cuando el interruptor esté encendido.

A partir de estas lecturas, podemos suponer que el interior de este interruptor se parece al siguiente diagrama:

  1. [+] y [lámpara] están en cortocircuito cuando el interruptor está encendido / cerrado.
  2. Un voltaje positivo de [lámpara] a [GND] siempre ilumina el LED.
  3. Un voltaje positivo de [+] a [GND] enciende el LED solo cuando el interruptor está encendido / cerrado.



Honestamente, solo podemos adivinar la presencia de una resistencia. El LED debe estar conectado a la resistencia adecuada para limitar la corriente que se le suministra, o se quemará. La mía no se quemó y parece que funciona correctamente. En el foro del sitio web del vendedor, encontré una publicación que habla sobre una resistencia instalada que admite hasta 12 V, y esto me ahorró tiempo en verificar / calcular una resistencia adecuada.

Conectamos el interruptor a Arduino


La forma más fácil es usar el interruptor con Arduino, ignorando el pin [lámpara]: conectar [GND] al GND en Arduino y conectar [+] a uno de los contactos numerados de Arduino, por ejemplo 3.

Si configuramos el pin 3 como INPUT_PULLUP ( igual que para el botón anterior ), obtendremos el resultado que se muestra a continuación. La esquina superior izquierda muestra el valor que recibiremos al ejecutar "digitalRead (3)" en el código Arduino.

Cuando el interruptor está encendido / cerrado, leemos el LOW y el LED se ilumina. Para usar dicho interruptor en esta configuración, podemos usar el mismo código Arduino que en el ejemplo del botón.


Problemas de esta solucion


Después de conectarse al Arduino, el circuito completo se ve así:


Sin embargo, aquí podemos ver que cuando el interruptor está cerrado, además de la pequeña resistencia limitadora de corriente frente al LED (supongo que su resistencia es de 100 ohmios), también hay una resistencia pull-up de 20 kOhm, lo que reduce aún más la cantidad de corriente que fluye a través del LED. Esto significa que aunque el circuito funciona, el LED no será muy brillante.

Otro inconveniente de este esquema es que no tenemos control de software sobre el LED: se enciende cuando el interruptor está encendido y se deshabilita en el caso contrario.

Puede ver qué sucede si conectamos el pin [lámpara] a 0V o + 5V.

Si [lámpara] está conectada a 0V, entonces el LED está constantemente apagado ( independientemente de la posición del interruptor ), y el reconocimiento de posición de Arduino aún se realiza. ¡Esto nos permite desactivar mediante programación el LED!


Si la [lámpara] está conectada a + 5V, entonces el LED está encendido constantemente ( independientemente de la posición del interruptor ), sin embargo, el reconocimiento de la posición de Arduino está roto: ALTO siempre se leerá desde el contacto.


Conectamos este interruptor a Arduino correctamente


¡Podemos superar las limitaciones descritas anteriormente ( baja corriente / brillo del LED y la falta de control del programa sobre el LED ) escribiendo más código! Para resolver el conflicto entre la capacidad de controlar el LED y el reconocimiento de posición que se rompió debido a él, podemos separar las dos tareas a tiempo, es decir, apagar temporalmente el LED al leer el contacto del sensor (3).

Primero, conecte el pin [lámpara] a otro pin Arduino de uso general, por ejemplo, a 4 para poder controlar la lámpara.

Para crear un programa que lea correctamente la posición del interruptor y controle el LED (haremos que parpadee), solo tenemos que apagar el LED antes de leer el estado del interruptor. El LED se apagará solo una fracción de milisegundo, por lo que el parpadeo no debería ser notable:

 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 } 

En Arduino Mega, los pines 2-13 y 44-46 pueden usar la función analogWrite, que en realidad no genera voltaje de 0V a + 5V, pero se aproxima usando una onda cuadrada. Si lo desea, puede usarlo para controlar el brillo del LED. Este código hará que la luz palpite, no solo parpadee:

 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); } 

Consejos de montaje


La publicación ya es bastante grande, así que no agregaré el tutorial de soldadura, ¡puedes buscarlo en Google!

Sin embargo, daré los consejos más básicos:

  • Al conectar cables con contactos metálicos grandes, primero asegúrese de que el soldador esté caliente y caliente el contacto metálico por un tiempo. El significado de la soldadura es formar una conexión permanente creando una aleación, pero si solo una parte de la conexión está caliente, entonces puede obtener fácilmente una "conexión en frío" que parece una conexión, pero que en realidad no está conectada.
  • Al conectar los dos cables, primero coloque en uno de ellos un trozo de tubo termorretráctil; después de la conexión, el tubo no se puede colocar. Esto parece obvio, pero lo olvido constantemente y tengo que usar cinta aislante en lugar del tubo ... Retire el tubo retráctil de la conexión para que no se caliente antes de tiempo. Después de verificar la conexión soldada, deslice el tubo sobre él y caliéntelo.
  • Los pequeños y delgados cables de conexión que mencioné al principio son adecuados para conexiones sin soldadura (por ejemplo, cuando se conectan a un Arduino), pero bastante frágiles. Después de soldar, use una pistola de pegamento para repararlos y eliminar todas las tensiones de la conexión. Por ejemplo, los cables rojos en la imagen a continuación se pueden tirar accidentalmente durante la operación, por lo que después de soldarlos los arreglé con una gota de pegamento caliente:


Parte 2. ¡Convertimos el dispositivo en un controlador de juego!


Para que el sistema operativo reconozca el dispositivo como un controlador de juegos USB, necesita un código bastante simple, pero, desafortunadamente, también debe reemplazar el firmware del chip USB Arduino por otro, que puede tomar aquí: https://github.com/harlequin-tech/arduino-usb .

Pero después de cargar este firmware en Arduino, el dispositivo se convierte en un joystick USB y deja de ser Arduino. Por lo tanto, para reprogramarlo, debe volver a actualizar el firmware original de Arduino. Estas iteraciones son bastante dolorosas: cargue el código Arduino, actualice el firmware del joystick, pruebe, actualice el firmware del arduino, repita ...

A continuación se muestra un ejemplo de un programa para Arduino que se puede usar con este firmware: configura tres botones como entradas, lee sus valores, copia los valores a la estructura de datos esperada por este firmware y luego envía los datos. Lavar, jabón, repetir.

 // 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) } 

Parte 3. ¡Integramos el dispositivo con nuestro propio juego!


Si tienes control sobre el juego con el que el dispositivo debe interactuar, entonces, como alternativa, puedes comunicarte directamente con el controlador, ¡no es necesario que sea visible para el sistema operativo como un joystick! Al comienzo de la publicación, mencioné Objetos en el espacio; Este es el enfoque que usaron sus desarrolladores. Crearon un protocolo de comunicación ASCII simple que permite que el controlador y el juego se comuniquen entre sí. Simplemente enumere los puertos seriales del sistema ( son puertos COM en Windows; por cierto, mire lo horrible que se ve en C ), encuentre el puerto al que está conectado el dispositivo llamado "Arduino" y comience a leer / escribir ASCII desde este enlace.

En el lado de Arduino, solo usamos las funciones Serial.print que se usaron en los ejemplos anteriores.

Al comienzo de esta publicación, también mencioné mi biblioteca para resolver este problema: https://github.com/hodgman/ois_protocol .

Contiene código C ++ que puede integrarse en el juego y usarse como un "servidor", y código Arduino que puede ejecutarse en el controlador para usarlo como un "cliente".

Personaliza Arduino


En example_hardware.h, creé clases para abstraer botones individuales / botones de radio; por ejemplo, "Switch" es un botón simple del primer ejemplo. y "LedSwitch2Pin" es un interruptor con un LED controlado del segundo ejemplo.

El código de muestra para mi barra de botones es en example.ino .

Como un pequeño ejemplo, digamos que tenemos un solo botón que debe enviarse al juego y un LED controlado por el juego. El código Arduino requerido se ve así:

 #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); } 

Personaliza el juego


El código del juego está escrito en el estilo de "encabezado único". Para importar la biblioteca, incluya oisdevice.h en el juego.

En un solo archivo CPP, antes de ejecutar el encabezado #include, escriba #define OIS_DEVICE_IMPL y #define OIS_SERIALPORT_IMPL: esto agregará el código fuente de las clases al archivo CPP. Si tiene sus propias declaraciones, registros, cadenas o vectores, existen varias otras macros OIS_ * que puede definir antes de importar el encabezado para aprovechar las capacidades del motor.

Para enumerar los puertos COM y crear una conexión con un dispositivo específico, puede usar el siguiente código:

 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"); ... } } 

Después de recibir una instancia de OisDevice, debe llamar regularmente a su función miembro Poll (por ejemplo, en cada cuadro), puede obtener el estado actual de la salida del controlador usando DeviceOutputs (), usar eventos del dispositivo usando PopEvents () y enviar valores al dispositivo usando SetInput ().

Aquí se puede encontrar una aplicación de ejemplo que hace todo esto: example_ois2vjoy / main.cpp .

Parte 4. ¿Qué sucede si quiero las partes 2 y 3 al mismo tiempo?


Para que el controlador funcione en otros juegos (parte 2), debes instalar tu propio firmware y un programa Arduino, pero para que el juego esté completamente programado por el juego, utilizamos el firmware Arduino estándar y otro programa Arduino. Pero, ¿qué pasa si queremos tener ambas posibilidades al mismo tiempo?

La aplicación de muestra a la que le di el enlace anterior ( ois2vjoy ) resuelve este problema.

Esta aplicación se comunica con el dispositivo OIS (el programa de la Parte 3), y luego en la PC convierte estos datos en datos regulares de controlador / joystick, que luego se transfieren al controlador virtual / dispositivo de joystick. Esto significa que puede permitir que su controlador use constantemente la biblioteca OIS (no se requiere ningún otro firmware), y si queremos usarlo como un controlador / joystick normal, simplemente ejecute la aplicación ois2vjoy en la PC, que realiza la conversión.

Parte 5. Finalización


Espero que alguien haya encontrado este artículo útil o interesante. ¡Gracias por leer hasta el final!

Si tiene curiosidad, ¡lo invito a participar en el desarrollo de la biblioteca ois_protocol ! ¡Creo que será genial desarrollar un protocolo único para admitir todo tipo de controladores caseros en los juegos y alentar a los juegos a admitir directamente los controladores caseros!

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


All Articles