Skip to content

Rust GUI Game Tutorial: Build a Simple Mug Game with Bevy Engine


Rust GUI game tutorial

🧱 Section 1: Rust GUI Game Tutorial – Project Setup with Bevy

🎯 Goal: Prepare a clean Rust game project using Bevy that opens a basic GUI window. This section is the foundation of your Rust GUI game development journey.


📌 Prerequisite

If you haven’t set up Rust yet, follow this step-by-step guide:
👉 How to Set Up Rust Development Environment on Windows

This Rust GUI game tutorial assumes you have Rust and cargo installed and properly configured.


🗂️ Step 1: Create a New Rust Project

Let’s start by creating a fresh Rust project for our mug game:

cargo new mug_game
cd mug_game

This command sets up a basic project with the following structure:

mug_game/
├── Cargo.toml
└── src/
    └── main.rs

📦 Step 2: Add Bevy to Your Project

We’ll use the Bevy engine, a modern game engine built in Rust. Add it to your Cargo.toml file:

# Cargo.toml

[dependencies]

bevy = “0.13”

✅ Bevy handles the game loop, 2D rendering, assets, input, and more — making it perfect for this Rust 2D game tutorial.

After saving the file, fetch the dependencies:

cargo build

This may take a few minutes on the first build, as Bevy is a large framework.


🧪 Step 3: Launch Your First GUI Window

Edit src/main.rs to show a blank window using Bevy:

use bevy::prelude::*;

/// Entry point of the mug game
fn main() {
    App::new()
        .add_plugins(DefaultPlugins) // Adds default rendering, input, windowing, etc.
        .add_startup_system(setup_camera) // Runs once when the game starts
        .run();
}

/// Sets up a 2D camera for rendering our game world
fn setup_camera(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());
}

This code:

  • Initializes the game app
  • Loads default plugins (window, input, render loop)
  • Spawns a 2D camera so we can render sprites later

Now run your project:

cargo run

You should see a blank GUI window — success! 🎉
Your first 2D Rust game window is live.


✅ Summary of This Section

StepDescription
Project Initcargo new mug_game
Bevy SetupAdd bevy = "0.13" in Cargo.toml
GUI WindowBevy’s default plugins + 2D camera

Shall we move on to Section 2: Loading Graphics & Displaying a Mug Icon?
In the next part, we’ll load a PNG image and render a mug sprite in the center of the screen.

Table of Contents

🖼️ Section 2: Load a Mug Icon and Display it on Screen

🎯 Goal: In this part of the Rust GUI game tutorial, you’ll load a PNG image of a mug and display it in the game window using the Bevy engine.


📁 Step 1: Prepare an Asset Folder

Create a folder named assets/ in your project root:

mug_game/
├── assets/
│ └── mug.png
├── Cargo.toml
└── src/
└── main.rs

Place a simple PNG image of a mug in that folder (name it mug.png).
You can download free assets from sites like:

📌 Tip: Keep the image size small (e.g. 64×64 or 128×128 pixels) for now.


✏️ Step 2: Update main.rs to Load and Display the Mug

Replace src/main.rs with this updated code:

use bevy::prelude::*;

/// Entry point for the Rust GUI game tutorial project
fn main() {
App::new()
.add_plugins(DefaultPlugins) // Core Bevy plugins
.add_startup_system(setup_camera)
.add_startup_system(spawn_mug)
.run();
}

/// Adds a 2D camera so the world is visible
fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}

/// Loads a mug sprite and displays it at the center of the screen
fn spawn_mug(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut textures: ResMut<Assets<Image>>,
) {
// Load the mug image from the assets folder
let texture_handle = asset_server.load("mug.png");

// Spawn the sprite at the center
commands.spawn(SpriteBundle {
texture: texture_handle,
transform: Transform::from_xyz(0.0, 0.0, 0.0), // center of the screen
..default()
});
}

⚙️ Step 3: Run the Game

Now run your game:

cargo run

If everything is correct, a window opens with your mug icon rendered in the center.
Congratulations! 🎉 You’ve completed a core milestone in your Rust 2D game development journey.


🧠 How It Works (Algorithm Explanation)

PartWhat It Does
AssetServer::loadLoads image from assets/ folder as texture handle
SpriteBundleA Bevy struct that displays an image
TransformControls position (x, y, z) on screen
Camera2dBundleRequired to display anything in a 2D space

🧠 Note: Asset loading in Bevy is asynchronous, but Bevy handles that behind the scenes.


✅ Summary

TaskResult
Created assets/Organized sprite resources
Loaded mug.pngDisplayed it on window
Used SpriteBundlePositioned sprite at center

Shall we continue to Section 3: Add Mouse Interaction to the Mug Icon?
Next, we’ll make the mug clickable and maybe even respond with a simple animation or log output.


🖱️ Section 3: Add Mouse Interaction to the Mug Icon

🎯 Goal: In this part of the Rust GUI game tutorial, you’ll learn how to detect mouse clicks on a sprite (the mug icon) and respond with movement or printed feedback. This introduces input handling in Bevy.


📌 Step 1: Tag the Mug Sprite

Bevy uses components to identify entities. We’ll create a custom component called Mug so we can detect which sprite was clicked.

// Define a marker component to identify the mug
#[derive(Component)]
struct Mug;

✏️ Step 2: Update Code to Include the Mug Tag

Modify the spawn_mug function to add the Mug component:

commands.spawn((
    SpriteBundle {
        texture: texture_handle,
        transform: Transform::from_xyz(0.0, 0.0, 0.0),
        ..default()
    },
    Mug, // Add our custom component to this sprite
));

🖱️ Step 3: Detect Mouse Click on the Mug

Now we add a system that checks if the user clicked on the mug. This uses Input<MouseButton>, the camera, and sprite positions.

fn click_mug(
    buttons: Res<Input<MouseButton>>,
    windows: Query<&Window>,
    camera_q: Query<(&Camera, &GlobalTransform)>,
    mug_q: Query<(&GlobalTransform, &Handle<Image>), With<Mug>>,
    images: Res<Assets<Image>>,
) {
    // Only run if left mouse button was just pressed
    if buttons.just_pressed(MouseButton::Left) {
        let window = windows.single();
        let (camera, camera_transform) = camera_q.single();

        if let Some(cursor_pos) = window.cursor_position() {
            // Convert cursor position to world coordinates
            if let Some(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_pos) {
                for (mug_transform, image_handle) in mug_q.iter() {
                    if let Some(image) = images.get(image_handle) {
                        let sprite_size = Vec2::new(image.width() as f32, image.height() as f32);
                        let mug_pos = mug_transform.translation.truncate();

                        let half = sprite_size / 2.0;
                        let min = mug_pos - half;
                        let max = mug_pos + half;

                        if (min.x..=max.x).contains(&world_pos.x) &&
                           (min.y..=max.y).contains(&world_pos.y) {
                            println!("☕ You clicked the mug!");
                        }
                    }
                }
            }
        }
    }
}

💡 This system:

  • Converts screen coordinates to Bevy world coordinates
  • Checks if the click was within the mug’s bounding box

🧩 Step 4: Register the Click System

Finally, add click_mug to your App setup:

.add_systems(Update, click_mug)

Update ensures this system runs every frame, checking for click input.


✅ Summary

TaskDescription
Defined Mug componentMarks the mug sprite
Checked mouse inputResponds only on left click
Calculated hitbox areaCompares cursor to sprite bounds
Printed interaction logClick feedback printed to console

🎉 Result

Now, when you click the mug icon in your game window, the terminal will display:

☕ You clicked the mug!

Next up in Section 4: Animate the Mug or Move It After Click, we’ll use Bevy’s system scheduler to animate or move the mug when clicked — opening the door to interactive gameplay.


🎬 Section 4: Animate or Move the Mug When Clicked

🎯 Goal: Extend your Rust GUI game tutorial by adding basic interactivity. When the user clicks the mug, it moves or animates in response — making the game feel alive.


🧠 Concept: Responding to Input in Bevy

In Bevy, gameplay behavior is composed of systems. We’ll:

  • Detect the mug click (already done in Section 3)
  • Update its position or apply a small animation

🆕 Step 1: Add a Movement Event

We’ll use Bevy’s event system to separate input from actions. This makes the architecture cleaner.

// Define a custom event type
struct MugClickedEvent;

Register the event in main():

.add_event::<MugClickedEvent>()

In the click_mug system, emit the event instead of printing:

event_writer.send(MugClickedEvent);

Update the function signature to include EventWriter:

fn click_mug(
    ...
    mut event_writer: EventWriter<MugClickedEvent>,
)

🏃 Step 2: Move the Mug After Click

Now create a new system to respond to the event:

fn move_mug_on_click(
    mut events: EventReader<MugClickedEvent>,
    mut query: Query<&mut Transform, With<Mug>>,
) {
    for _ in events.read() {
        for mut transform in &mut query {
            transform.translation.y += 50.0; // Move mug up by 50 units
            println!("🎉 Mug moved!");
        }
    }
}

This system reads the event and moves the mug upward when clicked.


📦 Step 3: Register the System

In your main() function, add:

.add_systems(Update, (click_mug, move_mug_on_click))

Bevy will run both systems in order, per frame.


✅ Summary

TaskResult
Defined EventMugClickedEvent to decouple logic
Detected ClickEmit event from click_mug()
Responded to EventMove mug up using Transform component

📌 Bonus: Add a Sound or Animation?

In later sections, you could:

  • Add a sound using bevy_kira_audio
  • Use time-based movement or fade-in/fade-out effects with Timer

🔍 Debug Output Example

☕ You clicked the mug!
🎉 Mug moved!

Next up in Section 5: Add a Score Counter or Game State System, we’ll begin managing game logic: when the player clicks mugs, they’ll gain points — your first step toward full gameplay!


🧮 Section 5: Add a Score Counter and Game State System

🎯 Goal: In this step of the Rust GUI game tutorial, we’ll introduce a simple game state and implement a score counter that increases every time the player clicks the mug icon.


🧠 Why Use a Game State?

Tracking game data like score, health, or inventory requires a resource or component that lives in the ECS world.
In Bevy, we use a Resource to store and globally access the score.


📦 Step 1: Define a Score Resource

Add this struct at the top of main.rs:

#[derive(Resource)]
struct Score(u32);

Register it as a startup resource in your app:

.insert_resource(Score(0))

✏️ Step 2: Update the Mug Click Logic to Add Points

In move_mug_on_click, modify it like this:

fn move_mug_on_click(
    mut events: EventReader<MugClickedEvent>,
    mut query: Query<&mut Transform, With<Mug>>,
    mut score: ResMut<Score>,
) {
    for _ in events.read() {
        for mut transform in &mut query {
            transform.translation.y += 50.0;
        }

        score.0 += 1;
        println!("🎯 Score: {}", score.0);
    }
}

This:

  • Moves the mug upward
  • Increments the score each time it’s clicked
  • Logs the current score

📺 Step 3: (Optional) Display the Score On Screen

We’ll later add UI rendering (Section 6), but for now, print to console:

🎯 Score: 1
🎯 Score: 2

✅ Summary

FeatureResult
Score ResourceTracks player progress
Game State LogicUpdates state with every interaction
Click FeedbackVisually moves mug and logs score

🧠 Concept Recap

  • Resource<T> holds global game-wide data
  • You access it using ResMut<T> to update it in systems
  • This separates data from entities — a key ECS pattern

🖥️ Section 6: Displaying the Score Using Bevy UI – Creating HUD in Rust

🎯 Goal: In this stage of our Rust GUI game tutorial, we’ll learn how to create and update a simple on-screen score counter using Bevy UI. This heads-up display (HUD) will provide real-time feedback to the player whenever the mug is clicked.


🧠 Why Use Bevy UI?

In many beginner Rust game tutorials, developers focus on logic but neglect visual feedback. However, building a 2D GUI in Rust using the Bevy engine’s built-in UI system is straightforward and essential for a better game experience. Whether it’s displaying health, score, or inventory, the HUD (heads-up display) is the player’s primary feedback channel.

👀 In our mug game, we’ll render the score as a floating text in the top-left corner of the game window.


📦 Step 1: Create a Score Text Component

We need a way to update the text dynamically, so we’ll tag the score text with a ScoreText component.

#[derive(Component)]
struct ScoreText;

🧩 Step 2: Spawn a Text UI Element in the Startup System

Bevy provides TextBundle for rendering text on the screen. Add a new system that sets up the UI:

fn setup_score_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn((
        TextBundle::from_section(
            "Score: 0",
            TextStyle {
                font: asset_server.load("fonts/FiraSans-Bold.ttf"), // add this font
                font_size: 30.0,
                color: Color::WHITE,
            },
        )
        .with_style(Style {
            position_type: PositionType::Absolute,
            position: UiRect {
                left: Val::Px(15.0),
                top: Val::Px(15.0),
                ..default()
            },
            ..default()
        }),
        ScoreText,
    ));
}

🗂️ Note: You need a font file!
Download FiraSans or use any .ttf and place it under assets/fonts/.

Your directory should look like this:

assets/
├── mug.png
└── fonts/
    └── FiraSans-Bold.ttf

🔁 Step 3: Update the Score Text When the Mug is Clicked

We’ll now modify our existing move_mug_on_click system to update the text on screen.

fn move_mug_on_click(
    mut events: EventReader<MugClickedEvent>,
    mut query: Query<&mut Transform, With<Mug>>,
    mut score: ResMut<Score>,
    mut text_query: Query<&mut Text, With<ScoreText>>,
) {
    for _ in events.read() {
        for mut transform in &mut query {
            transform.translation.y += 50.0;
        }

        score.0 += 1;

        for mut text in &mut text_query {
            text.sections[0].value = format!("Score: {}", score.0);
        }

        println!("🎯 Score updated: {}", score.0);
    }
}

🧠 This is a perfect example of how to update UI in Bevy: just query for your tagged UI entity, and change the text’s .sections[0].value.


🔧 Step 4: Register All Systems

Make sure your main() includes:

.insert_resource(Score(0))
.add_event::<MugClickedEvent>()
.add_startup_systems((setup_camera, spawn_mug, setup_score_ui))
.add_systems(Update, (click_mug, move_mug_on_click))

✅ What You’ve Learned

FeatureExplanation
TextBundleBevy’s way to show text on screen
ScoreText componentHelps us target only the score text entity
Dynamic UI updateUpdates score with every click
TextStyle & StyleControls font, size, color, and screen position
Using resources in UICombines ECS data with GUI rendering

📸 Example Output

When you run your game, the top-left corner should now show:

Score: 0

Each time you click the mug, it will increase:

Score: 1
Score: 2
Score: 3

All live, on-screen, in true Rust game development style.


🧠 Deep Dive: Why Use Text.sections[0].value?

Bevy UI text is composed of sections for multiple font styles or colors in one line. Even though we use only one section here, this design supports flexibility for multi-style text (like colored numbers or bold labels).


🏁 What’s Next?

Your 2D GUI in Rust now includes:

  • Clickable game objects
  • Moving animations
  • A real-time HUD using Bevy UI

Next in Section 7: Add a Coffee-Making Mechanic (Click + Timer), we’ll introduce a progress bar and simulate brewing a coffee when the mug is clicked repeatedly — moving toward a real gameplay loop.


⏱️ Section 7: Add a Coffee-Making Mechanic with a Timer and Progress Bar

🎯 Goal: In this advanced stage of the Rust GUI game tutorial, we’ll simulate a coffee-making process. When the user clicks the mug a certain number of times, a progress bar fills up. Once complete, the player “brews” a cup of coffee, incrementing a separate count.


☕ Gameplay Concept

  • Each time the mug is clicked, it adds +1 to a “brew progress”
  • Once progress hits a threshold (e.g. 5), it resets and increases a coffee_count
  • A visual progress bar (UI rectangle) reflects brewing status

📦 Step 1: Define Resources and Components

We need to track progress and coffee count:

#[derive(Resource)]
struct BrewProgress {
    current: u8,
    goal: u8,
}

#[derive(Resource)]
struct CoffeeCount(u32);

#[derive(Component)]
struct ProgressBarFill;

Initialize them in main():

.insert_resource(BrewProgress { current: 0, goal: 5 })
.insert_resource(CoffeeCount(0))

🧩 Step 2: Create the Progress Bar UI

We’ll display a progress bar below the score. It consists of:

  • A background node (gray)
  • A fill node (colored, scaled based on progress)
fn setup_progress_bar(mut commands: Commands) {
    commands
        .spawn(NodeBundle {
            style: Style {
                size: Size::new(Val::Px(200.0), Val::Px(20.0)),
                position_type: PositionType::Absolute,
                position: UiRect {
                    left: Val::Px(15.0),
                    top: Val::Px(60.0),
                    ..default()
                },
                ..default()
            },
            background_color: Color::DARK_GRAY.into(),
            ..default()
        })
        .with_children(|parent| {
            parent.spawn((
                NodeBundle {
                    style: Style {
                        size: Size::new(Val::Percent(0.0), Val::Percent(100.0)),
                        ..default()
                    },
                    background_color: Color::ORANGE_RED.into(),
                    ..default()
                },
                ProgressBarFill,
            ));
        });
}

✏️ Step 3: Update Progress on Mug Click

Modify move_mug_on_click:

fn move_mug_on_click(
    mut events: EventReader<MugClickedEvent>,
    mut mug_query: Query<&mut Transform, With<Mug>>,
    mut score: ResMut<Score>,
    mut progress: ResMut<BrewProgress>,
    mut coffee_count: ResMut<CoffeeCount>,
    mut bar_query: Query<&mut Style, With<ProgressBarFill>>,
) {
    for _ in events.read() {
        for mut transform in &mut mug_query {
            transform.translation.y += 10.0;
        }

        // Add progress
        if progress.current < progress.goal {
            progress.current += 1;
        }

        // If finished, make coffee
        if progress.current >= progress.goal {
            coffee_count.0 += 1;
            progress.current = 0;
            println!("☕ Coffee brewed! Total: {}", coffee_count.0);
        }

        // Update progress bar width
        for mut style in &mut bar_query {
            style.size.width = Val::Percent((progress.current as f32 / progress.goal as f32) * 100.0);
        }

        score.0 += 1;
        println!("🎯 Score: {} | Progress: {}", score.0, progress.current);
    }
}

✅ What’s Happening Here?

StepBehavior
Click mugMoves icon, increments brew progress
Progress goal metAdds 1 coffee, resets progress
UI updatesFills progress bar dynamically

🎮 Example Gameplay Loop

  • Click mug 5 times ⏩☕ Coffee brewed!” appears in console
  • Progress bar resets
  • Coffee count increments
  • Score continues to increase

🔍 Bonus Ideas

To enhance the mechanic:

  • Add coffee icons to screen (1 per brew)
  • Add a cooldown (Timer) so clicks must wait
  • Play a sound when brewing completes

🧠 ECS Recap

This mechanic uses:

  • Resources for global mutable state (BrewProgress, CoffeeCount)
  • Component-based UI (ProgressBarFill)
  • System logic for real-time input → state → visual update

✅ Summary Table

FeatureImplementation Detail
Brew LogicBrewProgress resource, goal check
Visual FeedbackUI bar with ProgressBarFill component
Score IntegrationUpdates both score and coffee count in sync

🧺 Section 8: Add Coffee Inventory UI (Icons or Counters)

🎯 Goal: In this part of the Rust GUI game tutorial, we’ll expand your GUI system to visually display the number of coffees brewed. You’ll learn how to use Bevy UI to show either coffee icons or a dynamic number counter — essential for real-time inventory systems in Rust game development.


📦 Concept Overview

So far, your game tracks brewed coffee internally. Now we’ll make that visible to the player in two possible ways:

  1. 📊 Number Counter – “Coffees: 3”
  2. 🧱 Icon-Based Inventory – e.g. 3 coffee cup images side by side

This simple inventory visualization is foundational to building resource systems in Rust GUI games — from shop mechanics to item crafting menus.

If you find this kind of coffee system intriguing, you might also enjoy
👉 Rust Coffee Vending Machine Simulator,
a fun Rust game dev post where coffee recipes are simulated with real-time logic. Definitely worth a read!


🔢 Option 1: Number-Based Coffee Counter

Let’s start with the simplest version: showing the coffee count as text on the screen.

🧩 Step 1: Add a New Component

#[derive(Component)]
struct CoffeeText;

🧩 Step 2: Spawn the UI Text

Place this inside a startup system (setup_coffee_counter_ui):

fn setup_coffee_counter_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn((
        TextBundle::from_section(
            "Coffees: 0",
            TextStyle {
                font: asset_server.load("fonts/FiraSans-Bold.ttf"),
                font_size: 30.0,
                color: Color::GOLD,
            },
        )
        .with_style(Style {
            position_type: PositionType::Absolute,
            position: UiRect {
                right: Val::Px(15.0),
                top: Val::Px(15.0),
                ..default()
            },
            ..default()
        }),
        CoffeeText,
    ));
}

🧩 Step 3: Update When Coffee is Brewed

Extend your move_mug_on_click system to include:

mut coffee_text_q: Query<&mut Text, With<CoffeeText>>,

And inside the system:

for mut coffee_text in &mut coffee_text_q {
    coffee_text.sections[0].value = format!("Coffees: {}", coffee_count.0);
}

🧱 Option 2: Icon-Based Inventory (Advanced)

If you want a graphical display (one coffee cup image per brewed unit):

  • Create a coffee_icon.png image and place it in assets/
  • When a coffee is brewed, spawn a new Sprite UI element as a child node
  • Limit maximum visible icons (e.g. 10)

We’ll expand this part later in Section 9: Grid-Based Coffee Inventory UI


✅ Summary Table

FeatureMethod Used
Count Display (text)TextBundle, CoffeeText component
Inventory TrackingCoffeeCount resource
Real-Time UpdatesECS system responding to brewing events
Bonus LearningUI layout and dynamic label management

🧠 Rust GUI Game Tutorial Deep Dive

By integrating these UI components, your game now resembles a real clicker/tycoon experience. This is a common pattern in Rust 2D game development — reacting to game state changes with on-screen feedback using Bevy UI.


🧱 Section 9: Grid-Based Coffee Inventory with Icons and Limits

🎯 Goal: In this part of the Rust GUI game tutorial, we’ll implement a coffee inventory that visually grows as you brew more coffee. Instead of just showing a number, each coffee brewed will add a small mug icon to a dynamic UI grid.

This is a major upgrade in user experience and prepares your Rust GUI game for more advanced features like inventory management, crafting, or item slots.


📦 Design Goals

  • 🧱 Display one coffee icon per brew
  • ➕ Add new icons dynamically each time a mug is brewed
  • 🎯 Limit the total number of visible icons (e.g., 10 max on screen)
  • 🔄 Reset or cycle inventory (optional advanced step)

☕ Combined with the score and progress bar from earlier sections, this creates a full clicker loop — something you’d see in real-world Rust game development using Bevy engine.


🧩 Step 1: Setup Inventory Container Node

Create a UI container at the bottom of the screen using a horizontal layout:

#[derive(Component)]
struct InventoryRoot;

fn setup_inventory_ui(mut commands: Commands) {
    commands.spawn((
        NodeBundle {
            style: Style {
                size: Size::new(Val::Px(400.0), Val::Px(64.0)),
                position_type: PositionType::Absolute,
                position: UiRect {
                    left: Val::Px(15.0),
                    bottom: Val::Px(15.0),
                    ..default()
                },
                flex_direction: FlexDirection::Row,
                align_items: AlignItems::Center,
                ..default()
            },
            background_color: Color::NONE.into(),
            ..default()
        },
        InventoryRoot,
    ));
}

💡 flex_direction: Row means children are laid out horizontally.


🖼️ Step 2: Load and Spawn Coffee Icon

We’ll dynamically spawn child nodes under InventoryRoot every time a coffee is brewed.

Assume coffee_icon.png is placed inside assets/.

Modify your move_mug_on_click system like this:

fn move_mug_on_click(
    mut events: EventReader<MugClickedEvent>,
    mut mug_q: Query<&mut Transform, With<Mug>>,
    mut score: ResMut<Score>,
    mut progress: ResMut<BrewProgress>,
    mut coffee_count: ResMut<CoffeeCount>,
    mut bar_q: Query<&mut Style, With<ProgressBarFill>>,
    mut text_q: Query<&mut Text, With<CoffeeText>>,
    inventory_q: Query<Entity, With<InventoryRoot>>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    for _ in events.read() {
        for mut mug in &mut mug_q {
            mug.translation.y += 10.0;
        }

        if progress.current < progress.goal {
            progress.current += 1;
        }

        if progress.current >= progress.goal {
            coffee_count.0 += 1;
            progress.current = 0;
            println!("☕ Coffee brewed! Total: {}", coffee_count.0);

            // Spawn coffee icon
            if let Ok(inventory_entity) = inventory_q.get_single() {
                if coffee_count.0 <= 10 {
                    commands.entity(inventory_entity).with_children(|parent| {
                        parent.spawn(ImageBundle {
                            style: Style {
                                size: Size::new(Val::Px(48.0), Val::Px(48.0)),
                                margin: UiRect::all(Val::Px(4.0)),
                                ..default()
                            },
                            image: UiImage::new(asset_server.load("coffee_icon.png")),
                            ..default()
                        });
                    });
                }
            }
        }

        for mut style in &mut bar_q {
            style.size.width = Val::Percent((progress.current as f32 / progress.goal as f32) * 100.0);
        }

        for mut text in &mut text_q {
            text.sections[0].value = format!("Coffees: {}", coffee_count.0);
        }

        score.0 += 1;
    }
}

🔧 Step 3: Asset Preparation

Your assets/ directory should now contain:

assets/
├── mug.png
├── fonts/
│   └── FiraSans-Bold.ttf
└── coffee_icon.png

You can download a simple coffee cup PNG from:


📊 Summary of Features

FeatureImplementation
Grid UIFlex-based horizontal node layout
Coffee trackingUpdates per brewed unit
Coffee iconsDynamically added as children
UI state synchronizationReal-time GUI reflects game logic

🔍 Advanced Options (Future)

  • 🔁 Add a limit (e.g., 10 icons max)
  • ⏱️ Add fade-in animations (with bevy_tweening)
  • 🧺 Use scroll containers or page-based inventories

🧠 ECS Insight

This system shows how powerful Entity-Component-System (ECS) design is in Rust game development:

  • Inventory is just another Node
  • Coffee icons are Entity children
  • The system modifies world state and UI in sync

✅ Final Results (after 5–10 clicks)

  • The mug moves
  • The score updates
  • The progress bar fills
  • Text displays “Coffees: 3”
  • Inventory bar fills with coffee icons

🔊 Section 10: Add Sound Effects and Polish the UX

🎯 Goal: In this final stage of the Rust GUI game tutorial, we’ll enrich the gameplay experience by adding sound effects. Each click and coffee brew will now have an audible response — completing the feel of an interactive, polished 2D Rust GUI game.


💡 Why UX Polish Matters in Rust GUI Games

Game mechanics alone don’t create immersion. The combination of visual feedback, real-time response, and sound design makes your Rust GUI game feel alive.
This section ties everything together — score system, coffee brewing logic, UI inventory, and now sound effects.

🔔 Whether you’re making a simple clicker or a full 2D RPG, understanding how to add game audio in Rust is a crucial step in becoming a complete game developer.


🎵 Step 1: Add the Audio Plugin Dependency

Update your Cargo.toml to include:

[dependencies]
bevy = "0.13"
bevy_kira_audio = "0.18"

This plugin provides high-level support for playing sound in Bevy games. It’s widely used in Rust game development for beginners who want a simple and effective audio API.


📦 Step 2: Load and Insert Audio Files

Place your .ogg or .mp3 files in the assets/audio/ folder:

assets/
├── audio/
│   ├── click.ogg
│   └── brew.ogg

Then register the plugin in main():

use bevy_kira_audio::prelude::*;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, AudioPlugin))
        ...

🧩 Step 3: Load Sounds into the ECS World

In your startup system:

#[derive(Resource)]
struct GameAudio {
    click: Handle<AudioSource>,
    brew: Handle<AudioSource>,
}

fn load_sounds(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.insert_resource(GameAudio {
        click: asset_server.load("audio/click.ogg"),
        brew: asset_server.load("audio/brew.ogg"),
    });
}

🧪 Step 4: Play Sound on Mug Click or Coffee Brew

Update move_mug_on_click to use the audio system:

fn move_mug_on_click(
    ...
    audio: Res<Audio>,
    game_audio: Res<GameAudio>,
) {
    for _ in events.read() {
        ...
        audio.play(game_audio.click.clone());

        if progress.current >= progress.goal {
            coffee_count.0 += 1;
            audio.play(game_audio.brew.clone());
            ...
        }
    }
}

Now every mug click will play a satisfying click sound, and a brew sound will play when coffee is made.


✅ Final Game Loop (Fully Polished)

  • 🎯 Rust GUI game tutorial journey complete
  • 👆 Mug clicks move icon + update progress
  • 🧠 Logic triggers score and brew state
  • 🧺 Coffee inventory fills with icons
  • 🖼️ GUI text and bar update in real-time
  • 🔊 Audio feedback brings it all to life

🔊 Tips for Audio UX Polish

TipDescription
Normalize volumeAvoid too loud/quiet sounds
Add variationUse 2–3 alternate click sounds randomly
Add delay or cooldown logicPrevent overlapping spam sounds
Consider spatial audioFor large games with map-based positioning

🎓 Wrap-Up: Your First Rust GUI Game Tutorial Completed

You’ve built a fully functional 2D game UI system with Rust and Bevy:

  • ✅ Clickable gameplay loop
  • ✅ Real-time progress & state
  • ✅ Dynamic UI (score, inventory, bar)
  • ✅ Audio effects for deeper interaction
  • ✅ Expandable ECS structure for future levels

This concludes your beginner-to-intermediate level Rust GUI game tutorial. You’ve laid the foundation for more complex mechanics like drag-and-drop UI, inventory merging, shop systems, or even crafting logic.

Want to dive deeper? Try adding:

  • Save/load with serde
  • Shop system using gold
  • Coffee upgrades that boost brew speed


🧩 Final Thoughts – Where to Go Next?

Congratulations! 🎉 You’ve completed a full Rust GUI game tutorial using the Bevy engine in Rust — one of the most beginner-friendly and powerful frameworks for making 2D games in Rust today.

Over the course of this project, you didn’t just build a toy app — you built a foundational clicker-style game with:

  • ✅ Interactive GUI components
  • ✅ Real-time state management
  • ✅ Sound integration
  • ✅ Dynamic UI layout
  • ✅ Icon-based inventory system

If you’re looking for a beginner Rust game tutorial that teaches you actual game design and ECS structure, this is it — a complete, scalable, and expandable base.


🚀 What You’ve Learned

SkillDescription
Bevy engine RustGame loop, ECS structure, UI system
Rust 2D gameCreated using sprites, 2D camera, UI overlays
Rust game development for beginnersA step-by-step path from logic to polish
GUI game Rust exampleFully playable with interaction, UI, and audio
Learn Rust with gameHands-on with Bevy ECS, components, resources, event systems
How to make a game in RustFrom setup to complete GUI gameplay

🔗 Want to Learn More?

If you’re feeling ambitious or curious about the backend logic of multiplayer-style games, or how to implement more complex simulations using text-based systems in Rust, check out these follow-up guides:

These articles are great companions to this GUI-based project — they tackle different game types, but share the same core Rust foundations.


You didn’t just learn Rust with a game — you’ve experienced firsthand how to make a game in Rust from concept to execution.

Happy coding! ☕🦀
If you’d like help exporting your game to web (WASM), adding animations, or even publishing it, let me know — we can build on this momentum together.

❓ FAQ – Rust GUI Game Tutorial

1. Do I need prior game development experience to follow this Rust GUI game tutorial?

No. This tutorial is designed for Rust beginners and also serves as a solid beginner Rust game tutorial with step-by-step guidance.


2. What is Bevy and why is it used here?

Bevy is a modern game engine built in Rust. It offers a powerful ECS (Entity Component System), real-time rendering, input handling, and a flexible UI system — making it perfect for Rust 2D game development.


3. Can I build this GUI game in Rust on macOS or Linux?

Yes! Rust and Bevy are fully cross-platform. This GUI game Rust example works on Windows, macOS, and Linux.


4. What’s the difference between a GUI-based Rust game and a MUD game?

A GUI game uses visual sprites, text, and graphics. A MUD (Multi-User Dungeon) is typically text-based. For MUDs, check out this terminal-based Rust MUD tutorial.


5. Can I replace the mug icon and use different graphics?

Absolutely. Just place your .png image into the assets/ folder and update the asset path in your code. Bevy makes asset swapping easy.


6. Can I add sound effects even if I’ve never used audio in programming before?

Yes. The tutorial uses bevy_kira_audio, which makes it beginner-friendly to add click and brew sounds with one line of code.


7. How do I deploy or share my Rust GUI game with others?

You can compile it to a native .exe, .app, or .elf file. Or, you can export it to WebAssembly (WASM) for browser-based play using wasm-pack.


8. What should I learn next after completing this Rust GUI game tutorial?

You could explore:


9. Is Bevy the only way to make GUI games in Rust?

No. Other options include ggez, macroquad, and SDL2. However, Bevy is the most modern and scalable option for serious Rust game development for beginners.


10. Can I use Bevy to make a full commercial game?

Yes. Bevy is open-source and MIT licensed. Many indie devs are using it to prototype or even release full games.


11. How do I customize the UI font and colors?

Just replace the .ttf file in your assets folder and change the TextStyle color/font in the code. Bevy supports complete UI theming.


12. How large can my game get before performance is affected?

Bevy is very performant and written in native Rust. Even hundreds of entities (like icons, enemies, bullets) are handled efficiently due to ECS.


13. Can I build this tutorial into a full clicker/idle game?

Yes! This is a perfect base for building a clicker, idle tycoon, or simulation game. Add timers, offline income, upgrades, and you’re there.

🦀 Rust Programming Language


🎮 Bevy Game Engine


🔊 bevy_kira_audio (Sound Plugin)


🎨 Free Game Assets

Leave a Reply

Your email address will not be published. Required fields are marked *