
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
Step | Description |
---|---|
Project Init | cargo new mug_game |
Bevy Setup | Add bevy = "0.13" in Cargo.toml |
GUI Window | Bevy’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)
Part | What It Does |
---|---|
AssetServer::load | Loads image from assets/ folder as texture handle |
SpriteBundle | A Bevy struct that displays an image |
Transform | Controls position (x, y, z) on screen |
Camera2dBundle | Required to display anything in a 2D space |
Note: Asset loading in Bevy is asynchronous, but Bevy handles that behind the scenes.
Summary
Task | Result |
---|---|
Created assets/ | Organized sprite resources |
Loaded mug.png | Displayed it on window |
Used SpriteBundle | Positioned 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
Task | Description |
---|---|
Defined Mug component | Marks the mug sprite |
Checked mouse input | Responds only on left click |
Calculated hitbox area | Compares cursor to sprite bounds |
Printed interaction log | Click 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
Task | Result |
---|---|
Defined Event | MugClickedEvent to decouple logic |
Detected Click | Emit event from click_mug() |
Responded to Event | Move 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
Feature | Result |
---|---|
Score Resource | Tracks player progress |
Game State Logic | Updates state with every interaction |
Click Feedback | Visually 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
Feature | Explanation |
---|---|
TextBundle | Bevy’s way to show text on screen |
ScoreText component | Helps us target only the score text entity |
Dynamic UI update | Updates score with every click |
TextStyle & Style | Controls font, size, color, and screen position |
Using resources in UI | Combines 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?
Step | Behavior |
---|---|
Click mug | Moves icon, increments brew progress |
Progress goal met | Adds 1 coffee, resets progress |
UI updates | Fills 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
Feature | Implementation Detail |
---|---|
Brew Logic | BrewProgress resource, goal check |
Visual Feedback | UI bar with ProgressBarFill component |
Score Integration | Updates 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:
Number Counter – “Coffees: 3”
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 inassets/
- 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
Feature | Method Used |
---|---|
Count Display (text) | TextBundle , CoffeeText component |
Inventory Tracking | CoffeeCount resource |
Real-Time Updates | ECS system responding to brewing events |
Bonus Learning | UI 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
Feature | Implementation |
---|---|
Grid UI | Flex-based horizontal node layout |
Coffee tracking | Updates per brewed unit |
Coffee icons | Dynamically added as children |
UI state synchronization | Real-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
Tip | Description |
---|---|
Normalize volume | Avoid too loud/quiet sounds |
Add variation | Use 2–3 alternate click sounds randomly |
Add delay or cooldown logic | Prevent overlapping spam sounds |
Consider spatial audio | For 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
Skill | Description |
---|---|
Bevy engine Rust | Game loop, ECS structure, UI system |
Rust 2D game | Created using sprites, 2D camera, UI overlays |
Rust game development for beginners | A step-by-step path from logic to polish |
GUI game Rust example | Fully playable with interaction, UI, and audio |
Learn Rust with game | Hands-on with Bevy ECS, components, resources, event systems |
How to make a game in Rust | From 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:
Rust MUD Game Tutorial – Map & World System
→ Learn how to generate and manage structured world data using structs and enums.Rust MUD Game Tutorial – Monsters, Turn-Based Combat & Event Handling
→ Go deeper into enemy logic, combat simulations, and state transitions.Rust MUD Game Tutorial Part 3 – Full Game Systems
→ Discover how to structure a large-scale Rust terminal game using modular ECS design.
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:
- Crafting systems
- Shop menus
- Save/load with
serde
- Or try the MUD game series:
World System
Combat & Monsters
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.
Official External Links – Rust GUI Game Tutorial
Rust Programming Language
Official site: https://www.rust-lang.org
→ Learn more about Rust, install the toolchain, and explore the language guide.
Bevy Game Engine
Bevy official site: https://bevyengine.org
→ The official homepage for the Bevy engine — documentation, roadmap, and examples.Bevy API Docs: https://docs.rs/bevy/latest/bevy
→ Complete API documentation for all Bevy features, including UI, ECS, and rendering.
bevy_kira_audio (Sound Plugin)
Crate page: https://crates.io/crates/bevy_kira_audio
→ Official plugin for playing audio in Bevy with simple commands.Docs: https://docs.rs/bevy_kira_audio/latest/bevy_kira_audio/
→ Detailed usage instructions and examples for adding sound to Bevy games.
Free Game Assets
Kenney Game Assets: https://kenney.nl/assets
→ High-quality free assets including UI elements, icons, and game sprites.OpenGameArt.org: https://opengameart.org
→ A vast repository of open-licensed audio, textures, icons, and more.Itch.io Game Assets: https://itch.io/game-assets/free
→ Community-driven, free and premium asset packs useful for 2D games.