Pular para o conteúdo

Desenvolvimento de jogos em Rust: Bevy, Macroquad e mais

Muita gente aprende Rust porque quer fazer um jogo. Que ótimo motivo! Jogos precisam de velocidade, de cuidado com a memória e de um código que não trave no meio da luta contra o chefão, e é exatamente nisso que o Rust é bom.

Esta página mostra um retrato honesto: o que as pessoas já constroem com Rust, qual engine ou biblioteca escolher e as duas ideias que quase todo jogo usa, o game loop (laço do jogo) e o ECS. Depois você vai ver um primeiro programa em Bevy e em Macroquad.

Qual exemplo roda onde? O game loop, o pequeno mundo ECS e o exercício de prática são Rust puro, então têm um botão “Experimente você mesmo” para o Rust Playground. Bevy e Macroquad abrem uma janela, então esses exemplos são para o seu próprio computador, com cargo run. O exemplo com Macroquad foi compilado (inclusive para a web) enquanto esta página era escrita; o exemplo com Bevy segue os exemplos oficiais do Bevy.

A situação real do desenvolvimento de jogos em Rust

Seção intitulada “A situação real do desenvolvimento de jogos em Rust”

O site da comunidade Are We Game Yet? (em inglês) acompanha o ecossistema de jogos em Rust. A resposta dele para a própria pergunta é: “Almost. We have the blocks, bring your own glue.” (“Quase. Nós temos as peças, traga a sua própria cola.”) É um bom resumo.

O que já é bom hoje:

  • Jogos de verdade são lançados. Tiny Glade, um jogo aconchegante de construir castelos no Steam, e Veloren, um RPG de voxels de código aberto, aparecem no Are We Game Yet como jogos feitos em Rust.
  • Uma engine forte. O Bevy tem uma comunidade grande e ativa, e uma versão nova a cada poucos meses.
  • Muitas peças prontas. O Are We Game Yet lista crates (pacotes) para física, áudio, entrada, rede, interface e muito mais.
  • O seu jogo pode rodar na web. O Rust compila para WebAssembly, então muitos jogos em Rust também rodam no navegador.

O que ainda é difícil:

  • Ainda não existe um “Unity do Rust”. A maioria das engines em Rust é voltada para código. Existem editores visuais (o Fyrox tem um, e o Godot pode usar Rust), mas eles são mais novos do que as grandes ferramentas comerciais.
  • As coisas mudam. O Bevy ainda não chegou à versão 1.0, e cada versão traz mudanças incompatíveis. Um tutorial do ano passado pode não compilar mais.
  • Tempo de compilação. Engines grandes levam minutos no primeiro build. Você vai aprender os truques para isso mais abaixo.

Se o seu objetivo é lançar um jogo 3D comercial no ano que vem, com uma equipe de artistas, Unity, Unreal ou Godot são escolhas mais seguras. Se você quer aprender, fazer jogos 2D, game jams, ferramentas ou simulações, ou simplesmente gosta de Rust, é um lugar muito divertido para estar. 🦀

Existem dois tipos de ferramenta. Uma engine (motor de jogo) entrega uma estrutura completa: como funcionam os objetos do jogo, as cenas e os assets. Uma biblioteca (ou framework) entrega uma janela, desenho, som e entrada, e o resto você organiza do seu jeito.

Crate O que é Bom para O que saber
bevy (0.20) Uma engine 2D e 3D completa, construída em torno do ECS Projetos maiores, 3D, aprender como as engines modernas funcionam A engine em Rust mais conhecida. Mudanças incompatíveis a cada versão; primeiro build demorado
macroquad (0.4) Uma biblioteca 2D simples, inspirada na raylib O seu primeiro jogo, game jams, jogos para a web Pouquíssimas dependências, compila rápido. Você chama funções de desenho dentro de um laço
ggez (0.10) Um framework 2D leve, inspirado no LÖVE Jogos 2D com uma estrutura clara Você implementa uma trait EventHandler com update e draw
fyrox (1.0) Uma engine 2D e 3D com editor de cenas Quem quer um editor, como no Godot ou no Unity Chegou à versão 1.0 em 2026. Comunidade menor que a do Bevy
raylib (6.0) Bindings em Rust para a biblioteca C raylib Quem já conhece a raylib do C Precisa de CMake e de ferramentas de build de C, porque compila a biblioteca C (veja FFI: chamando C)
godot (0.5, “gdext”) Bindings em Rust para a engine Godot 4 Usar o editor do Godot com a lógica do jogo em Rust Você escreve classes em Rust que o Godot carrega como uma GDExtension

Uma regra simples: comece com o Macroquad no seu primeiro jogo 2D. Escolha o Bevy quando quiser uma engine completa e estiver disposto a aprender ECS. Escolha o Godot com gdext ou o Fyrox se quiser um editor para posicionar as coisas visualmente.

Quase todo jogo é um grande laço que roda de novo e de novo até você sair:

  1. Ler a entrada (teclas, mouse, controle).
  2. Atualizar o jogo: mover as coisas, verificar colisões, contar pontos.
  3. Desenhar a nova imagem na tela.

Uma volta do laço se chama quadro (frame). A 60 quadros por segundo (FPS), cada quadro tem cerca de 16 milissegundos. Mas nem todos os quadros levam o mesmo tempo. O seu computador pode estar ocupado, ou um quadro pode precisar carregar um arquivo.

Se o seu jogo move o jogador “2 unidades a cada quadro”, ele roda mais rápido em um computador rápido e mais devagar em um lento. Uma solução popular é o fixed timestep (passo de tempo fixo): a lógica do jogo sempre avança em passos iguais (aqui, de 10 ms), e cada quadro roda quantos passos couberem no tempo que passou. O tempo que sobra espera em um acumulador até o próximo quadro.

const STEP_MS: u32 = 10; // a lógica do jogo sempre avança 10 ms de cada vez
const SPEED: i32 = 2; // o jogador anda 2 unidades a cada atualização
fn main() {
// Quanto tempo cada quadro levou de verdade. Um jogo real mede isso com um relógio.
let frame_times = [16, 16, 45, 5, 16, 33];
let mut accumulator = 0; // tempo que já passou, mas ainda não foi simulado
let mut position = 0;
for (frame, elapsed) in frame_times.into_iter().enumerate() {
accumulator += elapsed;
// Roda quantos passos fixos couberem no tempo que temos
let mut updates = 0;
while accumulator >= STEP_MS {
position += SPEED; // update(): a lógica do jogo
accumulator -= STEP_MS;
updates += 1;
}
// render(): desenha uma vez por quadro, aconteça o que acontecer
println!("quadro {frame}: {elapsed:>2} ms, {updates} atualização(ões), jogador em {position:>2}, sobram {accumulator} ms");
}
}

Saída:

quadro 0: 16 ms, 1 atualização(ões), jogador em 2, sobram 6 ms
quadro 1: 16 ms, 2 atualização(ões), jogador em 6, sobram 2 ms
quadro 2: 45 ms, 4 atualização(ões), jogador em 14, sobram 7 ms
quadro 3: 5 ms, 1 atualização(ões), jogador em 16, sobram 2 ms
quadro 4: 16 ms, 1 atualização(ões), jogador em 18, sobram 8 ms
quadro 5: 33 ms, 4 atualização(ões), jogador em 26, sobram 1 ms
▶ Experimente você mesmo

Olhe o quadro 2: ele foi lento (45 ms), então o jogo rodou 4 atualizações para recuperar o atraso. No total passaram 131 ms, e o jogo rodou 13 atualizações de 10 ms. O jogador termina em 26, não importa como o tempo foi dividido em quadros. Isso deixa a física estável e igual em qualquer computador, o que também importa para replays e jogos online. Um perigo: se um quadro for muito lento, o jogo roda muitas atualizações para recuperar o atraso, e isso deixa o próximo quadro lento também. Por isso os laços de verdade também limitam o número de atualizações por quadro (o relógio de jogo do Bevy, por exemplo, avança no máximo 250 ms por quadro, por padrão).

A outra forma comum é o delta time: você mede os segundos desde o último quadro e multiplica as velocidades por esse valor (x += speed * dt). É mais simples, e é o que os exemplos com Bevy e Macroquad mais abaixo fazem. Muitas engines usam os dois: delta time para animação e passo fixo para a física.

Em muitas linguagens, você criaria uma classe Player, uma classe Enemy que herda de Character, e assim por diante. Isso fica bagunçado rápido: e um inimigo que também pode ser pego do chão? A maioria das engines de jogos em Rust usa ECS (Entity Component System, ou sistema de entidades e componentes):

  • Uma entidade é só um ID, como um número. Ela não tem dados nem comportamento próprios.
  • Um componente é um pedaço de dados simples que você prende a uma entidade: Position, Velocity, Health. Em Rust, eles são structs ou enums normais.
  • Um sistema é uma função que roda em todas as entidades que têm certos componentes. Um sistema de movimento, por exemplo, trabalha com tudo o que tem ao mesmo tempo uma Position e uma Velocity.

Uma árvore tem Position, mas não tem Velocity, então o sistema de movimento a ignora. Quer que a árvore se mova? Dê a ela uma Velocity. Você não muda classe nenhuma. Dados e comportamento ficam separados, e isso também combina bem com as regras de borrowing (empréstimo) do Rust.

Aqui está um mundo ECS minúsculo em Rust puro. Cada tipo de componente mora no seu próprio vetor, e o ID da entidade é o índice. None significa “esta entidade não tem esse componente” (veja Option em Rust). Os textos impressos ficaram em inglês porque {:<6} alinha os nomes em colunas, e nomes traduzidos de tamanhos diferentes quebrariam esse alinhamento.

struct Position { x: i32, y: i32 }
struct Velocity { dx: i32, dy: i32 }
// Uma entidade é só um ID: um índice em cada lista de componentes
type Entity = usize;
#[derive(Default)]
struct World {
names: Vec<&'static str>,
positions: Vec<Option<Position>>,
velocities: Vec<Option<Velocity>>,
}
impl World {
fn spawn(&mut self, name: &'static str, pos: Option<Position>, vel: Option<Velocity>) -> Entity {
self.names.push(name);
self.positions.push(pos);
self.velocities.push(vel);
self.names.len() - 1
}
}
// Um sistema: trabalha com toda entidade que tem uma Position E uma Velocity
fn movement_system(world: &mut World) {
for (pos, vel) in world.positions.iter_mut().zip(&world.velocities) {
if let (Some(pos), Some(vel)) = (pos, vel) {
pos.x += vel.dx;
pos.y += vel.dy;
}
}
}
// Outro sistema: imprime toda entidade que tem uma Position
fn print_system(world: &World, tick: u32) {
println!("tick {tick}:");
for (id, pos) in world.positions.iter().enumerate() {
if let Some(pos) = pos {
println!(" #{id} {:<6} at ({}, {})", world.names[id], pos.x, pos.y);
}
}
}
fn main() {
let mut world = World::default();
world.spawn("player", Some(Position { x: 0, y: 0 }), Some(Velocity { dx: 1, dy: 0 }));
world.spawn("tree", Some(Position { x: 5, y: 2 }), None); // sem Velocity: nunca se move
world.spawn("arrow", Some(Position { x: 0, y: 3 }), Some(Velocity { dx: 3, dy: -1 }));
for tick in 1..=2 {
movement_system(&mut world);
print_system(&world, tick);
}
}

Saída:

tick 1:
#0 player at (1, 0)
#1 tree at (5, 2)
#2 arrow at (3, 2)
tick 2:
#0 player at (2, 0)
#1 tree at (5, 2)
#2 arrow at (6, 1)
▶ Experimente você mesmo

A árvore (tree) fica em (5, 2) porque não tem Velocity. Ninguém escreveu “árvores não se movem”; isso simplesmente resulta dos dados. As bibliotecas de ECS de verdade são muito mais rápidas e espertas do que isto (elas guardam os componentes de um jeito que a CPU lê rapidinho e rodam os sistemas em paralelo), mas a ideia é a mesma.

O Bevy é a engine em Rust mais conhecida, e tudo nele é ECS. A versão 0.20 saiu em outubro de 2026. O Bevy precisa do Rust stable mais recente, então atualize primeiro e depois crie um projeto:

Terminal window
rustup update
cargo new my_bevy_game
cd my_bevy_game
cargo add bevy

No Linux, você também precisa de algumas bibliotecas do sistema (som, entrada, gráficos). O guia de instalação do Bevy (em inglês) lista quais são para cada distribuição. No Windows, você precisa das Visual Studio C++ Build Tools, e no macOS, das ferramentas de linha de comando do Xcode.

Este programa é baseado no exemplo oficial 2d/move_sprite do Bevy. Ele desenha um quadrado laranja que desliza para a esquerda e para a direita. (O exemplo oficial carrega uma imagem; aqui usamos um quadrado de cor sólida, para você não precisar de nenhum arquivo de imagem.)

use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins) // janela, renderização, entrada, áudio...
.add_systems(Startup, setup) // roda uma vez, no início
.add_systems(Update, sprite_movement) // roda a cada quadro
.run();
}
// Um componente: para que lado esta entidade está se movendo
#[derive(Component)]
enum Direction {
Left,
Right,
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
// Cria uma entidade com três componentes
commands.spawn((
Sprite::from_color(Color::srgb(1.0, 0.5, 0.2), Vec2::new(50.0, 50.0)),
Transform::from_xyz(0.0, 0.0, 0.0),
Direction::Right,
));
}
// Um sistema: roda em toda entidade que tem uma Direction E um Transform
fn sprite_movement(time: Res<Time>, mut sprite_position: Query<(&mut Direction, &mut Transform)>) {
for (mut logo, mut transform) in &mut sprite_position {
match *logo {
Direction::Right => transform.translation.x += 150. * time.delta_secs(),
Direction::Left => transform.translation.x -= 150. * time.delta_secs(),
}
if transform.translation.x > 200. {
*logo = Direction::Left;
} else if transform.translation.x < -200. {
*logo = Direction::Right;
}
}
}

Compare com o mundo ECS minúsculo lá de cima. #[derive(Component)] transforma um enum normal em um componente. commands.spawn((...)) cria uma entidade com uma tupla de componentes. E Query<(&mut Direction, &mut Transform)> pede ao Bevy “toda entidade que tenha os dois”, exatamente como o nosso movement_system fazia com zip. time.delta_secs() é o delta time da seção do game loop, então o quadrado anda 150 pixels por segundo em qualquer computador.

Rode com cargo run. O primeiro build compila centenas de crates e pode levar vários minutos, então tenha paciência. Depois disso, os novos builds são bem mais rápidos.

O guia de instalação do Bevy recomenda duas coisas. Primeiro, acrescente isto ao Cargo.toml. Isso otimiza o Bevy e as suas outras dependências, mas deixa o seu próprio código rápido de compilar:

[dependencies]
bevy = "0.20"
# Ativa um pouco de otimização no perfil dev.
[profile.dev]
opt-level = 1
# Ativa bastante otimização no perfil dev para as dependências.
[profile.dev.package."*"]
opt-level = 3

Sem isso, um build de debug de um jogo pode ficar muito lento: poucos quadros por segundo e telas de carregamento longas. As dependências são compiladas uma vez só, então você paga pela otimização apenas no primeiro build. (Os perfis são explicados em Desempenho em Rust.)

Segundo, enquanto estiver desenvolvendo, ative a feature dynamic_linking. Assim o Bevy é compilado como uma biblioteca compartilhada separada, e o Cargo não precisa ligar o Bevy inteiro ao seu programa de novo a cada mudança:

Terminal window
cargo run --features bevy/dynamic_linking

Dois avisos do guia do Bevy: no Windows, dynamic_linking só funciona junto com as configurações de opt-level acima. E não lance o seu jogo com ela: o jogo precisaria de um arquivo de biblioteca extra ao lado dele. Compile a versão final sem essa feature. O guia também mostra como usar um linker mais rápido (lld ou mold), o que ajuda muito no Linux.

O Macroquad é bem diferente. Não tem ECS nem construtor de app: você mesmo escreve o game loop e desenha com funções simples, como draw_circle. Ele tem pouquíssimas dependências, então um build do zero leva segundos, e não minutos.

Terminal window
cargo new move_the_ball
cd move_the_ball
cargo add macroquad
[dependencies]
macroquad = "0.4"

Este joguinho mostra uma bola que você move com as setas do teclado:

use macroquad::prelude::*;
#[macroquad::main("Move the Ball")]
async fn main() {
let mut player = vec2(screen_width() / 2.0, screen_height() / 2.0);
let speed = 250.0; // pixels por segundo
loop {
// 1. Entrada: as setas movem o jogador
let dt = get_frame_time(); // segundos desde o último quadro
if is_key_down(KeyCode::Right) {
player.x += speed * dt;
}
if is_key_down(KeyCode::Left) {
player.x -= speed * dt;
}
if is_key_down(KeyCode::Down) {
player.y += speed * dt;
}
if is_key_down(KeyCode::Up) {
player.y -= speed * dt;
}
if is_key_pressed(KeyCode::Escape) {
break;
}
// 2. Mantém o jogador dentro da tela
player.x = player.x.clamp(0.0, screen_width());
player.y = player.y.clamp(0.0, screen_height());
// 3. Desenha tudo neste quadro
clear_background(DARKBLUE);
draw_circle(player.x, player.y, 20.0, ORANGE);
draw_text("Arrow keys to move, Esc to quit", 20.0, 30.0, 28.0, WHITE);
next_frame().await
}
}

Dá para ver os três passos do game loop: entrada, atualização, desenho. #[macroquad::main("Move the Ball")] abre uma janela com esse título. O main é async, mas você não precisa do Tokio nem de saber async: next_frame().await significa simplesmente “mostre este quadro e espere o próximo”. No Linux, o README do Macroquad lista alguns pacotes do sistema para instalar antes (pkg-config libx11-dev libxi-dev libgl1-mesa-dev libasound2-dev no Ubuntu).

Tanto o Macroquad quanto o Bevy conseguem compilar o seu jogo para o navegador, como WebAssembly. Se Wasm é novidade para você, leia Rust e WebAssembly primeiro. Você sempre começa adicionando o target do Wasm:

Terminal window
rustup target add wasm32-unknown-unknown

O Macroquad deixa isso muito fácil. Um único comando gera um arquivo .wasm:

Terminal window
cargo build --release --target wasm32-unknown-unknown

O arquivo vai parar em target/wasm32-unknown-unknown/release/move_the_ball.wasm (no exemplo acima, ele ficou com uns 600 KB). O README do Macroquad traz um index.html curtinho que o carrega com um pequeno arquivo JavaScript, o mq_js_bundle.js. Coloque os dois arquivos em uma pasta e sirva essa pasta com qualquer servidor web local, por exemplo basic-http-server . (instale com cargo install basic-http-server). Só dar um clique duplo no arquivo HTML normalmente não funciona, porque os navegadores bloqueiam o carregamento do .wasm direto do disco.

O Bevy usa o wasm-bindgen para criar o código de ligação em JavaScript. Estes passos foram adaptados do README de exemplos do Bevy (para o projeto my_bevy_game acima):

Terminal window
cargo install wasm-bindgen-cli
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --out-dir web --target web target/wasm32-unknown-unknown/release/my_bevy_game.wasm

Isso grava um arquivo .js e um _bg.wasm na pasta web; uma pequena página HTML importa o .js e inicia o jogo. O guia de instalação do Bevy também sugere um perfil wasm-release com opt-level = "s" para deixar o download menor. Builds para a web são um ótimo jeito de compartilhar jogos de game jam: as pessoas jogam com um clique, sem instalar nada.

Mover uma quantidade fixa a cada quadro. Com x += 5.0 a cada quadro, o jogo roda duas vezes mais rápido a 120 FPS do que a 60 FPS. Multiplique pelo delta time (x += speed * dt) ou use um passo de tempo fixo.

Seguir um tutorial antigo de Bevy. O Bevy muda a sua API a cada versão, então um post de blog sobre a 0.12 muitas vezes não compila com a 0.20. Confira qual versão o tutorial usa e prefira os exemplos oficiais da sua versão. Cada versão também tem um guia de migração no site do Bevy.

Builds de debug lentos. Se o seu jogo em Bevy roda a poucos quadros por segundo, você provavelmente esqueceu as configurações de opt-level no Cargo.toml.

Começar grande demais. Um RPG de mundo aberto é um primeiro projeto difícil em qualquer linguagem. Comece com Pong, Snake, Breakout ou Flappy Bird. Termine o jogo e depois faça o próximo um pouco maior.

Todo jogo tem telas: um menu, o jogo em si, uma tela de pausa, “game over”. Crie um enum GameState com Menu, Playing, Paused e GameOver, e um enum Input com Enter, Escape e PlayerDied. Escreva fn next_state(state: GameState, input: Input) -> GameState: Enter começa o jogo a partir do menu, Escape pausa e tira da pausa, morrer termina o jogo, e Enter no “game over” volta para o menu. Qualquer outra entrada não muda nada. Passe uma lista fixa de entradas e imprima cada mudança. Dica: use match na tupla (state, input) e termine com (same, _) => same.

Ver solução
#[derive(Debug, Clone, Copy)]
enum GameState {
Menu,
Playing,
Paused,
GameOver,
}
#[derive(Debug, Clone, Copy)]
enum Input {
Enter,
Escape,
PlayerDied,
}
fn next_state(state: GameState, input: Input) -> GameState {
match (state, input) {
(GameState::Menu, Input::Enter) => GameState::Playing,
(GameState::Playing, Input::Escape) => GameState::Paused,
(GameState::Paused, Input::Escape) => GameState::Playing,
(GameState::Playing, Input::PlayerDied) => GameState::GameOver,
(GameState::GameOver, Input::Enter) => GameState::Menu,
// Qualquer outra coisa: ignora a entrada e fica onde está
(same, _) => same,
}
}
fn main() {
use Input::*; // permite escrever Enter em vez de Input::Enter
let inputs = [Escape, Enter, Escape, PlayerDied, Escape, PlayerDied, Enter];
let mut state = GameState::Menu;
for input in inputs {
let new_state = next_state(state, input);
println!("{state:?} + {input:?} -> {new_state:?}");
state = new_state;
}
}

Saída:

Menu + Escape -> Menu
Menu + Enter -> Playing
Playing + Escape -> Paused
Paused + PlayerDied -> Paused
Paused + Escape -> Playing
Playing + PlayerDied -> GameOver
GameOver + Enter -> Menu
▶ Experimente você mesmo

Fazer match em uma tupla lista todas as mudanças permitidas em um só lugar, e o último braço deixa todas as outras entradas inofensivas. Um jogador pausado não pode morrer, porque (Paused, PlayerDied) cai em (same, _). O Bevy tem essa mesma ideia embutida, chamada States.

  • O Rust é uma opção real para jogos: “quase lá”, com uma engine forte (Bevy), bibliotecas simples (Macroquad, ggez), opções com editor (Fyrox, Godot com gdext) e muitas peças prontas.
  • Todo jogo roda um laço: entrada, atualização, desenho. Use delta time ou um passo de tempo fixo para que a velocidade não dependa da taxa de quadros.
  • O ECS divide um jogo em entidades (IDs), componentes (dados simples) e sistemas (funções que rodam nas entidades que têm certos componentes).
  • O Bevy é ECS de ponta a ponta. Acrescente as configurações de opt-level nos perfis e use dynamic_linking só durante o desenvolvimento.
  • O Macroquad é o começo mais rápido em 2D: você mesmo escreve o laço e desenha com funções simples.
  • Os dois conseguem compilar para o navegador com o target wasm32-unknown-unknown.

Teste o que aprendeu

1. No ECS, o que é uma entidade?

2. Por que um passo de tempo fixo faz o jogo se comportar igual em qualquer computador?

3. Com qual configuração do Bevy você NÃO deve lançar o seu jogo pronto?

4. Você quer fazer o seu primeiro joguinho 2D, com builds bem rápidos. Qual crate esta página sugere?

Pontuação: 0 / 4

  • Are We Game Yet? (em inglês): a visão geral da comunidade sobre engines de jogos em Rust, bibliotecas de física, áudio e rede, e jogos feitos com Rust.
  • Bevy Quick Start: Setup (em inglês): o guia oficial para instalar o Bevy, com as configurações para compilar rápido e os perfis de release e de Wasm.
  • Game development in Rust with Macroquad (em inglês): um livro on-line gratuito que constrói passo a passo um jogo de nave espacial completo com Macroquad.
  • The godot-rust book (em inglês): como escrever a lógica de jogos para o Godot 4 em Rust com o gdext.
  • Game Loop — Game Programming Patterns (em inglês): uma explicação amigável e detalhada sobre game loops, delta time e passos de tempo fixos.