Uso del joystick en una aplicación MFC.

 

 

Por: Demian Panello.

 

¿Hay algún joystick por ahí?.

 

El uso del joystick estándar, (4 botones), es bastante sencillo en C++, toda la funcionalidad del mismo se encuentra en la librería winmm.dll.

Allí encontrará funciones que permiten saber, por ejemplo, la cantidad de joystick soportados, la configuración de los mismos, si se ha pulsado un botón, (o dos en simultáneo), etc.

Una aplicación sencilla que maneje el joystick debe:

 

1) Averiguar si hay algún joystick instalado en el sistema: Esto se logra con la función joyGetNumDevs() que devuelve la cantidad de joystick instalados, 0 en caso de que no haya ninguno.

 

2) Capturar un joystick, (JOYSTICKID1 ó JOYSTICKID2) por medio de la función joySetCapture(HWND hwnd, UINT uJoyID, UINT uPeriod, BOOL fChanged) cuyos parámetros son: el handle de la ventana adonde se enviarán los mensajes del joystick, el joystick a capturar, (JOYSTICKID1 ó JOYSTICKID2), el tiempo en milésimas de  segundos entre cada mensaje y el último parámetro es TRUE en caso de dispararse los mensaje cuando el valor de la posición cambia por uno mayor, sino se procesan los mensaje según el parámetro uPeriod.

Esta función es importante para entender como usar el joystick en un programa. Luego de averiguado si existe algún joystick y en caso de obtener una respuesta positiva, (hay por lo menos uno), se debe capturar el dispositivo. Se entiende por capturar, el hecho de indicarle al sistema cuál será la ventana que manipulará los mensajes del joystick, (mover y botones de disparo). La función retorna además un entero largo, (MMRESULT en realidad) que pueden ser las siguientes constantes:

JOYERR_NOERROR: En caso que todo esté bien, (se pudo capturar el joystick).

MMSYSERR_NODRIVER: No está el driver del joystick.

JOYERR_NOCANDO: No se pudo capturar el joystick porque algún servicio del sistema no está disponible.

JOYERR_UNPLUGGED: Hay que conectar el joystick muchachos!. : )

 

3) Luego de pasar bien los puntos 1) y 2) hay que, en primer lugar, asignarle a la ventana los mensajes del joystick que vamos a utilizar, los cuales pueden ser:

MM_JOY1BUTTONDOWN: Se ha pulsado un botón.

MM_JOY1BUTTONUP: Se ha soltado un botón.

MM_JOY1MOVE: Ha cambiado el valor de las coordenadas x o y del joystick.

MM_JOY1ZMOVE: Ha cambiado el valor de la coordenada z del joystick.

 

Además están los mensajes para el joystick JOYSTICKID2, en caso de haber dos joystick instalados.

 

4) Escribir el código de los mensajes.

 

Esta es la receta básica para escribir una aplicación que use el joystick, pero antes de pasar al ejemplo, comprendamos como funciona el movimiento de la "palanca" del joystick, (o "cruz" en caso de tratarse de un gamepad).

 

¡Socorro, tiembla el joystick!.

 

El movimiento de un joystick está expresado en coordenadas sobre los ejes x, y, z. Descartemos, por ahora, el eje z.

El eje x toma valores entre 0 y 65535, (lo mismo el eje y). Cuando la palanca está en reposo, el valor del eje x es exactamente la mitad de 65535, (lo mismo ocurre con y).

Cuando se inclina la palanca, por ejemplo, hacia la derecha, x toma el valor 65535, (o uno aproximado, si no está perfectamente calibrado, lo que es normal) y cuando se la inclina a la izquierda toma el valor 0.

Con y pasa exactamente lo mismo, salvo que se alteran los valores al mover la palanca hacia adelante, (y=0) y hacia atrás, (y=65535).

Entonces si uno quiere averiguar hacia adónde tiene que mover un objeto, (una imagen por ejemplo), en respuesta a una inclinación de la palanca deberá verificar el valor de estas coordenadas, las cuales son argumentos del mensaje MM_JOY1MOVE.

Bien, pero chequear si es 0 o 65535 aveces es incómodo, puede no estar muy bien calibrado el joystick, entonces existe una pequeña "vibración", (u oscilación), y jamás toma un valor preciso de los extremos ni en reposo, entonces se suele acotar el rango de los valores de manera tal que sea más cómoda la lectura. Sería más sencillo si tanto x como y tomaran valores por ejemplo entre 0 y 31, donde 15 sería el reposo ó entre 0 y 15 donde 7 sería el reposo. Para lograr acotar el rango, a partir de los valores originales trate de comprender lo siguiente:

Un número como 65535 es un entero sin signo, o sea dos bytes, donde los dígitos, (binarios, se entiende), más significativos, ( los de mayor "peso", los más importantes, a la hora de construir el "dato"), se encuentran a la izquierda. Si de los valores de x e y, de alguna forma, tomo por ejemplo, los primeros 5 dígitos binarios, obtendría un nuevo número entre 0 y 31, (2 elevado a la 5 es 32, entre 0 y 31 hay 32 números),  que me anula la posibilidad de error de lectura ya que en este rango no se presentan oscilaciones, (salvo que el joystick esté ¡horriblemente calibrado! o es una porquería, en cuyo caso aconsejo calibrarlo o tirarlo). Para esto hay que desplazar los bits de los valores de x e y hacia la derecha 11 posiciones, (recuerde que dos bytes son 16 bits, el primero en posición 0 y el último en 15).

 

Si no le quedó muy claro esto último, (cosa que es probable y normal, ...la primera vez que leí  o me hablaron sobre desplazamiento de bits en programación, dije...¿qué?, ¿a quién desplazaron?), no se preocupe, el ejemplo quizá aclare un poco las cosas, (eso espero).

 

Aplicación MFC que usa el joystick.

                                                                                       

Como ejemplo haremos un programita con un diálogo por el cual podremos mover con el joystick un icono, (el icono de Yerba Mate y Visual C++ Yerba Mate y Visual C++ ), e ir pulsando los botones para escuchar diferentes sonidos.

 

Lo primero que debe hacer luego de creada la aplicación Dialog Based con el AppWizard, (en el ejemplo el nombre que le puse es joy1), es agregar al proyecto la librería winmm.lib. Vaya entonces a Project -> Add to project -> Files y seleccione winmm.lib, (en el fuente que puede descargar de este ejemplo la puede encontrar).

Luego en el archivo de implementación de la clase del diálogo, (joy1Dlg.cpp), agregue el archivo de cabecera mmsystem.h.

#include "mmsystem.h"

 

Cree un icono con el editor de recursos o importe uno y colóquele el ID: IDI_ICON1, (para averiguar como hacer esto, puede leer el capítulo VIII de Aplicaciones usando MFC).

Coloque un control picture en el diálogo y como propiedades especifique:

ID: IDC_OBJETO.

Type: ICON.

Image: IDI_ICON1.

Visible: con tilde.

 

Agregaremos además un objeto CPoint a la clase del diálogo para poder almacenar la posición del icono. Con el derecho del mouse pulse sobre la clase del diálogo, (Cjoy1Dlg), y seleccione Add Member Variable. En Variable Type escriba CPoint y en Variable Name: pt. (Si lo desea puede inicializar los campos x e y de pt en el constructor del diálogo, pero no hace falta en este ejemplo).

 

Siguiendo la receta de los puntos 1, 2, 3 y 4, deberíamos empezar por el 1) y el 2): averiguar si hay algún joystick instalado y capturarlo.

El mejor lugar para hacer esto es cuando se inicia la aplicación, o sea en OnInitDialog(). Allí escriba, (a continuación de lo que ya viene escrito antes de return TRUE).

 

......

CRect rect;
m_Objeto.GetClientRect (&rect);
pt=rect.CenterPoint ();

if (!joyGetNumDevs())   (1)
    {
     MessageBox("No ningún joystick instalado", "Sin joystick", MB_ICONERROR);
     EndDialog(IDCANCEL);
    }
else
  if (joySetCapture(this->m_hWnd, JOYSTICKID1, NULL, FALSE) != JOYERR_NOERROR) (2)
      {
        MessageBox("No se ha podido capturar el joystick", "Error al capturar", MB_ICONERROR);
        EndDialog(IDCANCEL);
      }
.....

 

En las primeras tres líneas se inicializa la variable pt con los valores del punto central del icono.

En (1) se averigua si la cantidad de joysticks es 0, de ser así se muestra un mensaje de error y se termina la aplicación. En caso contrario, hay algún joystick instalado, entonces en (2) se procede a capturar uno, (JOYSTICKID1), por medio de la función joySetCapture(), indicándole por medio de m_hWnd que el diálogo recibirá los mensajes del dispositivo y que no será el envío de mensaje por medio de un timer, especificando NULL y FALSE los siguientes parámetros. Si esta función no devuelve JOYERR_NOERROR es porque existió algún problema, por lo tanto, mostramos un mensaje y terminamos.

Puede probar la aplicación, no va a ocurrir nada, sólo se va a enterar si puede o no usar un joystick.

 

Agregando mensajes sin el Class Wizard.

 

Bueno, ahora pasamos al punto 3) de la receta, el cual dice que debemos asignarle al diálogo los mensaje que queremos procesar del joystick. Nosotros capturamos el joystick JOYSTICKID1 y queremos mover un objeto y pulsar botones, entonces necesitamos los mensajes: MM_JOY1MOVE y MM_JOY1BUTTONDOWN. Pensará usar el Class Wizard, como lo hace siempre, para por ejemplo agregar el mensaje WM_LBUTTONDOWN que permite procesar los click en el diálogo. Si va al Class Wizard se encontrará con la novedad que estos mensajes del joystick no están..., ¿y ahora, qué hacemos?.

El Class Wizard ofrece una importante cantidad de mensajes típicos para agregar a un diálogo, pero no todos. Tendremos, entonces, que agregarlos "a mano".

Cuando a través del Class Wizard agregamos un mensaje se insertan varias líneas en nuestros archivos de código, ahora, para realizar lo mismo con estos mensajes del joystick deberemos localizar una sección del código del archivo de implementación de nuestro diálogo, (joy1Dlg.cpp), que se encuentra encerrada entre las declaraciones BEGIN_MESSAGE_MAP(CJou1Dlg, CDialog) y END_MESSAGE_MAP(). Allí debe escribir lo siguiente, (líneas resaltadas en amarillo):

 

BEGIN_MESSAGE_MAP(CJoy1Dlg, CDialog)
    //{{AFX_MSG_MAP(CJoy1Dlg)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_LBUTTONDOWN()

    //}}AFX_MSG_MAP
    ON_MESSAGE(MM_JOY1BUTTONDOWN, OnJoy1ButtonDown)
    ON_MESSAGE(MM_JOY1MOVE, OnJoy1Move)

END_MESSAGE_MAP()

 

Aquí se está haciendo uso de la macro ON_MESSAGE() la cual permite relacionar alguna función de nuestro programa, (en el ejemplo sería OnJoy1ButtonDown, que aún no escribimos), con un mensaje.

Ambos mensajes reciben dos parámetros, uno de tipo WPARAM y otro LPARAM, parámetros típicos en los mensajes, los cuales llevan la información de, por ejemplo, que botón se pulsó, coordenadas, combinaciones de pulsaciones, etc.

Entonces deberemos agregar las funciones miembro OnJoy1ButtonDown() y OnJoy1Move() a nuestro diálogo, (pueden tener cualquier nombre estas funciones).

Pulse con el derecho sobre la clase del diálogo en el Class View y seleccione Add Member Function y como Function Type escriba LRESULT y en Function Declaration: OnJoy1ButtonDown(WPARAM wParam, LPARAM lParam).

Esto creará el "cuerpo" de nuestra función que manipulará las pulsaciones de los botones del joystick.

Escriba:

 

LRESULT CJoy1Dlg::OnJoy1ButtonDown(WPARAM wParam, LPARAM lParam)
{
    if (wParam & JOY_BUTTON1)
        sndPlaySound("jump.wav", SND_ASYNC);
   else if (wParam & JOY_BUTTON2)
        sndPlaySound("bang.wav", SND_ASYNC);
   else if (wParam & JOY_BUTTON3)
        sndPlaySound("bounce.wav", SND_ASYNC);
    else if (wParam & JOY_BUTTON4)
        sndPlaySound("piehit2.wav", SND_ASYNC);
    return 0;
}

 

Aquí simplemente se verifica el estado de algunos bits el valor del parámetro wParam realizando un &, (AND a nivel de bits) con constantes, que pueden ser:

 

JOY_BUTTON1CHG: el primer botón ha cambiado de estado.

JOY_BUTTON2CHG: el segundo botón ha cambiado de estado.

JOY_BUTTON3CHG: el tercer botón ha cambiado de estado.

JOY_BUTTON4CHG: el cuarto botón ha cambiado de estado.

 

y también:

 

JOY_BUTTON1: el primer botón ha sido pulsado.

JOY_BUTTON2: el segundo botón ha sido pulsado.

JOY_BUTTON3: el tercero botón ha sido pulsado.

JOY_BUTTON4: el cuarto botón ha sido pulsado.

 

De acuerdo al botón pulsado, se reproduce un archivo de audio .WAV por medio de la función sndPlaySound() cuyos parámetros son el nombre del archivo, (path incluido si no está en el mismo directorio que el proyecto o el ejecutable) y el modo de reproducción, en este caso SND_ASYNC significa reproducción sincrónica, lo que permite realizar otras tareas mientras se reproduce el sonido.

El parámetro lParam almacena en el byte de orden bajo la coordenada x y en el byte de orden alto la coordenada y del joystick. En este momento esto no nos interesa. Continúe leyendo.

 

Nos falta agregar la función que representará el mensaje MM_JOY1MOVE en nuestra aplicación. Realice el mismo procedimiento que lo hecho para OnJoy1ButtonDown, (el mismo retorno y los mismos parámetros), con el nombre de función OnJoy1Move(WPARAM wParam, LPARAM lParam). Y escriba:

 

LRESULT CJoy1Dlg::OnJoy1Move(WPARAM wParam, LPARAM lParam)
{
int x, y, xo, yo;
CString pos;
CRect dlgRect;
   
//Obtengo los valores del rectángulo donde se encuentra enmarcado el icono.
//Para luego poder desplazarlo.

GetClientRect(&dlgRect);
   
//tomo los valores originales para demostrar como oscilan
//esto lo puede borrar, no hace al funcionamiento de la rutina.

xo = LOWORD(lParam);
yo = HIWORD(lParam);
   
/* Las posiciones del joystick toman valores entre 0 y 65535. Se toman
  los 5 bits más significantes del valor para que el mismo quede
  expresado en un rango entre 0 y 31.
  Vuelvo a tomar los valores pero ahora desplazados 11 bits a la derecha.
*/


x = LOWORD(lParam) >> 11;
y = HIWORD(lParam) >> 11;

/*comparo los valores para saber si debo mover el icono hacia la derecha,
   o hacia la izquierda, o hacia arriba o hacia abajo
*/

if (x == 0)
    pt.x -= 10;
else if (x == 31)
    pt.x += 10 ;

if (y ==0)
    pt.y -= 10;
else if (y == 31)
    pt.y += 10;

//Muestro en la barra del diálogo un texto con las coordenadas originales del joystick
//y las expresadas en el nuevo rango.

pos.Empty ();
pos.Format ("Valores originales: (%i, %i) // Valores entre (0 y 31): (%i, %i)", xo, yo, x, y);
SetWindowText(pos);
   
//si el icono se va por los laterales aparece nuevamente por el opuesto
if (pt.x < dlgRect.left)
        pt.x=dlgRect.right;
if (pt.x > dlgRect.right)
        pt.x=dlgRect.left;
if (pt.y < dlgRect.top)
        pt.y=dlgRect.bottom;
if (pt.y > dlgRect.bottom)
        pt.y=dlgRect.top;

//muevo el icono
m_Objeto.MoveWindow (pt.x ,pt.y ,32,32, TRUE);
   
return 0;
}

 

Ahora sí nos interesa el parámetro lParam, pues de acuerdo a los valores de x e y del joystick desplazaremos el icono.

lParam es de tipo DWORD, o sea 2 WORD, (2 palabras), en la palabra de orden bajo, (la de la derecha), trae el valor de x y la de orden alto, (la de la izquierda), el de y. Para poder "sacarlos" usamos las macros LOWORD y HIWORD. Estas macros reciben un valor de tipo DWORD, (palabra doble, 4 bytes), y devuelven el contenido almacenado en cada palabra, LOWORD en la palabra de orden bajo y HIWORD en la palabra de orden alto.

En este ejemplo se aplica el desplazamiento de bits en cada palabra para obtener el rango de las coordenadas entre 0 y 31.

Y luego se verifican los valores. Por ejemplo: si x = 0 entonces se está inclinando la palanca hacia la izquierda, se restan 10 pixeles a la posición x actual del icono, (objeto pt), si por el contrario x = 31 se inclinó la palanca hacia la derecha, entonces se suman 10 pixeles a la posición x del icono, etc.

 

Hemos llegado al final de este artículo, introductorio, sobre el manejo del joystick en C++. En el fuente encontrarán la librería winmm.lib y los archivos de audio usados..

 

Feliz Navidad y Año Nuevo para todos!.

 

Descargar fuente del ejemplo: joy1.zip (70 Kb).

Volver a la página principal