Skip to content
Comfy Engine

Macroquad vs Bevy for your first 2D game in Rust

Macroquad vs Bevy for your first 2D game in Rust

Sooner or later every person who wants to make a 2D game in Rust ends up with two browser tabs open: one for macroquad, one for Bevy. Both are free, both are open source, both can put a sprite on the screen in an afternoon. They are also built on almost opposite ideas about what a game framework should be, and picking the wrong one for your temperament costs more time than picking the "wrong" one on technical grounds. I have shipped small prototypes with both, and this is the comparison I wish someone had handed me before the first one.

I will keep this version-neutral on purpose. Both projects move, Bevy in particular, so treat the code here as the shape of each API rather than something to paste and compile against a specific release.

Two different starting points

Macroquad answers the question "what is the least code between me and a coloured rectangle?" It is a library with a small, immediate-mode API: you write a loop, you call drawing functions every frame, and that is the whole story. It sits on top of miniquad, a thin cross-platform layer for windows, input and graphics.

Bevy answers a bigger question: "how should a whole game be organised?" It is an engine built around an entity component system (ECS), with a scheduler, a plugin system, a renderer on top of wgpu, an asset pipeline and a lot of optional pieces. You do not write the main loop. You describe data and systems, and Bevy runs them.

Neither question is wrong. But you will feel the difference within the first hour.

What the first program looks like

Here is roughly what a moving square looks like in macroquad. The attribute macro sets up the window and turns your async main into the frame loop.

use macroquad::prelude::*;

#[macroquad::main("Square")]
async fn main() {
    let mut x = 100.0;
    loop {
        if is_key_down(KeyCode::Right) {
            x += 200.0 * get_frame_time();
        }
        clear_background(DARKGRAY);
        draw_rectangle(x, 120.0, 32.0, 32.0, ORANGE);
        next_frame().await;
    }
}

Everything is visible: the state is a local variable, input is a function call, drawing is a function call, and next_frame().await hands control back so the frame can be presented. The async part looks odd at first. It exists mostly so the same code can yield to the browser on the web, and in practice you can ignore it beyond that last line.

The Bevy version of the same idea is shaped differently. This is illustrative, because the exact names of bundles, components and scheduling labels have changed across releases.

use bevy::prelude::*;

#[derive(Component)]
struct Player;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn)
        .add_systems(Update, move_player)
        .run();
}

// spawn: create a camera and a sprite entity tagged with Player
// move_player: query Transform with Player, read keyboard input,
// add speed * delta time to translation.x

There is more ceremony, and the payoff only shows once the game grows. The player is an entity, its position lives in a Transform component, and move_player is a plain function that asks Bevy for "every entity that has a Transform and a Player marker". Add a hundred enemies and the system for moving them looks the same.

API style and how it feels

Macroquad feels like a sketchbook. Global functions, very few types to learn, and you own all the structure. For a jam game this is lovely: a struct Game with a few Vecs, an update method and a draw method, and you are done. It is also where macroquad asks for discipline. Nothing stops your main from becoming four hundred lines of tangled state, and when that happens it is your fault, not the library's. If you want a mental model for growing that structure yourself, the piece on how to make a game engine covers the loop, the state and the renderer you end up writing anyway.

Bevy feels like a framework with opinions. Systems take parameters that Bevy fills in (queries, resources, events), and the borrow checker problems that haunt hand-rolled Rust game code mostly disappear, because Bevy splits access between systems for you. The cost is a vocabulary: entities, components, resources, commands, schedules, states, plugins. None of it is hard, but there is a lot of it before your game is fun.

The honest summary: macroquad gets out of your way, Bevy gives you a way.

Compile times

This is the part people underestimate. Macroquad has a small dependency tree, and a clean build is quick; incremental builds after a one-line change are usually short enough that you never break your flow.

Bevy is a large engine with many crates. A first clean build takes a while, and incremental builds depend heavily on your setup. Bevy's own documentation describes ways to speed this up, such as enabling dynamic linking during development, using a faster linker and tuning optimisation levels for dependencies. A typical development profile looks something like this:

[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3

The idea is simple: your own code compiles quickly with light optimisation, while the dependencies are built once with full optimisation so the game does not crawl in debug mode. This same trick helps macroquad projects too, but you feel it far more with Bevy.

A small worked example of why this matters. Say you tweak a jump value 40 times in an evening. At 3 seconds per rebuild, that is 2 minutes of waiting. At 15 seconds per rebuild it is 10 minutes, and worse, 40 small breaks in concentration. I am not quoting benchmarks for either library here (your machine and project decide that), but measure your own loop in the first week, because it shapes how much you experiment.

ECS or not

Bevy is ECS all the way down. If you want to learn data-oriented design, or your game has many similar things (bullets, particles, units), this is a good fit, and the scheduler can run systems in parallel when their data access does not overlap.

Macroquad has no ECS. You can bring one if you like (a small crate such as hecs works fine next to it), or use plain structs and vectors, which is what most small macroquad games do. For a game with one player, a dozen enemies and some tiles, plain Vec<Enemy> is not a compromise. It is often the clearer design. The wider shape of these choices is covered in game engine architecture, and it applies to both camps.

Ecosystem and tooling

Bevy has the larger ecosystem by a wide margin: community plugins for physics, tilemaps, UI, input mapping, inspectors, audio and more, plus a very active community producing examples. The flip side is churn. Bevy has been evolving quickly, breaking releases are normal, and third-party plugins sometimes trail behind the engine. Pin your versions and upgrade on purpose, not by accident.

Macroquad's ecosystem is smaller and more stable in feel. It ships with the basics (textures, text, simple audio, a camera, a small immediate-mode UI) and many people pair it with plain crates for everything else. Fewer moving parts means fewer surprises, and also fewer ready-made answers.

Both handle the usual 2D asset formats you need for sprites. For levels, both work with external editors such as LDtk or Tiled through community crates or your own loader.

Web export

This is where macroquad has a reputation for being pleasant. Building for the wasm32-unknown-unknown target and serving the output with the small JavaScript loader macroquad provides is a short, well-documented path, and the resulting files tend to be small. For jams on itch.io, that matters.

Bevy also runs in the browser, and its examples show it working. The output is bigger, the first build for the web target is slower, and you need to think a little more about features you do not want on the web. It is entirely doable for a jam, but budget a few extra hours the first time.

The comparison table

Question Macroquad Bevy
Core idea Immediate-mode library, you write the loop ECS engine, it runs your systems
Learning curve Small API, learn in a day Larger vocabulary, learn in a week or two
Code structure Entirely up to you Imposed by ECS and plugins
Clean build Quick Noticeably longer
Ecosystem Small, stable Large, moves fast
Upgrades Rarely painful Plan time for breaking changes
Web export Simple, small output Works, heavier output
Good fit Jams, prototypes, solo 2D games Larger games, many entities, learning ECS

When I pick which

I reach for macroquad when the game fits in my head: a jam, an arcade idea, a toy to test a mechanic. Being able to read the whole program top to bottom is worth a lot when you have 48 hours. It is also a fine choice if you want to understand rendering and game structure yourself, because you build the structure.

I reach for Bevy when I expect the project to grow past what I can hold in my head, when there are lots of similar entities, or when I want to use the plugin ecosystem rather than write my own tilemap loader for the third time. It is also the better choice if you specifically want to learn ECS, because it teaches by forcing you to use it.

There are other options worth a look too, such as ggez, Fyrox or a hand-built stack on winit and wgpu, and there is a broader roundup on the other engines page. More Rust-focused articles live in the Rust gamedev section.

A small experiment before you decide

Do not decide from articles, including this one. Spend one evening with each and build the same thing: a square that moves with the arrow keys, jumps with gravity, and collides with three platforms. Time yourself, write down the moments you got stuck, and note your rebuild time after a one-line change. The library that left you wanting to add a fourth platform is the right one for now. You can always switch on the next game, and the platform logic you wrote will move over with less pain than you expect.

Filed in Rust Gamedev

Written by Kasimir Lund

Kasimir writes about Rust for games: engine internals, ownership patterns that survive a real game loop, and getting builds onto other people's machines. He has been writing small engines for himself since the SDL days and lives in Gothenburg.

Keep reading