Resources and errors
In this chapter we will refactor our code without adding any new functionality
to the game. We do this to build a foundation to be able to add a
loading screen during the loading of resources in the web version. We also
want to be able to refactor all the drawing to be done by the structs. Finally
we will be able to move code away from our main
function which is starting
to get a bit hard to follow.
Implementation
Resources struct
We start by creating a new struct called Resources
that will contain all the
files we load from the file system. Add it above the main
function. The
struct will have a field for every asset loaded.
struct Resources {
ship_texture: Texture2D,
bullet_texture: Texture2D,
explosion_texture: Texture2D,
enemy_small_texture: Texture2D,
theme_music: Sound,
sound_explosion: Sound,
sound_laser: Sound,
ui_skin: Skin,
}
Resources impl
Directly below the Resources
struct we’ll add an implementation block for it.
To begin with it will only contain a new
method that loads all the files and
returns an instance of the struct if everything went as expected. We’ll reuse
the code that used to be in the main
function to load all the files.
We’ll also store the UI Skin
as a resource so we won’t have to return the
font and all the images used for it.
The difference in the code is that we’ve replaced all the unwrap()
and
expect()
calls to use the ?
operator instead. Using this the error will be
returned instead of exiting the program. This means we will be able to handle
the error in a single place in our main
function if we want to. The error
message is an enum of the type macroquad::Error
.
The errors available in Macroquad are documented in macroquad::Error.
impl Resources {
async fn new() -> Result<Resources, macroquad::Error> {
let ship_texture: Texture2D = load_texture("ship.png").await?;
ship_texture.set_filter(FilterMode::Nearest);
let bullet_texture: Texture2D = load_texture("laser-bolts.png").await?;
bullet_texture.set_filter(FilterMode::Nearest);
let explosion_texture: Texture2D = load_texture("explosion.png").await?;
explosion_texture.set_filter(FilterMode::Nearest);
let enemy_small_texture: Texture2D = load_texture("enemy-small.png").await?;
enemy_small_texture.set_filter(FilterMode::Nearest);
build_textures_atlas();
let theme_music = load_sound("8bit-spaceshooter.ogg").await?;
let sound_explosion = load_sound("explosion.wav").await?;
let sound_laser = load_sound("laser.wav").await?;
let window_background = load_image("window_background.png").await?;
let button_background = load_image("button_background.png").await?;
let button_clicked_background = load_image("button_clicked_background.png").await?;
let font = load_file("atari_games.ttf").await?;
let window_style = root_ui()
.style_builder()
.background(window_background)
.background_margin(RectOffset::new(32.0, 76.0, 44.0, 20.0))
.margin(RectOffset::new(0.0, -40.0, 0.0, 0.0))
.build();
let button_style = root_ui()
.style_builder()
.background(button_background)
.background_clicked(button_clicked_background)
.background_margin(RectOffset::new(16.0, 16.0, 16.0, 16.0))
.margin(RectOffset::new(16.0, 0.0, -8.0, -8.0))
.font(&font)?
.text_color(WHITE)
.font_size(64)
.build();
let label_style = root_ui()
.style_builder()
.font(&font)?
.text_color(WHITE)
.font_size(28)
.build();
let ui_skin = Skin {
window_style,
button_style,
label_style,
..root_ui().default_skin()
};
Ok(Resources {
ship_texture,
bullet_texture,
explosion_texture,
enemy_small_texture,
theme_music,
sound_explosion,
sound_laser,
ui_skin,
})
}
}
Returning errors
To keep things as simple as possible we’ll let our main
function return a
result that may be an error. This means we can use the ?
operator in the
main
function as well. If the main
function returns an error, the game
will quit and the error message will be printed on the console.
The standard return value for the main
function is ()
, which is the Rust
unit type that can be used if no value will be returned. Before when the
function didn’t specify a return value, this was still returned implicitly.
If the last expression in a function ends with a semi colon (;
) the return
value will be skipped and ()
is returned instead.
#[macroquad::main("My game")]
async fn main() -> Result<(), macroquad::Error> {
If you want to know how the Rust unit type works you can find more information in the Rust unit documentation.
Remove unwrap()
When loading the material for the shader we used to use the method unwrap()
which we will now change to the ?
operator to return any error instead. This
change is in the last line of the code below.
let material = load_material(
ShaderSource::Glsl {
vertex: VERTEX_SHADER,
fragment: FRAGMENT_SHADER,
},
MaterialParams {
uniforms: vec![
("iResolution".to_owned(), UniformType::Float2),
("direction_modifier".to_owned(), UniformType::Float1),
],
..Default::default()
},
)?;
Load resources
We’ve finally reached the most interesting part of this chapter. It’s time to
change the code that loads file assets to instead instantiate our Resources
struct. We add the result to the resources
variable that we can use later
when we need to use a resource.
Note that we use await
after the new()
method as it is async. We also use
the ?
operator to bubble up any errors.
set_pc_assets_folder("assets");
let resources = Resources::new().await?;
Update resource usages
Now that we have loaded all the assets with the Resources
struct we need to
update all the places that uses a resource so that they retrieve the asset
from it instead. We basically just add resources.
in front of every resource
name.
Game music
play_sound(
&resources.theme_music,
PlaySoundParams {
looped: true,
volume: 1.,
},
);
User interface
Now that we’ve saved the UI Skin
in our Resources
struct we only need to
activate it using root_ui().push_skin()
. We can replace all the lines
that builds the UI with a single line.
root_ui().push_skin(&resources.ui_skin);
let window_size = vec2(370.0, 320.0);
Laser sound
The laser sound needs to use the resources
variable.
if is_key_pressed(KeyCode::Space) {
bullets.push(Shape {
x: circle.x,
y: circle.y - 24.0,
speed: circle.speed * 2.0,
size: 32.0,
collided: false,
});
play_sound_once(&resources.sound_laser);
}
Explosions
We need to update both the texture and the sound for the explosions.
explosions.push((
Emitter::new(EmitterConfig {
amount: square.size.round() as u32 * 4,
texture: Some(resources.explosion_texture.clone()),
..particle_explosion()
}),
vec2(square.x, square.y),
));
play_sound_once(&resources.sound_explosion);
Bullets
Update the call to drawing bullets to use the texture from resources
.
for bullet in &bullets {
draw_texture_ex(
&resources.bullet_texture,
bullet.x - bullet.size / 2.0,
bullet.y - bullet.size / 2.0,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(bullet.size, bullet.size)),
source: Some(bullet_frame.source_rect),
..Default::default()
},
);
}
Spaceship
The spaceship also needs to use the texture from resources
.
let ship_frame = ship_sprite.frame();
draw_texture_ex(
&resources.ship_texture,
circle.x - ship_frame.dest_size.x,
circle.y - ship_frame.dest_size.y,
WHITE,
DrawTextureParams {
dest_size: Some(ship_frame.dest_size * 2.0),
source: Some(ship_frame.source_rect),
..Default::default()
},
);
Enemies
When the enemies are drawn, we need to add resources
as well.
for square in &squares {
draw_texture_ex(
&resources.enemy_small_texture,
square.x - square.size / 2.0,
square.y - square.size / 2.0,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(square.size, square.size)),
source: Some(enemy_frame.source_rect),
..Default::default()
},
);
}
That’s everything that needs to be changed this time. In this chapter we’ve created a struct that contains all the loaded assets that we use when drawing textures and playing sounds.
Instead of just exiting the game when encountering an error you could try to
display the error message on the screen using the draw_text()
function of
Macroquad. Remember that the program will then need to keep on running and do
nothing but displaying the text.
Try the game
The game should work exactly like before.
Sometimes the cargo dependencies can become out of sync. Some users have
experienced this in this chapter. The symptoms are that the buttons in the
main menu starts to “glitch” and it requires multiple clicks to press the
buttons. A workaround for this issue is to rebuild all the dependencies using
cargo clean
.