Post

Arquitetura de Controle em um Jogo

O que precisamos fazer?

Queremos processar os inputs que ocorrem em um gamepad para mudarmos o comportamento de variáveis gráficas e/ou de som.

No exemplo, queremos mover a tela e modificar a tonalidade de um som (hz).

Estruturas de um Game Input (Controle)

Segue abaixo as structs que usaremos para ter controles no jogo.

Botões e seus estados

A primeira estrutura importante é o estado de cada botão digital de um controle como X, Y, A, B, RB, LB, etc.

1
2
3
4
5
6
7
8
9
struct Game_Button
{
    // indica número de transições em um frame (up-to-down, down-to-up)
    u32 transition_count; 
	
	
    // indica se o estado do gamepad é pressionado
    bool pressed;
};

Para sabermos se houve uma transição de estado de botão, precisaremos na lógica obter 2 game_inputs. Um para o novo estado e outro para o antigo estado (frame anterior).

Dessa forma, sempre que finalizar o frame, faremos um swap para identificar se o botão anterior estava pressionado e agora está solto e vice-versa.

O Controle

Abaixo segue a struct com os botões sendo que, cada botão é um Game_Button.

Nosso controle é composto de 12 botões baseado em um controle Xbox. O controle terá os botões digitais + 2 sticks para o eixo X e Y, além de informação se aceita a parte analógica e se está conectado.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct Game_Controller
{
    bool connected;
    bool analog;
    float stick_avg_x;
    float stick_avg_y;

    union
    {
        Game_Button buttons[12];

        struct
        {
            Game_Button move_up;
            Game_Button move_down;
            Game_Button move_left;
            Game_Button move_right;

            Game_Button action_up;
            Game_Button action_down;
            Game_Button action_left;
            Game_Button action_right;

            Game_Button left_shoulder;
            Game_Button right_shoulder;

            Game_Button start;
            Game_Button back;
        };
    };
};

Unions em C

Dentro do nosso controle, definimos um union. Um union nada mais é do que informar que as variáveis ali dentro, estão apontando para o mesmo endereço de memória. Ou seja, um union:

1
2
3
4
5
union
{
    int x;
    float y;
}

Quer dizer que temos tanto o x quanto o y apontando para o mesmo endereço, só que neste caso um é float-point e outro não.

O mesmo acontece no nosso exemplo de controle. Tanto o array buttons quanto a struct anônima de Game_Button estão apontando para o mesmo endereço.

Fazemos isso para que quando precisarmos de um looping para acessar todos os botões, teremos um array pronto para isso. Enquanto quando precisarmos acessar individualmente cada botão, também conseguiremos.

Outro ponto é que estamos usando um union anônimo. Isso significa que podemos acessar move_up ou qualquer outro valor da struct como se fosse uma propriedade de Game_Controller, veja:

1
2
my_controller.buttons; // <- funciona por causa da union anonima.
my_controller.move_up; // <- funciona por causa da union anonima E struct anonima.

O Input

Agora, vamos ter uma última struct que irá ter um array de controles para usarmos no jogo e para fazermos o swap dos estados em cada frame.

1
2
3
4
5
6
struct Game_Input
{
    // 0.   teclado
    // 1-4. 4 controles xbox
    Game_Controller controllers[5];
};

Processando Botões digitais

Para processar cada um dos botões vindo do GamePad devemos:

  1. Buscar do XInput o estado atual do botão (pressed)
  2. Realizar o swap do antigo estado do controle com o novo estado do controle e marcar se houve uma transição.
1
2
3
4
5
6
7
8
9
10
internal void
win32_process_button(Game_Button *old_state, Game_Button *new_state,
                     WORD bit_mask, WORD buttons)
{
    // chega se o gamepad foi pressionado usando o bitmask indicado
    new_state->pressed = (buttons & bit_mask) == bit_mask;
	
    // verifica se o estado mudou baseado no previous vs new.
    new_state->transition_count = old_state->pressed != new_state->pressed ? 1 : 0;
}

Agora vamos alocar 2 game inputs para a transição de estados. Cada input permite 5 controles (4 xbox + 1 keyboard).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main loop
Game_Input input[2] = {};
Game_Input *new_input = &input[0];
Game_Input *old_input = &input[1];


u32 index = 0;
Game_Controller *new_keyboard = &input->controllers[index];
Game_Controller *old_keyboard = &input->controllers[index];
*new_keyboard = {};
new_keyboard->connected = true;


win32_process_button(&old_controller->action_down, &new_controller->action_down, 
                     XINPUT_GAMEPAD_A, pad->wButtons);
					 
// fazer o swap dos controles ao final do main-loop

Deadzone e normalização

Para que os controles funcionem corretamente vamos fazer 2 processos - corrigir o deadzone e aplicar a normalização.

O Deadzone é um valor de “erros” para os sticks dos controles pois eles não iniciam exatamente com Zero - por questões de hardware.

Precisamos corrigir esse limite (threshold) de erro.

Depois, vamos normalizar o valor para que fique entre -1.0 à 1.0. Dessa forma, criamos nossa própria unidade de medida para trabalhar depois.

Logo, com esse range fixo, podemos escalar o valor para quantos desejarmos.

1
2
3
4
5
6
7
8
9
10
11
12
13
internal f32
win32_process_stick(SHORT stick, short dead_zone_threshold)
{
    // o valores max-min do stick são -32768 a 32767. Então vamos dividir
    // pelo valor máximo para normalizarmos entre -1.0 até 1.0
    f32 value = 0;
    if (stick < -dead_zone_threshold) {
        value = (f32) ((stick + dead_zone_threshold) / (32768.0f - dead_zone_threshold));
    } else if (stick > dead_zone_threshold) {
        value = (f32) ((stick - dead_zone_threshold) / (32767.0f - dead_zone_threshold));
    }
    return value;
}

No main-loop:

1
2
3
4
5
float xo = win32_process_stick(pad->sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
float yo = win32_process_stick(pad->sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);

new_controller->stick_avg_x = xo;
new_controller->stick_avg_y = yo;
Esta postagem está licenciada sob CC BY 4.0 pelo autor.