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
Remapeamento de teclas: Adicione ao InputManager a capacidade de remapear teclas em runtime, com persistencia em arquivo.
Combo system: Implemente um sistema de combos que detecte sequencias de input (como em jogos de luta).
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
- Artigo anterior: Game Loop e Rendering
- Zig Design Patterns — Padroes de projeto
- Zig Data Structures — HashMap e ArrayList
Duvidas sobre input handling? Participe da comunidade Zig Brasil!