Input Handling em Jogos com Zig: Teclado, Mouse, Gamepad e Abstraccao

Um sistema de input bem projetado e a ponte entre o jogador e o jogo. Ele precisa ser responsivo, configuravel e desacoplado da logica do jogo. Neste artigo, construimos um sistema de input completo em Zig, cobrindo teclado, mouse, gamepad, mapeamento de acoes e input buffering.

Este artigo faz parte da serie Desenvolvimento de Jogos com Zig. Certifique-se de ter concluido o setup e o game loop.

Input Basico com Raylib

Raylib fornece funcoes diretas para captura de input:

Teclado

const rl = @cImport(@cInclude("raylib.h"));

pub fn main() void {
    rl.InitWindow(800, 600, "Input Basico");
    defer rl.CloseWindow();
    rl.SetTargetFPS(60);

    var x: f32 = 400;
    var y: f32 = 300;

    while (!rl.WindowShouldClose()) {
        const dt = rl.GetFrameTime();

        // IsKeyDown: retorna true enquanto a tecla esta pressionada
        if (rl.IsKeyDown(rl.KEY_W)) y -= 200 * dt;
        if (rl.IsKeyDown(rl.KEY_S)) y += 200 * dt;
        if (rl.IsKeyDown(rl.KEY_A)) x -= 200 * dt;
        if (rl.IsKeyDown(rl.KEY_D)) x += 200 * dt;

        // IsKeyPressed: retorna true apenas no frame em que a tecla foi pressionada
        if (rl.IsKeyPressed(rl.KEY_SPACE)) {
            // Acao unica (pular, atirar, etc.)
            rl.DrawText("PULO!", 10, 50, 20, rl.YELLOW);
        }

        // IsKeyReleased: retorna true apenas no frame em que a tecla foi solta
        if (rl.IsKeyReleased(rl.KEY_SPACE)) {
            // Fim da acao
        }

        rl.BeginDrawing();
        rl.ClearBackground(rl.BLACK);
        rl.DrawCircle(@intFromFloat(x), @intFromFloat(y), 16, rl.RED);
        rl.DrawFPS(10, 10);
        rl.EndDrawing();
    }
}

Mouse

const rl = @cImport(@cInclude("raylib.h"));

pub fn main() void {
    rl.InitWindow(800, 600, "Mouse Input");
    defer rl.CloseWindow();
    rl.SetTargetFPS(60);

    var cor = rl.BLUE;
    var raio: f32 = 20;

    while (!rl.WindowShouldClose()) {
        // Posicao do mouse
        const mouse_x = rl.GetMouseX();
        const mouse_y = rl.GetMouseY();

        // Botoes do mouse
        if (rl.IsMouseButtonDown(rl.MOUSE_BUTTON_LEFT)) {
            cor = rl.RED;
        } else if (rl.IsMouseButtonDown(rl.MOUSE_BUTTON_RIGHT)) {
            cor = rl.GREEN;
        } else {
            cor = rl.BLUE;
        }

        // Scroll do mouse
        const scroll = rl.GetMouseWheelMove();
        raio += scroll * 5;
        if (raio < 5) raio = 5;
        if (raio > 100) raio = 100;

        rl.BeginDrawing();
        rl.ClearBackground(rl.BLACK);

        rl.DrawCircle(mouse_x, mouse_y, raio, cor);

        // Crosshair
        rl.DrawLine(mouse_x - 10, mouse_y, mouse_x + 10, mouse_y, rl.WHITE);
        rl.DrawLine(mouse_x, mouse_y - 10, mouse_x, mouse_y + 10, rl.WHITE);

        rl.EndDrawing();
    }
}

Sistema de Input Abstrato

Para jogos serios, nunca acople a logica do jogo diretamente aos codigos de tecla. Em vez disso, use um sistema de mapeamento de acoes:

const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));

pub const Acao = enum {
    mover_cima,
    mover_baixo,
    mover_esquerda,
    mover_direita,
    pular,
    atacar,
    interagir,
    pausar,
};

pub const FonteInput = union(enum) {
    tecla: c_int,
    mouse_botao: c_int,
    gamepad_botao: c_int,
};

pub const InputManager = struct {
    mapeamento: std.AutoHashMap(Acao, FonteInput),
    estado_atual: std.AutoHashMap(Acao, bool),
    estado_anterior: std.AutoHashMap(Acao, bool),

    pub fn init(allocator: std.mem.Allocator) InputManager {
        return .{
            .mapeamento = std.AutoHashMap(Acao, FonteInput).init(allocator),
            .estado_atual = std.AutoHashMap(Acao, bool).init(allocator),
            .estado_anterior = std.AutoHashMap(Acao, bool).init(allocator),
        };
    }

    pub fn deinit(self: *InputManager) void {
        self.mapeamento.deinit();
        self.estado_atual.deinit();
        self.estado_anterior.deinit();
    }

    pub fn mapear(self: *InputManager, acao: Acao, fonte: FonteInput) !void {
        try self.mapeamento.put(acao, fonte);
    }

    pub fn configurarPadrao(self: *InputManager) !void {
        try self.mapear(.mover_cima, .{ .tecla = rl.KEY_W });
        try self.mapear(.mover_baixo, .{ .tecla = rl.KEY_S });
        try self.mapear(.mover_esquerda, .{ .tecla = rl.KEY_A });
        try self.mapear(.mover_direita, .{ .tecla = rl.KEY_D });
        try self.mapear(.pular, .{ .tecla = rl.KEY_SPACE });
        try self.mapear(.atacar, .{ .mouse_botao = rl.MOUSE_BUTTON_LEFT });
        try self.mapear(.interagir, .{ .tecla = rl.KEY_E });
        try self.mapear(.pausar, .{ .tecla = rl.KEY_ESCAPE });
    }

    pub fn update(self: *InputManager) void {
        // Copiar estado atual para anterior
        var iter = self.estado_atual.iterator();
        while (iter.next()) |entrada| {
            self.estado_anterior.put(entrada.key_ptr.*, entrada.value_ptr.*) catch {};
        }

        // Atualizar estado atual
        var map_iter = self.mapeamento.iterator();
        while (map_iter.next()) |entrada| {
            const acao = entrada.key_ptr.*;
            const fonte = entrada.value_ptr.*;

            const pressionado = switch (fonte) {
                .tecla => |k| rl.IsKeyDown(k),
                .mouse_botao => |b| rl.IsMouseButtonDown(b),
                .gamepad_botao => |b| rl.IsGamepadButtonDown(0, b),
            };

            self.estado_atual.put(acao, pressionado) catch {};
        }
    }

    // Retorna true enquanto a acao esta ativa
    pub fn pressionado(self: *InputManager, acao: Acao) bool {
        return self.estado_atual.get(acao) orelse false;
    }

    // Retorna true apenas no frame em que a acao comecou
    pub fn acabouDePressionar(self: *InputManager, acao: Acao) bool {
        const atual = self.estado_atual.get(acao) orelse false;
        const anterior = self.estado_anterior.get(acao) orelse false;
        return atual and !anterior;
    }

    // Retorna true apenas no frame em que a acao terminou
    pub fn acabouDeSoltar(self: *InputManager, acao: Acao) bool {
        const atual = self.estado_atual.get(acao) orelse false;
        const anterior = self.estado_anterior.get(acao) orelse false;
        return !atual and anterior;
    }
};

Usando o InputManager

pub fn main() !void {
    rl.InitWindow(800, 600, "Input Abstrato");
    defer rl.CloseWindow();
    rl.SetTargetFPS(60);

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var input = InputManager.init(gpa.allocator());
    defer input.deinit();
    try input.configurarPadrao();

    var x: f32 = 400;
    var y: f32 = 300;
    var vel_y: f32 = 0;
    var no_chao = true;

    while (!rl.WindowShouldClose()) {
        const dt = rl.GetFrameTime();
        input.update();

        // Movimento horizontal
        if (input.pressionado(.mover_esquerda)) x -= 200 * dt;
        if (input.pressionado(.mover_direita)) x += 200 * dt;

        // Pulo
        if (input.acabouDePressionar(.pular) and no_chao) {
            vel_y = -400;
            no_chao = false;
        }

        // Gravidade
        vel_y += 800 * dt;
        y += vel_y * dt;

        // Chao
        if (y > 500) {
            y = 500;
            vel_y = 0;
            no_chao = true;
        }

        // Pausa
        if (input.acabouDePressionar(.pausar)) {
            // Toggle pausa
        }

        rl.BeginDrawing();
        rl.ClearBackground(rl.BLACK);
        rl.DrawRectangle(@intFromFloat(x - 16), @intFromFloat(y - 32), 32, 32, rl.BLUE);
        rl.DrawLine(0, 516, 800, 516, rl.GREEN); // Chao
        rl.EndDrawing();
    }
}

Suporte a Gamepad

const rl = @cImport(@cInclude("raylib.h"));

pub const GamepadInput = struct {
    pub fn detectarGamepads() void {
        var i: c_int = 0;
        while (i < 4) : (i += 1) {
            if (rl.IsGamepadAvailable(i)) {
                const nome = rl.GetGamepadName(i);
                rl.TraceLog(rl.LOG_INFO, "Gamepad %d: %s", i, nome);
            }
        }
    }

    pub fn lerAnalogico(gamepad: c_int, eixo: c_int, deadzone: f32) f32 {
        const valor = rl.GetGamepadAxisMovement(gamepad, eixo);
        if (@abs(valor) < deadzone) return 0;
        return valor;
    }

    pub fn obterDirecao(gamepad: c_int) struct { x: f32, y: f32 } {
        const deadzone: f32 = 0.15;
        return .{
            .x = lerAnalogico(gamepad, rl.GAMEPAD_AXIS_LEFT_X, deadzone),
            .y = lerAnalogico(gamepad, rl.GAMEPAD_AXIS_LEFT_Y, deadzone),
        };
    }
};

Input Buffering

Input buffering e essencial para jogos de acao. Ele armazena inputs recentes para que o jogador nao precise ter timing perfeito:

const std = @import("std");
const rl = @cImport(@cInclude("raylib.h"));

pub const InputBuffer = struct {
    const MAX_BUFFER = 10;

    const EntradaBuffered = struct {
        acao: u8,
        timestamp: f64,
    };

    buffer: [MAX_BUFFER]EntradaBuffered = undefined,
    tamanho: usize = 0,
    janela_buffer: f64 = 0.15, // 150ms de janela

    pub fn adicionar(self: *InputBuffer, acao: u8) void {
        if (self.tamanho < MAX_BUFFER) {
            self.buffer[self.tamanho] = .{
                .acao = acao,
                .timestamp = rl.GetTime(),
            };
            self.tamanho += 1;
        }
    }

    pub fn consumir(self: *InputBuffer, acao: u8) bool {
        const agora = rl.GetTime();

        var i: usize = 0;
        while (i < self.tamanho) : (i += 1) {
            if (self.buffer[i].acao == acao and
                agora - self.buffer[i].timestamp <= self.janela_buffer)
            {
                // Remover do buffer
                self.remover(i);
                return true;
            }
        }
        return false;
    }

    fn remover(self: *InputBuffer, idx: usize) void {
        if (idx < self.tamanho - 1) {
            std.mem.copyForwards(
                EntradaBuffered,
                self.buffer[idx..],
                self.buffer[idx + 1 .. self.tamanho],
            );
        }
        self.tamanho -= 1;
    }

    pub fn limparExpirados(self: *InputBuffer) void {
        const agora = rl.GetTime();
        var i: usize = 0;
        while (i < self.tamanho) {
            if (agora - self.buffer[i].timestamp > self.janela_buffer) {
                self.remover(i);
            } else {
                i += 1;
            }
        }
    }
};

Sistema de Replay

Gravando e reproduzindo inputs para replays:

const std = @import("std");

pub const FrameInput = struct {
    frame: u64,
    acoes: u32, // Bitmask de acoes ativas
};

pub const ReplaySystem = struct {
    gravacao: std.ArrayList(FrameInput),
    modo: enum { nenhum, gravando, reproduzindo } = .nenhum,
    frame_atual: u64 = 0,
    idx_replay: usize = 0,

    pub fn init(allocator: std.mem.Allocator) ReplaySystem {
        return .{
            .gravacao = std.ArrayList(FrameInput).init(allocator),
        };
    }

    pub fn deinit(self: *ReplaySystem) void {
        self.gravacao.deinit();
    }

    pub fn iniciarGravacao(self: *ReplaySystem) void {
        self.gravacao.clearRetainingCapacity();
        self.modo = .gravando;
        self.frame_atual = 0;
    }

    pub fn gravarFrame(self: *ReplaySystem, acoes: u32) !void {
        if (self.modo != .gravando) return;
        try self.gravacao.append(.{
            .frame = self.frame_atual,
            .acoes = acoes,
        });
        self.frame_atual += 1;
    }

    pub fn iniciarReplay(self: *ReplaySystem) void {
        self.modo = .reproduzindo;
        self.frame_atual = 0;
        self.idx_replay = 0;
    }

    pub fn obterAcoesFrame(self: *ReplaySystem) ?u32 {
        if (self.modo != .reproduzindo) return null;
        if (self.idx_replay >= self.gravacao.items.len) {
            self.modo = .nenhum;
            return null;
        }

        const frame_gravado = self.gravacao.items[self.idx_replay];
        if (frame_gravado.frame == self.frame_atual) {
            self.idx_replay += 1;
            self.frame_atual += 1;
            return frame_gravado.acoes;
        }

        self.frame_atual += 1;
        return 0;
    }
};

Exercicios

  1. Remapeamento de teclas: Adicione ao InputManager a capacidade de remapear teclas em runtime, com persistencia em arquivo.

  2. Combo system: Implemente um sistema de combos que detecte sequencias de input (como em jogos de luta).

  3. Input multi-jogador: Estenda o InputManager para suportar dois jogadores simultaneos com mapeamentos independentes.


Proximo Artigo

No proximo artigo, exploramos o padrao ECS (Entity Component System), a arquitetura mais usada em game dev moderno.

Conteudo Relacionado


Duvidas sobre input handling? Participe da comunidade Zig Brasil!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.