Tutorial de SDL – Parte 19 – Gamepads e Joysticks

Continuando nossa série de artigos traduzido do site lazyfoo, veremos agora com ler os dados originados de um controle de videogame, ao invés do teclado/mouse.

Assim como no caso do mouse e teclado, o SDL possui a habilidade de ler a entrada de um joystick. Nesse artigo, iremos fazer uma seta se mover baseado nos dados de entrada de um dispositivo desse tipo.
//Analog joystick dead zone
const int JOYSTICK_DEAD_ZONE = 8000;
O jeito que o SDL usa para lidar com os dados analógicos de um joystick é converter a posição em um número entre -32768 e 32767. Isso significa que um toque de leve poderia reportar uma posição superior a 1000. Nós queremos ignorar toques de leve, assim queremos criar uma zona morta onde os dados de entrada do joystick são ignorados. Por isso definimos essa constante aqui e veremos como isso funciona mais tarde.
//Game Controller 1 handler
SDL_Joystick* gGameController = NULL;
O tipo de dado para um joystick é o SDL_Joystick. Aqui declaramos o manipulador para o joystick que usaremos para interagir com ele no código.
bool init() {
   //Initialization flag
   bool success = true;

   //Initialize SDL
   if( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_JOYSTICK ) < 0 )
   {
      printf( "SDL could not initialize! SDL Error: %s\n", SDL_GetError() );
      success = false;
   }

Isso é importante:

Até agora, só inicializamos vídeo para renderiza-lo na tela. Agora precisamos inicializar um subsistema para interagir com o joystick ou ler os dados vindo desse dispositivo não funcionará.

//Set texture filtering to linear
if( !SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY, "1" ) )
{
   printf( "Warning: Linear texture filtering not enabled!" );
}
//Check for joysticks
if( SDL_NumJoysticks() < 1 )
{
   printf( "Warning: No joysticks connected!\n" );
}
else
{
   //Load joystick
   gGameController = SDL_JoystickOpen( 0 );
   if( gGameController == NULL )
   {
      printf( "Warning: Unable to open game controller! SDL Error: %s\n", SDL_GetError() );
   }
}
Após a inicialização desse subsistema, queremos abrir o joystick para uso. Primeiro, chamamos SDL_NumJoystick para verificar se temos pelo menos um joystick conectado. Se existir, chamamos SDL_JoystickOpen para abrir o joystick com indíce 0. Depois que o joystick é aberto, ele irá reportar eventos para a fila de eventos do SDL.
void close()
{
   //Free loaded images
   gArrowTexture.free();
   //Close game controller
   SDL_JoystickClose( gGameController );
   gGameController = NULL;
   //Destroy window
   SDL_DestroyRenderer( gRenderer );
   SDL_DestroyWindow( gWindow );
   gWindow = NULL;
   gRenderer = NULL;
   //Quit SDL subsystems
   IMG_Quit();
   SDL_Quit();
}
Quando tivermos finalizado o uso do joystick, precisamos fecha-lo com a função SDL_JoystickClose.
//Main loop flag
bool quit = false;
//Event handler
SDL_Event e;
//Normalized direction
int xDir = 0;
int yDir = 0;
No exemplo desse artigo, queremos rastrear a posição xy do joystick. Se x for qual a -1, a posição x do joystick aponta para a esquerda. Se for +1, a posição x aponta para a direita. A posição y para o joystick é positiva quando aponta para cima e negativa quando aponta para baixo, de forma que y=+1 significa para cima e y=-1 para baixo. Se x ou y forem 0, significa que estamos da zona morta e no centro.
//Handle events on queue
while( SDL_PollEvent( &e ) != 0 )
{
   //User requests quit
   if( e.type == SDL_QUIT )
   {
      quit = true;
   }
   else if( e.type == SDL_JOYAXISMOTION )
   {
      //Motion on controller 0
      if( e.jaxis.which == 0 )
      {
         //X axis motion
         if( e.jaxis.axis == 0 )
         {
            //Left of dead zone
            if( e.jaxis.value < -JOYSTICK_DEAD_ZONE )
            {
               xDir = -1;
            }
            //Right of dead zone
            else if( e.jaxis.value > JOYSTICK_DEAD_ZONE )
            {
               xDir = 1;
            }
            else
            {
               xDir = 0;
            }
         }

Em nosso loop de eventos, nós iremos verificar se o joystick se moveu usando a função SDL_JoyAxisEvent. A variável which informa de qual joystick o movimento veio, e aqui verificamos que o evento veio do joystick 0.

Em seguida, queremos verificar se temos movimento na direção x ou na direção y, que é indicado pela variável axis. Tipicamente axis=0 representa o eixo x.

A variável value informa qual a posição analógica nos eixos. Se a posição x for menor do que a zona morta, a direção é configurada para negativa. Se for maior, a direção é configurada para positiva. Se estiver na zona morta, a direção é configurada para 0.

         //Y axis motion
         else if( e.jaxis.axis == 1 )
         {
            //Below of dead zone
            if( e.jaxis.value < -JOYSTICK_DEAD_ZONE )
            {
               yDir = -1;
            }
            //Above of dead zone
            else if( e.jaxis.value > JOYSTICK_DEAD_ZONE )
            {
               yDir = 1;
            }
            else
            {
               yDir = 0;
            }
         }
      }
   }
}
Fazemos a mesma coisa novamente para o eixo y, que é identificado com o id 1.
//Clear screen
SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
SDL_RenderClear( gRenderer );
//Calculate angle double joystickAngle = atan2( (double)yDir, (double)xDir ) * ( 180.0 / M_PI );
//Correct angle
if( xDir == 0 && yDir == 0 )
{
   joystickAngle = 0;
}

Antes de renderizarmos a seta que irá apontar para a direção que o direcional analógico do joystick foi pressionado, precisamos calcular o ângulo. Fazemos isso usando a função atan2 do cmath, que representa o arco tangente 2, ou tangente inversa 2.

Para quem está familiarizado com trigonometria, essa é basicamente a função tangente inversa com algum código adicional que leva em consideração em qual quadrante os valores estão.

Para quem está familiarizado apenas com geometria, apenas saiba que você fornece a posição yx e irá obter o ângulo em radianos. O SDL necessita do ângulo de rotação em graus,  então nós temos que converter de radianos para graus multiplicando por 180 dividido por Pi.

Quando tanto a posição x quanto y são 0, poderiamos obter lixo como ângulo, então precisamos corrigir o valor para 0.

//Render joystick 8 way angle
gArrowTexture.render( ( SCREEN_WIDTH - gArrowTexture.getWidth() ) / 2, ( SCREEN_HEIGHT - gArrowTexture.getHeight() ) / 2, NULL, joystickAngle );
//Update screen
SDL_RenderPresent( gRenderer );

Finalmente renderizamos a seta na tela.

Existem outros eventos para joysticks como button presses, pov hats, and pluggin in or removing a controller. Eles são bastantes simples e você deve ser capaz de utiliza-los somente dando uma olhada rápida na documentação e experimentando-os.

Baixe os arquivo de código de fontes para esse tutorial aqui.