Skip to content

Commit d36eb6c

Browse files
bors[bot]zicklag
andauthored
Merge #164
164: Implement Load Progress Tracking r=zicklag a=zicklag Loading screen still needs to be implemented. This PR implements a solution for tracking the load progress of game assets. This works towards #161, but it doesn't implement the UI yet, just the load progress tracking. It allows us to get the load progress for both `GameMeta` and `LevelMeta` and adds logic to wait until game and level assets are loaded before starting the game/level. This time period should be replaced with a loading screen in a subsequent PR. #### Why This is So Complicated Tracking the loading progress for the game is not trivial due to the fact that we have _several_ different kinds of asset and handles referenced throughout the `GameMeta` struct at many different levels of struct nesting. My initial thought was to add a simple function where I simply manually return all the `HandleId`s in the various different parts of the `GameMeta` struct in a vector. This instantly became very difficult because of how spread out between types the handles are. There wasn't a good way to know if I had gotten all the handles, and if we added more handles later, we would have to remember to update the list which would be horrible to manage and would almost surely become out of date. Instead, in this PR I added a `HasLoadProgress` trait that we use a custom derive macro to derive on `GameMeta` and all of the structs that it contains. This ensures that _all_ of the handles contained in the `GameMeta` struct will be properly evaluated when checking load progress and it doesn't require difficult manual listing of handles. The cost is that we write a macro and add a macro crate to our codebase, but this took me less than a day, and it is a relatively simple macro. I think it turned out rather elegant and I don't know of any other reasonable alternative at this point, but let me know if you have other thoughts! Co-authored-by: Zicklag <zicklag@katharostech.com>
2 parents 8ea2453 + ecfd640 commit d36eb6c

11 files changed

Lines changed: 349 additions & 41 deletions

File tree

‎Cargo.lock‎

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ authors = ["The Fish Fight Game & Spicy Lobster Developers"]
66
license = "MIT OR Apache-2.0"
77
edition = "2021"
88

9+
[workspace]
10+
members = [
11+
".",
12+
"macros"
13+
]
14+
915
[dependencies]
16+
punchy_macros = { path = "./macros" }
17+
1018
anyhow = "1.0.58"
1119
bevy = "0.7.0"
1220
bevy-parallax = "0.1.2"

‎macros/Cargo.toml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "punchy_macros"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
proc-macro = true
8+
9+
[dependencies]
10+
syn = "1.0.98"
11+
quote = "1.0.20"
12+
proc-macro2 = "1.0.42"

‎macros/src/lib.rs‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::TokenStream as TokenStream2;
3+
use quote::{quote, quote_spanned};
4+
use syn::{parse_quote, spanned::Spanned};
5+
6+
/// Derive macro for `HasLoadProgress`
7+
///
8+
/// May be used to implement `HasLoadProgress` on structs where all fields implement
9+
/// `HasLoadProgress`.
10+
///
11+
/// Fields not implementing `HasLoadProgress` may be skipped with `#[has_load_progress(none)]` added
12+
/// to the field.
13+
///
14+
/// `#[has_load_progress(none)]` may also be added to the struct itself to use the default
15+
/// implementation of `HasLoadProgress` which returns no progress and nothing to load.
16+
///
17+
/// `#[has_load_progress(none)]` could also be added to enums to derive the default implementation
18+
/// with no load progress, but cannot be added to enum variants.
19+
#[proc_macro_derive(HasLoadProgress, attributes(has_load_progress))]
20+
pub fn has_load_progress(input: TokenStream) -> TokenStream {
21+
let input = syn::parse(input).unwrap();
22+
23+
impl_has_load_progress(&input).into()
24+
}
25+
26+
fn impl_has_load_progress(input: &syn::DeriveInput) -> TokenStream2 {
27+
// The attribute that may be added to skip fields or use the default implementation.
28+
let no_load_attr: syn::Attribute = parse_quote! {
29+
#[has_load_progress(none)]
30+
};
31+
32+
let item_ident = &input.ident;
33+
let mut impl_function_body = quote! {};
34+
35+
// Check for `#[has_load_progress(none)]` on the item itself
36+
let mut skip_all_fields = false;
37+
for attr in &input.attrs {
38+
if attr.path == parse_quote!(has_load_progress) {
39+
if attr == &no_load_attr {
40+
skip_all_fields = true;
41+
} else {
42+
return quote_spanned!(attr.span() =>
43+
compile_error!("Attribute must be `#[has_load_progress(none)]` if specified");
44+
);
45+
}
46+
}
47+
}
48+
49+
// If we are skipping all fields
50+
if skip_all_fields {
51+
impl_function_body = quote! {
52+
crate::loading::progress::LoadProgress::default()
53+
};
54+
55+
// If we should process struct fields
56+
} else {
57+
// Parse the struct
58+
let in_struct = match &input.data {
59+
syn::Data::Struct(s) => s,
60+
syn::Data::Enum(_) | syn::Data::Union(_) => {
61+
return quote_spanned! { input.ident.span() =>
62+
compile_error!("You may only derive HasLoadProgress on structs");
63+
};
64+
}
65+
};
66+
67+
// Start a list of the progresses for each field
68+
let mut progresses = Vec::new();
69+
'field: for field in &in_struct.fields {
70+
// Skip this field if it has `#[has_load_progress(none)]`
71+
for attr in &field.attrs {
72+
if attr.path == parse_quote!(has_load_progress) {
73+
if attr == &no_load_attr {
74+
continue 'field;
75+
} else {
76+
impl_function_body = quote_spanned! { attr.span() =>
77+
compile_error!("Attribute be `#[has_load_progress(none)]` if specified");
78+
}
79+
}
80+
}
81+
}
82+
83+
// Add this fields load progress to the list of progresses
84+
let field_ident = field.ident.as_ref().expect("Field ident");
85+
progresses.push(quote_spanned! { field_ident.span() =>
86+
crate::loading::progress::HasLoadProgress::load_progress(
87+
&self.#field_ident,
88+
loading_resources
89+
)
90+
})
91+
}
92+
93+
// Retrun the merged progress result
94+
impl_function_body = quote! {
95+
#impl_function_body
96+
crate::loading::progress::LoadProgress::merged([ #( #progresses),* ])
97+
};
98+
}
99+
100+
// Fill out rest of impl block
101+
quote! {
102+
impl crate::loading::progress::HasLoadProgress for #item_ident {
103+
fn load_progress(
104+
&self,
105+
loading_resources: &crate::loading::progress::LoadingResources
106+
) -> crate::loading::progress::LoadProgress {
107+
#impl_function_body
108+
}
109+
}
110+
}
111+
}

‎src/assets.rs‎

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,33 @@ impl AssetLoader for GameMetaLoader {
105105
meta.main_menu.background_image.image_handle = main_menu_background;
106106
dependencies.push(main_menu_background_path);
107107

108-
// Load the music
108+
// Load UI border images
109+
let mut load_border_image = |border: &mut BorderImageMeta| {
110+
let (path, handle) = get_relative_asset(load_context, &self_path, &border.image);
111+
dependencies.push(path);
112+
border.handle = handle;
113+
};
114+
load_border_image(&mut meta.ui_theme.hud.portrait_frame);
115+
load_border_image(&mut meta.ui_theme.panel.border);
116+
load_border_image(&mut meta.ui_theme.hud.lifebar.background_image);
117+
load_border_image(&mut meta.ui_theme.hud.lifebar.progress_image);
118+
for button in meta.ui_theme.button_styles.values_mut() {
119+
load_border_image(&mut button.borders.default);
120+
if let Some(border) = &mut button.borders.clicked {
121+
load_border_image(border);
122+
}
123+
if let Some(border) = &mut button.borders.focused {
124+
load_border_image(border);
125+
}
126+
}
109127

128+
// Load the music
110129
let (music_path, music_handle) =
111130
get_relative_asset(load_context, &self_path, &meta.main_menu.music);
112131
meta.main_menu.music_handle = music_handle;
113132
dependencies.push(music_path);
114133

115134
// Load UI fonts
116-
117135
for (font_name, font_relative_path) in &meta.ui_theme.font_families {
118136
let (font_path, font_handle) =
119137
get_relative_asset(load_context, &self_path, font_relative_path);
@@ -150,15 +168,6 @@ impl AssetLoader for LevelMetaLoader {
150168

151169
let self_path = load_context.path();
152170

153-
// Convert all parallax paths to relative asset paths so that the convention matches the
154-
// rest of the paths used by the asset loaders.
155-
for layer in &mut meta.parallax_background.layers {
156-
layer.path = relative_asset_path(self_path, &layer.path)
157-
.to_str()
158-
.unwrap()
159-
.to_owned();
160-
}
161-
162171
let mut dependencies = Vec::new();
163172

164173
// Load the players
@@ -189,8 +198,24 @@ impl AssetLoader for LevelMetaLoader {
189198
item.item_handle = item_handle;
190199
}
191200

192-
// Load the music
201+
// Load parallax background layers
202+
for layer in &mut meta.parallax_background.layers {
203+
let (path, handle) = get_relative_asset(load_context, self_path, &layer.path);
204+
205+
// Update the layer path to use an absolute path so that it matches the conventione
206+
// used by the bevy_parallax_background plugin.
207+
layer.path = path
208+
.path()
209+
.as_os_str()
210+
.to_str()
211+
.expect("utf8-filename")
212+
.to_string();
193213

214+
layer.image_handle = handle;
215+
dependencies.push(path);
216+
}
217+
218+
// Load the music
194219
let (music_path, music_handle) =
195220
get_relative_asset(load_context, self_path, &meta.music);
196221
meta.music_handle = music_handle;

‎src/loading.rs‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ use leafwing_input_manager::{
3131
InputManagerBundle,
3232
};
3333

34+
use progress::{HasLoadProgress, LoadingResources};
35+
pub mod progress;
36+
3437
pub struct LoadingPlugin;
3538

3639
impl Plugin for LoadingPlugin {
@@ -74,6 +77,7 @@ impl Plugin for LoadingPlugin {
7477
}
7578
}
7679

80+
/// System param used to load and hot reload the game
7781
#[derive(SystemParam)]
7882
pub struct GameLoader<'w, 's> {
7983
skip_next_asset_update_event: Local<'s, bool>,
@@ -82,8 +86,8 @@ pub struct GameLoader<'w, 's> {
8286
game_handle: Res<'w, Handle<GameMeta>>,
8387
assets: ResMut<'w, Assets<GameMeta>>,
8488
egui_ctx: ResMut<'w, EguiContext>,
85-
asset_server: Res<'w, AssetServer>,
8689
events: EventReader<'w, 's, AssetEvent<GameMeta>>,
90+
loading_resources: LoadingResources<'w, 's>,
8791
}
8892

8993
impl<'w, 's> GameLoader<'w, 's> {
@@ -106,12 +110,22 @@ impl<'w, 's> GameLoader<'w, 's> {
106110
game_handle,
107111
mut assets,
108112
mut egui_ctx,
109-
asset_server,
110113
..
111114
} = self;
112115

113116
if let Some(game) = assets.get_mut(game_handle.clone_weak()) {
114-
debug!("Loaded game");
117+
// Track load progress
118+
let load_progress = game.load_progress(&self.loading_resources);
119+
debug!(
120+
%load_progress,
121+
"Loading game assets: {:.2}% ",
122+
load_progress.as_percent()
123+
);
124+
125+
// Wait until assets are loaded to start game
126+
if load_progress.as_percent() < 1.0 {
127+
return;
128+
}
115129

116130
// Hot reload preparation
117131
if is_hot_reload {
@@ -164,11 +178,10 @@ impl<'w, 's> GameLoader<'w, 's> {
164178

165179
// Helper to load border images
166180
let mut load_border_image = |border: &mut BorderImageMeta| {
167-
border.handle = asset_server.load(&border.image);
168181
border.egui_texture = egui_ctx.add_image(border.handle.clone_weak());
169182
};
170183

171-
// Load border images
184+
// Add Border images to egui context
172185
load_border_image(&mut game.ui_theme.hud.portrait_frame);
173186
load_border_image(&mut game.ui_theme.panel.border);
174187
load_border_image(&mut game.ui_theme.hud.lifebar.background_image);
@@ -310,9 +323,22 @@ fn load_level(
310323
game: Res<GameMeta>,
311324
windows: Res<Windows>,
312325
mut storage: ResMut<Storage>,
326+
loading_resources: LoadingResources,
313327
) {
314328
if let Some(level) = assets.get(level_handle.clone_weak()) {
315-
debug!("Loaded level");
329+
// Track load progress
330+
let load_progress = level.load_progress(&loading_resources);
331+
debug!(
332+
%load_progress,
333+
"Loading level assets: {:.2}% ",
334+
load_progress.as_percent()
335+
);
336+
337+
// Wait until assets are loaded to start game
338+
if load_progress.as_percent() < 1.0 {
339+
return;
340+
}
341+
316342
let window = windows.primary();
317343

318344
// Setup the parallax background

0 commit comments

Comments
 (0)