Resurser och felmeddelanden
I detta kapitel kommer vi refaktorisera vår kod utan att lägga någon direkt
funktionalitet i spelet. Detta gör vi framförallt för att bygga en grund för
att senare kunna lägga till en laddningsskärm som visar att resurserna håller
på att laddas in. Dessutom kommer vi kunna refaktorisera alla draw-anrop så
att de görs av dom structar som ritas ut. Vi får också fördelen att vi kan
flytta bort kod från vår main
-funktion som börjar bli svår att överblicka.
Implementering
Resources struct
Till att börja med skapar vi en ny struct som vi kallar Resources
som kommer
innehålla alla filer vi laddar in från filsystemet. Lägg in den ovanför
main
-funktionen. Structen har ett fält för varje resurs vi laddar in.
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
Direkt under Resources
-structen skapar vi ett implementationsblock för den.
Till att börja med kommer den bara innehålla en new
-funktion som laddar in
alla filer och returnerar en instans av structen om allt går bra. Här använder
vi i stort sett samma kod som tidigare låg i main
-funktionen för att ladda
in alla filer.
Vi sparar även hela UI-skinnet som en resurs så vi inte behöver returnera alla
separata bilder och fonten. Notera att vi även här har bytt ut unwrap()
efter font()
-funktionerna till att använda ?
-operatorn.
Skillnaden är att vi har bytt ut alla unwrap()
och expect()
till ?
-operatorn. Med hjälp av denna kommer felmeddelandet returneras
istället för att avsluta programmet. Det gör att vi kan hantera felmeddelandet
på ett ställe i vår main
-funktion om vi vill. Felmeddelandet är en enum av
typen macroquad::Error
.
Vilka felmeddelanden som finns i Macroquad finns beskrivet i dokumentationen för 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,
})
}
}
Returnera fel
För enkelhetens skull kommer vi låta vår main
-funktion returnera ett
resultat som kan vara ett felmeddelande. Det gör att vi kan använda
?
-operatorn även i main
-funktionen. Om main
-funktionen returnerar ett
felmeddelande kommer applikationen att avslutas och felmeddelandet skrivas ut
på konsollen.
Det vanliga returvärdet i funktionen är ()
som är Rusts “unit typ” som kan
användas om inget värde ska returneras. När funktionen tidigare inte hade
något explicit returvärde så returnerades detta istället implicit.
Om det sista uttrycket i en funktion avslutas med ett semikolon ;
så slängs
dess returvärde bort och ()
returneras istället.
#[macroquad::main("My game")]
async fn main() -> Result<(), macroquad::Error> {
Om du undrar hur Rusts unit-typ fungerar så hittar du lite mer information i Rusts dokumentation av unit.
Ta bort unwrap()
Vid inladdningen av materialet för shadern använde vi tidigare metoden
unwrap()
som vi byter ut mot ?
-operatorn för att returnera eventuella fel
istället. Ändringen sker på sista raden i kodexemplet.
let material = load_material(
ShaderSource::Glsl {
vertex: VERTEX_SHADER,
fragment: FRAGMENT_SHADER,
},
MaterialParams {
uniforms: vec![
UniformDesc::new("iResolution", UniformType::Float2),
UniformDesc::new("direction_modifier", UniformType::Float1),
],
..Default::default()
},
)?;
Ladda resurser
Nu kommer vi till den intressanta biten i detta avsnitt. Det är dags att byta
ut all kod som laddar in filresurser till att instantiera vår Resources
struct istället. Resultat lägger vi variabeln resources
som vi senare kommer
använda när vi vill komma åt en resurs.
Notera att den använder sig av await
metoden som kör new
-metoden som är
async
. Vi använder oss även här av ?
-operatorn för att direkt returnera om
vi får ett fel.
set_pc_assets_folder("assets");
let resources = Resources::new().await?;
Uppdatera resursanvändningar
Nu när vi har laddat in resurserna med Resources
så behöver vi uppdatera
alla ställen som använder en resurs så att de läser från resources
-variabeln
istället för direkt från en variabel. Vi lägger helt enkelt till resources.
framför alla resursnamn.
Spelmusik
play_sound(
&resources.theme_music,
PlaySoundParams {
looped: true,
volume: 1.,
},
);
Gränssnittet
Nu när vi har sparat gränssnittets utssende i vår Resources
struct räcker
det med att sätta det som aktivt skin med root_ui().push_skin()
. Här kan vi
alltså ta bort alla rader som bygger upp utseendet med en enda rad.
root_ui().push_skin(&resources.ui_skin);
let window_size = vec2(370.0, 320.0);
Laserljud
Laserljudet behöver använda resources
-structen.
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);
}
Explosioner
För explosionerna måste vi uppdatera referensen till både texturen och explosionsljudet.
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);
Kulor
Uppdatera utritningen av kulorna till att använda texturen från 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()
},
);
}
Skeppet
Skeppet behöver också använda texturen från 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()
},
);
Fiender
När fiendera ritas ut behöver resources
läggas till i referensen av texturen.
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()
},
);
}
Det ska vara allt som behöver ändras den här gången. Vi har nu skapat en struct som innehåller alla resurser som vi kan använda oss av när vi ritar ut texturer och spelar upp ljud.
Istället för att bara avsluta applikationen så kan du pröva att skriva ut
felmeddelandet på skärmen med Macroquads draw_text
funktion. Tänk på att
programmet då måste fortsätta köra utan att göra något annat än att rita ut
text.
Prova spelet
Spelet ska se ut precis som förut.