[HANDMADE GAMEDEV]

Input de joystick com XInput

Uma das premissas que adotei para o desenvolvimento da minha game engine é usar ao máximo os recursos nativos de cada sistema e evitar dependências tanto para o programador, quanto para o jogador. E foi pesquisando soluções nativas para implementar suporte a joystick que encontrei o XInput, que veio substituir o antigo DirectInput . Neste post, vou mostrar como utilizei o XINPUT para dar suporte a joystick na minha engine e mais importante, sem forçar uma dependência em tempo de execução.

Múltiplas versões

Existem 3 versões do XINPUT, cada uma distribuída em uma versão diferente do windows.

XInput 1.4:  Nativa no Windows 8 e obrigatória para Windows Store apps ou desktop apps para Windows8.
XInput 9.1.0: XInput 9.1.0 nativa no Windows Vista, Windows 7, e Windows 8.
XInput 1.3: XInput 1.3 é parte do DirectX SDK com suporte pra Windows Vista, Windows 7, e Windows 8.

Decidindo que versão usar

Recapitulando, temos 3 versões possíveis – todas elas nativas para alguma versão do Windows. Escolher uma, entretanto, significa automaticamente não suportar as demais. Caso o jogo rode em um ambiente sem a versão escolhida, o jogador deveria instalar a versão correta do DirectX. Evidentemente, este cenário viola completamente a premissa que comentei há pouco.

Por isso, eu decidi não “linkar” estaticamente com nenhuma das versões e simplesmente tentar carregar – em tempo de execução – a dll do XInput que estiver disponível no sistema. Assim, desde que a engine exponha apenas funcionalidades básicas, comuns às 3 versões, é possível suportar uma quantidade maior de sistemas operacionais.

A inicialização do XInput ficou assim:

void Win32_initXInput()
{
  char* xInputDllName = "xinput1_1.dll"; 
  HMODULE hXInput = LoadLibraryA(xInputDllName);
  if (!hXInput)
  {				
    xInputDllName = "xinput9_1_0.dll";
    hXInput = LoadLibraryA(xInputDllName);
  }

  if (!hXInput)
  {
    xInputDllName = "xinput1_3.dll";
    hXInput = LoadLibraryA(xInputDllName);
  }

  if (!hXInput)
  {
    LogError("could not initialize xinput. No valid xinput dll found");
    XInputGetState = XInputGetStateDummy;
    XInputSetState = XInputGetStateDummy;		
    return;
  }

  LogInfo("Initializing xinput %s", xInputDllName);

  //get xinput function pointers
  XInputGetState = (XInputGetStateFunc*) 
    GetProcAddress(hXInput, "XInputGetState");
  XInputSetState = (XInputSetStateFunc*)
    GetProcAddress(hXInput, "XInputSetState");

  if (!XInputGetState) XInputGetState = XInputGetStateDummy;
  if (!XInputSetState) XInputSetState = XInputGetStateDummy;
}

A função tenta carregar, da mais antiga para a mais recente, qualquer dll do XInput que esteja disponível no sistema. Uma vez carregada a dll, obtenho manualmente os ponteiros para as funções que preciso chamar. Especificamente XInputGetState e XinputSetState que me permitem interações básicas como vibrar e ler o estado dos botões.

API de input da engine

Minha proposta para a API de input da engine segue a filosofia da Unity3D, onde se pode checar não só se um botão está pressionado ou não, mas também se foi pressionado ou liberado durante o frame atual.

Os eixos analógicos são representados com um float de 32 bits que vai de -1 até 1. O estado dos botões é representado por um único byte, onde o bit 0 indica o estado do botão e o bit 1, indica se este botão mudou de estado no frame atual.

Abaixo, a interface da API de input de joystick.

#define KEYSTATE_PRESSED  0x01	
#define KEYSTATE_CHANGED  0x02

typedef int8 KeyState;

inline int8 getButton(uint16 key, uint16 index = 0) const;
inline int8 getButtonDown(uint16 key, uint16 index = 0) const;
inline int8 getButtonUp(uint16 key, uint16 index = 0) const;
inline float getAxis(uint16 axis, uint16 index = 0) const;

//digital buttons
#define GAMEPAD_DPAD_UP	0x00
#define GAMEPAD_DPAD_DOWN	0x01
#define GAMEPAD_DPAD_LEFT	0x02
#define GAMEPAD_DPAD_RIGHT	0x03
#define GAMEPAD_START	0x04
#define GAMEPAD_FN1	0x04
#define GAMEPAD_BACK	0x05
#define GAMEPAD_FN2	0x05
#define GAMEPAD_LEFT_THUMB	0x06
#define GAMEPAD_RIGHT_THUMB	0x07
#define GAMEPAD_LEFT_SHOULDER	0x08
#define GAMEPAD_RIGHT_SHOULDER	0x09
#define GAMEPAD_A	0x0A
#define GAMEPAD_BTN1	0x0A
#define GAMEPAD_B	0x0B
#define GAMEPAD_BTN2	0x0B
#define GAMEPAD_X	0x0C
#define GAMEPAD_BTN3	0x0C
#define GAMEPAD_Y	0x0D
#define GAMEPAD_BTN4	0x0D

// axis
#define GAMEPAD_AXIS_LX 0x00
#define GAMEPAD_AXIS_LY 0x01
#define GAMEPAD_AXIS_RX 0x02
#define GAMEPAD_AXIS_RY 0x03
#define GAMEPAD_AXIS_LTRIGGER 0x04
#define GAMEPAD_AXIS_RTRIGGER 0x05

Uma vez estabelecida a interface, implementa-la com XINPUT foi mais simples do que imaginei. O XINPUT é bastante enxuto, em contraste com a maioria das outras APIs da Microsoft.

Fazendo polling dos joysticks

Obter o estado do joystick a cada frame é tão simples quanto fazer uma chamada a XInputGetState de onde se pode obter uma estrutura XINPUT_GAMEPAD.

A função retorna ERROR_DEVICE_NOT_CONNECTED para joysticks desconectados.

static inline void Win32_processGamepadInput(ldare::Input& gameInput)
{
  // clear 'changed' bit from input key state
  for(int gamepadIndex=0; gamepadIndex < MAX_GAMEPADS ; gamepadIndex++)
  {
    for(int i=0; i < GAMEPAD_MAX_DIGITAL_BUTTONS ; i++)
    {
      gameInput.gamepad[gamepadIndex].button[i] &= ~KEYSTATE_CHANGED;
    }
  }

  // get gamepad input
  for(int16 gamepadIndex = 0; gamepadIndex < MAX_GAMEPADS; gamepadIndex++)
  {
    ldare::platform::XINPUT_STATE gamepadState;
    Gamepad& gamepad = gameInput.gamepad[gamepadIndex];

    // ignore unconnected controllers
    if ( platform::XInputGetState(gamepadIndex, &gamepadState) == ERROR_DEVICE_NOT_CONNECTED )
    {
      if ( gamepad.connected)
      {
        gamepad = {};					
      }
      gamepad.connected = 0;
      continue;
    }
    // Get digital buttons here
    // Get axis here
    // Get Triggers here
  }
  ...
}

Botões digitais

A estrutura  XINPUT_GAMEPAD contém um array com o estado de cada botão digital do joystick, bastando checar o índice dobotão correspondente. Valores > 0 indicam que o botão está pressionado.

// Get digital buttons here
  WORD buttons = gamepadState.Gamepad.wButtons;
  uint8 isDown=0;
  uint8 wasDown=0;

#define GET_GAMEPAD_BUTTON(btn) do {\
  isDown = (buttons & XINPUT_##btn) > 0;\
  wasDown = gamepad.button[btn] & KEYSTATE_PRESSED;\
  gamepad.button[btn] = ((isDown != wasDown) << 0x01) | isDown;\
} while(0)

  GET_GAMEPAD_BUTTON(GAMEPAD_DPAD_UP);			
  GET_GAMEPAD_BUTTON(GAMEPAD_DPAD_DOWN);
  GET_GAMEPAD_BUTTON(GAMEPAD_DPAD_LEFT);
  GET_GAMEPAD_BUTTON(GAMEPAD_DPAD_RIGHT);
  GET_GAMEPAD_BUTTON(GAMEPAD_START);
  GET_GAMEPAD_BUTTON(GAMEPAD_BACK);
  GET_GAMEPAD_BUTTON(GAMEPAD_LEFT_THUMB);
  GET_GAMEPAD_BUTTON(GAMEPAD_RIGHT_THUMB);
  GET_GAMEPAD_BUTTON(GAMEPAD_LEFT_SHOULDER);
  GET_GAMEPAD_BUTTON(GAMEPAD_RIGHT_SHOULDER);
  GET_GAMEPAD_BUTTON(GAMEPAD_A);
  GET_GAMEPAD_BUTTON(GAMEPAD_B);
  GET_GAMEPAD_BUTTON(GAMEPAD_X);
  GET_GAMEPAD_BUTTON(GAMEPAD_Y);

#undef SET_GAMEPAD_BUTTON

Eixos e deadzones

De maneira similar, os eixos são lidos da mesma estrutura. Os valores vão de -32768 até 32767, onde 0 indica que o eixo está centralizado. Note que é necessário considerar uma zona morta, out dead zone, onde as leituras devem ignoradas, pois é possível ler valores diferentes de 0 mesmo sem mover os eixos em nenhuma direção.

Após obter o estado dos eixos, a função lê o estado dos triggers, tem um intervalo de 0 a 255 e também considero uma dead zone para evitar falsos positivos, como no caso dos eixos.

static inline void Win32_processGamepadInput(ldare::Input& gameInput)
{
  ...
  //TODO: Make these calculations directly in assembly to make it faster
#define GAMEPAD_AXIS_VALUE(value) (value/(float)(value < 0 ? XINPUT_MIN_AXIS_VALUE * -1: XINPUT_MAX_AXIS_VALUE))
#define GAMEPAD_AXIS_IS_DEADZONE(value, deadzone) ( value > -deadzone && value < deadzone)

  // Left thumb axis
  int32 axisX = gamepadState.Gamepad.sThumbLX;
  int32 axisY = gamepadState.Gamepad.sThumbLY;
  int32 deadZone = XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE;

  gamepad.axis[GAMEPAD_AXIS_LX] = GAMEPAD_AXIS_IS_DEADZONE(axisX, deadZone) ? 0.0f :
    GAMEPAD_AXIS_VALUE(axisX);
  gamepad.axis[GAMEPAD_AXIS_LY] = GAMEPAD_AXIS_IS_DEADZONE(axisY, deadZone) ? 0.0f :	
    GAMEPAD_AXIS_VALUE(axisY);

  // Right thumb axis
  axisX = gamepadState.Gamepad.sThumbRX;
  axisY = gamepadState.Gamepad.sThumbRY;
  deadZone = XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE;
  gamepad.axis[GAMEPAD_AXIS_RX] = GAMEPAD_AXIS_IS_DEADZONE(axisX, deadZone) ? 0.0f :
    GAMEPAD_AXIS_VALUE(axisX);

  gamepad.axis[GAMEPAD_AXIS_RY] = GAMEPAD_AXIS_IS_DEADZONE(axisY, deadZone) ? 0.0f :	
    GAMEPAD_AXIS_VALUE(axisY);

  // Left trigger
  axisX = gamepadState.Gamepad.bLeftTrigger;
  axisY = gamepadState.Gamepad.bRightTrigger;
  deadZone = XINPUT_GAMEPAD_TRIGGER_THRESHOLD;

  gamepad.axis[GAMEPAD_AXIS_LTRIGGER] = GAMEPAD_AXIS_IS_DEADZONE(axisX, deadZone) ? 0.0f :	
    axisX/(float) XINPUT_MAX_TRIGGER_VALUE;

  gamepad.axis[GAMEPAD_AXIS_RTRIGGER] = GAMEPAD_AXIS_IS_DEADZONE(axisY, deadZone) ? 0.0f :	
    axisY/(float) XINPUT_MAX_TRIGGER_VALUE;

#undef GAMEPAD_AXIS_IS_DEADZONE
#undef GAMEPAD_AXIS_VALUE
  gamepad.connected = 1;

}

Considerações finais

Ainda não tive oportunidade nem necessidade de avaliar latência ou precisão das leituras, mas é sem dúvida algo que precisarei fazer no futuro.

No mais, eu achei o XINPUT bastante enxuto e o fato de ser nativo torna ainda mais interessante. Não surpreende ser tão utilizada por inúmeros jogos pra PC atualmente. Me pergunto se terei a mesma facilidade com a API de Joystick da apple quando for implementar isso no Mac.

O código com toda a implementação pode ser acessado no repo do projeto no github.

Happy Coding.