How to Use the Macroquad Crate for Simple 2D Games

Macroquad is a lightweight, cross-platform game engine for Rust that simplifies 2D development by providing a single, unified API for graphics, audio, and input without requiring complex build configurations.

When the engine gets in the way

You have an idea for a game. Maybe it's a platformer where the gravity flips every ten seconds. Maybe it's a dungeon crawler with procedural rooms. You open your editor, ready to code. Then you hit the wall. Do you set up a window with winit? Do you fight the graphics API? Do you spend three days just getting a triangle on the screen? Most game engines in Rust demand you build the engine before you build the game. You configure backends, manage event loops, and wrestle with shader compilation before you write a single line of game logic.

macroquad says no. It gives you a window, a renderer, and an input loop in one line. You write game logic. The crate handles the rest. It is a lightweight, cross-platform engine designed for rapid prototyping and simple 2D games. You get a unified API for graphics, audio, and input without complex build configurations. It works on Windows, Mac, Linux, and in the browser via WebAssembly. You stop building the engine and start building the game.

The pre-wired arcade cabinet

Think of macroquad as a pre-wired arcade cabinet. You don't solder the wires. You don't calibrate the CRT. You just plug in your cartridge and play. Under the hood, it uses miniquad, which talks to OpenGL, WebGL, and Metal. You get one API that works everywhere. The crate exports a prelude module that dumps everything you need into scope. No use std::... chains. No nested namespaces. Just use macroquad::prelude::*; and you're ready.

The community convention is to use the wildcard import from the prelude. macroquad namespaces its exports carefully, so * is safe and idiomatic here. You'll see it in every tutorial and example. Follow the convention. It keeps your code clean and matches the crate's philosophy of low friction.

Stop fighting the boilerplate. Import the prelude and move on.

Your first frame

Here is a minimal game. It creates a window and draws a red square that follows your mouse cursor. Copy this into main.rs, add macroquad = "0.4" to your Cargo.toml, and run it.

use macroquad::prelude::*;

// The #[macroquad::main] attribute sets up the window and the async runtime.
// It transforms your function into the entry point for the game loop.
#[macroquad::main]
fn game() {
    // The game runs in an infinite loop.
    loop {
        // Clear the screen every frame. Without this, you get motion trails.
        clear_background(DARKBLUE);

        // Get the mouse position. Coordinates start at top-left (0,0).
        let (mx, my) = mouse_position();

        // Draw a rectangle centered on the mouse.
        // The first two args are top-left corner, so we offset by half the size.
        draw_rectangle(mx - 25.0, my - 25.0, 50.0, 50.0, RED);

        // Request the next frame. This yields control back to the engine.
        // The engine renders what you drew, processes input, and wakes you up.
        next_frame().await;
    }
}

When you run this, macroquad initializes the graphics backend. It creates a window. It calls your game function. Inside, you have an infinite loop. Every iteration, you clear the screen, read input, draw shapes, and then call next_frame().await. That await is the handoff. You tell the engine, "I'm done with this frame. Go render what I drew, then wake me up for the next one." The engine handles the vertical sync and the frame timing. You don't manage threads. You don't manage the event loop. You just describe the state of the world, frame by frame.

The coordinate system is simple. (0, 0) is the top-left corner of the window. X increases to the right. Y increases downward. This matches screen pixels by default. If you need a different coordinate system, macroquad provides camera tools to transform the space.

That await is the heartbeat. Without it, the game is just a frozen painting.

Moving beyond the square

A following square is a start. Real games need state, input handling, and smooth movement. Here is a more realistic example. It tracks a player position, handles keyboard input, and uses delta time to ensure the movement feels the same on a 60Hz monitor and a 144Hz monitor.

use macroquad::prelude::*;

// Group game state in a struct. This keeps variables organized and prevents
// passing ten arguments between functions.
struct GameState {
    player_x: f32,
    player_y: f32,
    speed: f32,
}

#[macroquad::main]
fn game() {
    // Initialize state once, outside the loop.
    // The player starts in the center of a 800x600 window.
    let mut state = GameState {
        player_x: 400.0,
        player_y: 300.0,
        speed: 200.0, // Pixels per second.
    };

    loop {
        // Get delta time. This is the time elapsed since the last frame.
        // Multiplying by dt makes movement frame-rate independent.
        let dt = get_frame_time();

        // Update logic based on input.
        // is_key_down returns true while the key is held.
        if is_key_down(KeyCode::Right) {
            state.player_x += state.speed * dt;
        }
        if is_key_down(KeyCode::Left) {
            state.player_x -= state.speed * dt;
        }
        if is_key_down(KeyCode::Up) {
            state.player_y -= state.speed * dt;
        }
        if is_key_down(KeyCode::Down) {
            state.player_y += state.speed * dt;
        }

        // Draw logic.
        clear_background(BLACK);

        // Draw the player as a circle.
        draw_circle(state.player_x, state.player_y, 20.0, YELLOW);

        // Draw instructions.
        draw_text("Arrow keys to move", 10.0, 10.0, WHITE);

        // Hand off to the engine.
        next_frame().await;
    }
}

The struct keeps your state organized. As your game grows, you'll add enemies, projectiles, and score. A struct prevents your main function from becoming a wall of variables. The speed field is in pixels per second. Multiplying by dt converts that to pixels per frame. If the frame takes 0.016 seconds, the player moves 200.0 * 0.016 pixels. If the frame takes 0.033 seconds, the player moves 200.0 * 0.033 pixels. The speed stays constant regardless of frame rate.

is_key_down checks if the key is currently pressed. There is also is_key_pressed, which returns true only on the frame the key was first pressed. Use is_key_down for continuous movement. Use is_key_pressed for actions like jumping or shooting.

Frame-rate independence isn't optional. Multiply by delta time or your player teleports on 144Hz monitors.

Where things go wrong

macroquad is simple, but simplicity hides constraints. You'll hit a few walls if you treat it like a general-purpose runtime.

Asset loading is async. Functions like load_texture and load_sound return futures. You must await them. If you try to load a texture inside the loop without awaiting, the compiler rejects you. If you load assets every frame, you'll leak memory and stutter the game. Load assets once, before the loop, or use a lazy loading pattern.

// BAD: Loading inside the loop without caching.
// This loads the file every frame. The game will choke.
loop {
    let tex = load_texture("player.png").await.unwrap();
    draw_texture(tex, 0.0, 0.0, WHITE);
    next_frame().await;
}

// GOOD: Load once, use many times.
let tex = load_texture("player.png").await.unwrap();
loop {
    draw_texture(tex, 0.0, 0.0, WHITE);
    next_frame().await;
}

The crate is single-threaded. The graphics context lives on the main thread. If you try to spawn a thread and use macroquad functions from that thread, the compiler rejects you with E0277 (trait bound not satisfied). The context is not Send. If you need heavy computation, do it in a separate thread, but keep the results separate from the drawing calls. Send the results back via a channel and apply them in the main loop.

Blocking calls kill the frame rate. If you do a heavy calculation inside the loop, the frame rate drops. macroquad doesn't have a background thread for you. You have to manage that. Keep the update loop light. If your logic takes more than 16ms, your frame rate falls below 60fps. Profile before you panic.

Keep the main thread light. If your update loop takes more than 16ms, your frame rate drops. Profile before you panic.

Choosing your tool

macroquad is not the only game crate in Rust. It is a specific tool for a specific job. Pick it based on your needs.

Use macroquad when you want to prototype a game mechanic in an afternoon and hate boilerplate. Use macroquad when you are targeting the browser with WebAssembly and need a single-file setup. Use macroquad when you are learning Rust and want to focus on game logic, not window management. Use macroquad when you are building a simple 2D game with straightforward state and no complex entity systems.

Use bevy when your game grows into a complex simulation with hundreds of interacting entities. Use bevy when you need a data-oriented architecture for performance. Use bevy when you want a community-driven ecosystem with plugins for UI, animation, and physics.

Use wgpu when you need fine-grained control over shaders and pipelines. Use wgpu when you are building a custom engine or a graphics tool. Use wgpu when you need to optimize the rendering loop for a specific GPU.

Pick the tool that matches the complexity. Don't use a sledgehammer to crack a nut, and don't use a nutcracker to build a house.

Where to go next