Bienvenidos sean a este post, hoy veremos como inicializar nuestros eventos para detectar los joysticks.
Para comenzar a trabajar, vamos a necesitar el proyecto que estuvimos utilizando. Sino lo poseen les dejo un link para descargarlo:
Descarguen el archivo y extraigan el contenido en el PC. Lo unico que deben hacer es revincular a SDL y SDL_Image. Para ello, les recomiendo este post donde comento como hacerlo para SDL y este otro post donde hago lo mismo para SDL_Image. Una vez que este todo funcionando correctamente, podemos continuar. Nuestro primer paso sera crear una nueva clase con el nombre de ManejarEntradas. Una vez creado, iremos a ManejarEntradas.h y le agregaremos el siguiente codigo:
ManejarEntradas.h
#pragma once
#ifndef __ManejarEntradas__
#define __ManejarEntradas__
#include <SDL3/SDL.h>
#include <SDL3/SDL_joystick.h>
#include <vector>
#include "Juego.h"
class ManejarEntradas
{
public:
static ManejarEntradas* instanciar();
void actualizar();
void limpiar();
void iniciarJoysticks();
bool joysticksIniciados();
private:
ManejarEntradas();
~ManejarEntradas();
static ManejarEntradas* e_pInstancia;
std::vector<SDL_Joystick*> m_joysticks;
bool m_bJoyIniciados;
int total_joy;
SDL_JoystickID* joysticks;
};
#endif // !__ManejarEntradas__
typedef ManejarEntradas Controles;
El condicional es para asegurarnos que solo exista una instancia de esta clase. Luego incluiremos todas las librerias necesarias, la unica nueva es SDL_Joystick que nos permitira acceso a mas utilidades para los joysticks/gamepads. La otra que tenemos es vector pero es para poder crear objetos de este tipo para almacenar todos los joysticks conectados. En la parte privada, tenemos al constructor y destructor y otras variables que usaremos. Tenemos la variable estatica e_pInstancia que usaremos para almacenar nuestro singleton. Asi como tambien el vector para los joysticks, m_bJoyiniciados para indicar si estan iniciados y total_joy para almacenar la cantidad de joysticks. En la parte publica, tenemos los prototipos para las distintas funciones. Por ultimo, tenemos un nuevo alias para esta clase. Pasemos a ManejarEntradas.cpp y agreguemos el siguiente codigo:
ManejarEntradas.cpp
#include "ManejarEntradas.h"
#include <Windows.h>
ManejarEntradas* ManejarEntradas::e_pInstancia = 0;
ManejarEntradas* ManejarEntradas::instanciar() {
if (e_pInstancia == 0)
e_pInstancia = new ManejarEntradas();
return e_pInstancia;
}
void ManejarEntradas::iniciarJoysticks() {
if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
SDL_InitSubSystem(SDL_INIT_JOYSTICK);
joysticks = SDL_GetJoysticks(&total_joy);
if (total_joy > 0) {
for (int i = 0; i < total_joy; i++) {
SDL_Joystick* joy = SDL_OpenJoystick(joysticks[i]);
if (joy) {
m_joysticks.push_back(joy);
}
else {
OutputDebugStringA(SDL_GetError());
}
}
SDL_SetJoystickEventsEnabled(true);
m_bJoyIniciados = true;
char bufmsj[200] = "";
sprintf_s(bufmsj, "Joysticks Iniciados: %d\n", m_joysticks.size());
OutputDebugStringA(bufmsj);
}
else {
m_bJoyIniciados = false;
}
}
bool ManejarEntradas::joysticksIniciados() {
return m_bJoyIniciados;
}
void ManejarEntradas::limpiar() {
if (m_bJoyIniciados) {
for (unsigned int i = 0; i < total_joy; i++) {
SDL_CloseJoystick(m_joysticks[i]);
}
}
}
void ManejarEntradas::actualizar() {
SDL_Event evento;
while (SDL_PollEvent(&evento)) {
if (evento.type == SDL_EVENT_QUIT)
Eljuego::instanciar()->limpiar();
}
}
ManejarEntradas::ManejarEntradas() {}
ManejarEntradas::~ManejarEntradas() {}
Lo primero que haremos en este codigo es establecer un valor inicial para nuestra variable estatica. Luego tenemos la definicion de la funcion que se encarga del singleton. Repetimos el mismo proceso que utilizamos en otros casos, donde verificamos si el puntero de la instancia es igual a cero. En caso de ser verdadero, creamos una instancia para este y lo devolvemos. Para la proxima, al estar creado siempre devolveremos la misma instancia u objeto. Pasemos a analizar la siguiente funcion que se encargara de iniciar a los joysticks, y lo primero sera el siguiente condicional:
if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
SDL_InitSubSystem(SDL_INIT_JOYSTICK);
En este condicional verificamos si el subsistema de Joystick fue iniciado en caso de no ser asi, es decir que es igual a 0, procede a iniciarlo. Luego tenemos un llamado a SDL_GetJoysticks para almacenar en total_joy la cantidad de joysticks conectados. El siguiente condicional verifica si tenes alguno o algunos joysticks conectados, en todo caso si es mayor a 0 procedera a ejecutar este bucle:
for (int i = 0; i < total_joy; i++) {
SDL_Joystick* joy = SDL_OpenJoystick(joysticks[i]);
if (joy) {
m_joysticks.push_back(joy);
}
else {
OutputDebugStringA(SDL_GetError());
}
}
En este bucle pasara por todos los joysticks que tenga conectado, lo primero que hara sera crear un objeto de tipo SDL_Joystick llamado joy donde le pasaremos el dato del joystick que abrimos en base al indice del bucle. El siguiente condicional verifica si existe y en caso de ser cierto procede a agregar dentro de m_joysticks de lo contrario mostrara un mensaje de error. La siguiente linea se encarga de establecer el evento de joystick como activado, para luego establecer a m_bJoysticksIniciados como verdadero para indicar que se inicio por lo menos un Joystick, analicemos las siguientes tres lineas:
char bufmsj[200] = "";
sprintf_s(bufmsj, "Joysticks Iniciados: %d\n", m_joysticks.size());
OutputDebugStringA(bufmsj);
Con estas tres lineas mostraremos en la pantalla de salida la cantidad que tenemos conectadas, la primera sera una variable de tipo char para almacenar un texto a la cual llamaremos bufmsj. La siguiente linea sera la funcion sprintf_s la cual nos permitira componer una cadena (string) donde primero informamos el «contenedor» o la variable donde almacenaremos la cadena. Luego el mensaje con los formateadores, trabaja igual que printf, y por ultimo pasamos la informacion a asignar en el texto, en este caso el total de nuestro array m_Joysticks. Una vez hecho, enviaremos el resultado a OutputDebugStringA, la cual se encarga de mostrar este texto en la salida del depurador pero porque tanto lio para mostrar este dato? Esto es sencillo, dado que no estamos usando una aplicacion de Consola no tenemos una forma evidente de mostrar si realmente esta funcionando o no. Para ello, debemos utilizar la ventana de salida de nuestro depurador y el unico inconveniente que tenemos es que OutputDebugStringA solo acepta texto y no muestra valores de variables. Entonces por esta razon, debemos hacer esa «conversion» previa para poder mostrar el valor de alguna forma. Con esto explicado continuemos con el archivo y pasaremos a nuestra siguiente modificacion sera la definicion de la funcion limpiar para lo cual agregaremos el siguiente bloque:
void ManejarEntradas::limpiar() {
if (m_bJoyIniciados) {
for (unsigned int i = 0; i < total_joy; i++) {
SDL_CloseJoystick(m_joysticks[i]);
}
}
}
En este caso primero verifica que m_bJoyIniciados sea verdadero y en caso de ser cierto usamos un bucle para que pase por todos los joysticks cargados en m_Joysticks y los cierra con SDL_JoystickClose, limpiando de memoria a todos los joysticks. La siguiente modificacion sera agregar la definicion de la funcion actualizar mediante el siguiente bloque:
void ManejarEntradas::actualizar() {
SDL_Event evento;
while (SDL_PollEvent(&evento)) {
if (evento.type == SDL_EVENT_QUIT)
Eljuego::instanciar()->limpiar();
}
}
Primero crearemos un objeto de tipo SDL_Event y despues con el while chequearemos constantemente el estado de este evento. En esta version solamente le haremos chequear si se ejecuta SDL_EVENT_QUIT y si esto es verdad llama a limpiar de la clase Juego. Con esto concluimos por ahora con la clase ManejarEntradas. Nuestra siguiente modificacion sera en la clase Juego donde agregaremos la siguiente linea entre los include de Juego.h:
#include "ManejarEntradas.h"
Esto nos permitira tener acceso a la clase, la siguiente modificacion sera en la funcion iniciar dentro de Juego.cpp a la cual le agregaremos la siguiente linea antes de la devolucion del true:
Controles::instanciar()->iniciarJoysticks();
Con esto brindamos a nuestro juego la capacidad de poder iniciar joysticks. Nuestra siguiente modificacion sera la funcion manejaEventos de esta clase. Cambiemos su codigo actual al siguiente:
void Juego::manejaEventos() {
Controles::instanciar()->actualizar();
}
Como pueden ver ahora nuestro manejador de eventos sera el que creamos anteriormente, y la ultima modificacion que haremos es modificar a limpiar de la siguiente manera:
void Juego::limpiar() {
OutputDebugStringA("Limpiando Memoria\n");
SDL_DestroyWindow(m_pVentana);
SDL_DestroyRenderer(m_pRenderer);
Controles::instanciar()->limpiar();
m_bCorriendo = false;
SDL_Quit();
}
Agregamos el llamado a limpiar de la clase ManejarEntradas antes de la llamada a SDL_QUIT, el resto sigue siendo lo mismo que antes. Con todo esto realizado podemos probar nuestro juego nuevamente pero antes de compilarlo y probarlo conecten un joystick, puede ser cualquiera que detecte Windows ya que en mi caso conecte uno de PS4, compilen y prueben, si miran en el panel inferior de salida ahora debera aparecerles este mensaje

Como pueden ver ya iniciamos las capturas de todos los joysticks que haya en nuestro equipo.
Con los joysticks detectados, el siguiente paso es trabajar con los movimientos y para ello usaremos los sticks analogicos. Es recomendable averiguar cuales son los ejes que utilizan para su joystick por medio de los programas citados en este post, en el caso particular mio voy a usar un mando de PS4 en una pc con Windows 10 y tiene los siguientes valores:
- Movimiento de izquierda y derecha del stick 1 es el eje 0
- Movimiento de abajo y arriba del stick 1 es el eje 1
- Movimiento de izquierda y derecha del stick 2 es el eje 2
- Movimiento de abajo y arriba del stick 2 es el eje 3
Como podemos tener que manejar multiples controles con multiples ejes vamos a crear un array dinamico (vector) de pares de Vector2D, uno por cada stick, para esto debemos primero agregar a la clase para manipular estos datos, en el archivo de encabezado de ManejarEntradas agregaremos la siguiente linea:
#include "Vector2D.h"
Esta se agrega como siempre en la parte de los archivos a incluir, nuestro siguiente paso sera agregar la siguiente linea en la parte privada en el mismo archivo:
std::vector<std::pair<Vector2D*, Vector2D*>> m_valoresJoystick;
Con este nuevo vector creado solo nos resta definir esta nueva variable. Para definirlo, debemos modificar el bucle dentro de iniciarJoysticks en el archivo .cpp de la siguiente manera:
for (int i = 0; i < total_joy; i++) {
SDL_Joystick* joy = SDL_OpenJoystick(joysticks[i]);
if (joy) {
m_joysticks.push_back(joy);
m_valoresJoystick.push_back(std::make_pair(
new Vector2D(0, 0), new Vector2D(0, 0)));
}
else {
OutputDebugStringA(SDL_GetError());
}
}
En este caso solamente modificamos dentro del condicional cuando joy no es null, donde agregamos la linea que agregara un nuevo par por medio de make_pair donde le pasaremos dos objetos de tipo Vector2D que estan solamente iniciados con un valor sin importancia. No solamente tenemos la cantidad de joysticks sino ahora tambien podremos saber los ejes de cada uno. Nuestra siguiente modificacion sera agregar en la parte privada de ManejarEntradas.h la siguiente linea:
const int m_joystickZonaMuerta = 10000;
Esta nos sera de utilidad para cuando trabajemos con los ejes de los stick. Y sera para representar cuando estos esten en reposo. Este valor puede parecer alto pero algunos mandos pueden tener una gran sensibilidad y deberemos poner valores mas altos o que se ajusten de mejor manera a nuestro dispositivo. En este mismo archivo, pasemos a la parte publica de la clase agregaremos los siguientes dos prototipos:
int valorX(int, int);
int valorY(int, int);
Estas dos funciones seran para saber los valores de los ejes X e Y, nuestro siguiente paso sera definir las funciones en el archivo ManejarEntradas.cpp, definamos primero a valorX mediante el siguiente codigo:
int ManejarEntradas::valorX(int joy, int stick) {
if (m_valoresJoystick.size() > 0) {
if (stick == 1)
return m_valoresJoystick[joy].first->getX();
else if (stick == 2)
return m_valoresJoystick[joy].second->getX();
}
return 0;
}
En esta funcion recibe dos valores, uno para identificar el joystick (joy) y otro para identificar el mando (stick). Luego verificamos que m_valoresJoystick sea mayor a 0, es decir que haya al menos uno, despues tenemos un condicional donde verifica si stick es igual a 1, el primero y por ende devolvemos el valor de X del joystick informado del primer objeto del par. Lo siguiente es un else para el caso de que sea el stick sea el 2. Hara exactamente lo mismo pero en lugar de devolver el primer objeto devuelve el segundo objeto del par. Por ultimo, si no existe ningun Joystick devuelve un 0 y sale de la funcion, vamos a agregar la definicion de la funcion valorY:
int ManejarEntradas::valorY(int joy, int stick) {
if (m_valoresJoystick.size() > 0) {
if (stick == 1)
return m_valoresJoystick[joy].first->getY();
else if (stick == 2)
return m_valoresJoystick[joy].second->getY();
}
return 0;
}
Esta funcion es exactamente igual a la anterior pero en lugar de devolver el valor del eje X devolvera el valor del eje Y, con esto definido podemos pasar a la siguiente modificacion en el metodo actualizar de esta clase. Esta la modificaremos de la siguiente manera:
void ManejarEntradas::actualizar() {
SDL_Event evento;
while (SDL_PollEvent(&evento)) {
switch (evento.type) {
case SDL_EVENT_QUIT:
Eljuego::instanciar()->limpiar();
break;
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
int joy_usando = 0;
int cualEs = evento.jdevice.which;
for (int i = 0; i < m_joysticks.size(); i++) {
if (joysticks[i] == cualEs)
joy_usando = i;
}
switch (evento.jaxis.axis) {
case 0:
if (evento.jaxis.value > m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].first->setX(1);
else if (evento.jaxis.value < -m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].first->setX(-1);
else
m_valoresJoystick[joy_usando].first->setX(0);
break;
case 1:
if (evento.jaxis.value > m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].first->setY(1);
else if (evento.jaxis.value < -m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].first->setY(-1);
else
m_valoresJoystick[joy_usando].first->setY(0);
break;
case 2:
if (evento.jaxis.value > m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].second->setX(1);
else if (evento.jaxis.value < -m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].second->setX(-1);
else
m_valoresJoystick[joy_usando].second->setX(0);
break;
case 3:
if (evento.jaxis.value > m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].second->setY(1);
else if (evento.jaxis.value < -m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].second->setY(-1);
else
m_valoresJoystick[joy_usando].second->setY(0);
break;
}
break;
}
}
}
Lo primero que modificamos es al switch para manejar mas distintos tipos de eventos. El nuevo case es para SDL_EVENT_JOYSTICK_AXIS_MOTION y sera el encargado de actuar cuando movamos los sticks. Lo primero que haremos es definir es el mando que estamos usando. Primero definimos una variable donde la almacenareoms. Luego usamos a which para pasarnos su id pero necesitaremos de un bucle donde pasamos por todos los mandos almacenados en el array y aquel que coincida con el almacenado anteriormenete se asigna a la variable definida anteriormente. Luego tenemos otro switch donde monitoreaamos todos los ejes de cada stick. Analicemos el primer caso:
case 0:
if (evento.jaxis.value > m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].first->setX(1);
else if (evento.jaxis.value < -m_joystickZonaMuerta)
m_valoresJoystick[joy_usando].first->setX(-1);
else
m_valoresJoystick[joy_usando].first->setX(0);
break;
Primero verificamos si el eje es igual a 0, y en caso de ser cierto usaremos, aunque no sea recomendable, una serie de condicionales para verficar los posibles tres valores evento.jaxis.value. En el primer condicional verifica si es mayor a m_joystickZonaMuerta, este valor lo definimos anteriormente. Si se cumple esta condicion procede a usar setX con el valor de 1, y le pasamos a first para que sea el primer mando o el primer objeto del par. El segundo condicional verifica que evento.jaxis.value sea menor al valor negativo de m_joystickZonaMuerta. Ahora si se cumple este caso, setea el valor de X con -1, para que vaya hacia el otro lado. Y por ultimo, tenemos un else para el caso de cuando evento.jaxis.value es igual a 0, por ende setea a X con 0. Como se daran cuenta todo este condicional verifica si el primer stick o mando se movio hacia la derecha (cuando el valor es positivo) o hacia la izquierda (cuando es negativo) o cuando es de 0, indicando que esta quieto. El siguiente case es para verificar si el primer stick se movio hacia arriba o hacia abajo o esta quieto, es exactamente igual pero utilizar el setY en lugar del setX.
Los siguientes case hacen exactamente lo mismo a los dos anteriores pero enfocado en el segundo stick. Para este caso usaremos second en lugar de first, el resto es exactamente lo mismo a los dos cases anteriores. Con todo esto comentado, ya tenemos cubierto todos los posibles movimientos de ambos stick analogicos.
La siguiente modificacion sera en la clase Jugador, para ello debemos ir a Jugador.h donde agregaremos a la libreria anterior:
#include "ManejarEntradas.h"
Para poder acceder a todo lo programado anteriormente. La siguiente modificacion sera agregar a la parte privada de la clase y en ella a la siguiente linea:
void manejaEntrada();
En este caso solamente agregamos un prototipo que llamaremos manejaEntrada y la usaremos para poder usar a los joysticks. Lo siguiente sera definir el prototipo en Jugador.cpp. Para ello, agregaremos el siguiente bloque:
void Jugador::manejaEntrada()
{
if (Controles::instanciar()->joysticksIniciados()) {
if (Controles::instanciar()->valorX(0, 1) > 0 ||
Controles::instanciar()->valorX(0, 1) < 0) {
m_velocidad.setX(1 * Controles::instanciar()->
valorX(0, 1));
}
if (Controles::instanciar()->valorY(0, 1) > 0 ||
Controles::instanciar()->valorY(0, 1) < 0) {
m_velocidad.setY(1 * Controles::instanciar()->
valorY(0, 1));
}
if (Controles::instanciar()->valorX(0, 2) > 0 ||
Controles::instanciar()->valorX(0, 2) < 0) {
m_velocidad.setX(1 * Controles::instanciar()->
valorX(0, 2));
}
if (Controles::instanciar()->valorY(0, 2) > 0 ||
Controles::instanciar()->valorY(0, 2) < 0) {
m_velocidad.setY(1 * Controles::instanciar()->
valorY(0, 2));
}
}
}
Como pueden ver estara compuesto de varios condicionales, el primero verifica si hay joysticks iniciados, en caso de ser verdadero tendremos un conjunto de condicionales para verificar varios estados. Vamos a tomar el primero:
if (Controles::instanciar()->valorX(0, 1) > 0 ||
Controles::instanciar()->valorX(0, 1) < 0) {
m_velocidad.setX(1 * Controles::instanciar()->
valorX(0, 1));
}
Desde Instanciar llamaremos a valorX y le pasaremos el valor del primer joystick y el valor del primer stick o mando, en este caso verificamos primero si esto es mayor a 0 o si este es menor a 0. Eysto equivale a decir a que se esta moviendo, y en cualquiera de los dos casos estableceremos el valor de X por medio de setX al cual le pasaremos el valor obtenido por valorX y multiplicado por 1. Este condicional se repetira tres veces mas pero en el segundo caso seguiremos mirando al primer joystick y primer stick pero ahora trabajaremos sobre el eje Y. El tercer condicional volvemos a repetir el trabajo sobre el eje X, nos seguimos centrando en el primer joystick pero ahora nos centramos en el segundo stick, por eso ahora el valor sera de 2, y el ultimo condicional sigue con el primer joystick y segundo stick pero trabaja sobre el eje Y. En todos los casos siempre trabaja de la misma forma pero lo unico que variamos son los valores tanto en valorX como en valorY. Nuestra siguiente modificacion sera en la funcion actualizar de la clase Jugador, veamos su codigo actual:
void Jugador::actualizar() {
m_frameActual = int(((SDL_GetTicks() / 100) % 6));
m_aceleracion.setX(0.1f);
SDLObjetoJuego::actualizar();
}
Ahora lo modificaremos de la siguiente manera:
void Jugador::actualizar() {
m_velocidad.setX(0);
m_velocidad.setY(0);
manejaEntrada();
m_frameActual = int(((SDL_GetTicks() / 100) % 6));
SDLObjetoJuego::actualizar();
}
En esta modificacion quitamos el incremento automatico con aceleracion, establecimos a X e Y de m_velocidad con cero para que se quede quieto nuestro objeto, lo siguiente es llamar a manejaEntrada para poder manejar a nuestro jugador. El resto sigue de la misma manera que antes, con esto tenemos todo lo necesario para probarlo. Compilemos y veamos que sucede con nuestro juego ahora mediante el siguiente video
En el video podemos ver como finalmente podemos manejar un objeto por medio de los dos sticks analogicos de nuestro joystick/gamepad. En mi caso en particular use un mando de PS4 y para otros mandos deban modificar los valores de los ejes en la funcion actualizar de ManejarEntradas.
En resumen, hoy hemos agregado la primera forma de controlar a nuestro objeto y darle movimiento, desde como habilitarlo, detectarlo y ver cuantos conectados, asi como despues poder controlarlo con nuestros sticks analogicos. Espero les haya sido de utilidad y les dejo un link a GitHub donde estan los codigos creados hoy:
Iniciando los joysticks / GitHub
Les dejo algunas de mis redes sociales para seguirme o recibir una notificacion cada vez que subo un nuevo post:


Donación
Es para mantenimento del sitio, gracias!
$1.50





