docs/bevy/examples/showcase/loading_screen.rs 9.5 K · 305 lines · raw · history

1 //! Shows how to create a loading screen that waits for assets to load and render.
2 use bevy::{ecs::system::SystemId, prelude::*};
3
4 use pipelines_ready::*;
5
6 // The way we'll go about doing this in this example is to
7 // keep track of all assets that we want to have loaded before
8 // we transition to the desired scene.
9 //
10 // In order to ensure that visual assets are fully rendered
11 // before transitioning to the scene, we need to get the
12 // current status of cached pipelines.
13 //
14 // While loading and pipelines compilation is happening, we
15 // will show a loading screen. Once loading is complete, we
16 // will transition to the scene we just loaded.
17
18 fn main() {
19 App::new()
20 .add_plugins(DefaultPlugins)
21 // `PipelinesReadyPlugin` is declared in the `pipelines_ready` module below.
22 .add_plugins(PipelinesReadyPlugin)
23 .insert_resource(LoadingState::default())
24 .insert_resource(LoadingData::new(5))
25 .add_systems(Startup, (setup, load_loading_screen))
26 .add_systems(
27 Update,
28 (update_loading_data, level_selection, display_loading_screen),
29 )
30 .run();
31 }
32
33 // A `Resource` that holds the current loading state.
34 #[derive(Resource, Default)]
35 enum LoadingState {
36 #[default]
37 LevelReady,
38 LevelLoading,
39 }
40
41 // A resource that holds the current loading data.
42 #[derive(Resource, Debug, Default)]
43 struct LoadingData {
44 // This will hold the currently unloaded/loading assets.
45 loading_assets: Vec<UntypedHandle>,
46 // Number of frames that everything needs to be ready for.
47 // This is to prevent going into the fully loaded state in instances
48 // where there might be a some frames between certain loading/pipelines action.
49 confirmation_frames_target: usize,
50 // Current number of confirmation frames.
51 confirmation_frames_count: usize,
52 }
53
54 impl LoadingData {
55 fn new(confirmation_frames_target: usize) -> Self {
56 Self {
57 loading_assets: Vec::new(),
58 confirmation_frames_target,
59 confirmation_frames_count: 0,
60 }
61 }
62 }
63
64 // This resource will hold the level related systems ID for later use.
65 #[derive(Resource)]
66 struct LevelData {
67 unload_level_id: SystemId,
68 level_1_id: SystemId,
69 level_2_id: SystemId,
70 }
71
72 fn setup(mut commands: Commands) {
73 let level_data = LevelData {
74 unload_level_id: commands.register_system(unload_current_level),
75 level_1_id: commands.register_system(load_level_1),
76 level_2_id: commands.register_system(load_level_2),
77 };
78 commands.insert_resource(level_data);
79
80 // Spawns the UI that will show the user prompts.
81 let text_style = TextFont {
82 font_size: FontSize::Px(42.0),
83 ..default()
84 };
85 commands
86 .spawn((
87 Node {
88 justify_self: JustifySelf::Center,
89 align_self: AlignSelf::FlexEnd,
90 ..default()
91 },
92 BackgroundColor(Color::NONE),
93 ))
94 .with_child((Text::new("Press 1 or 2 to load a new scene."), text_style));
95 }
96
97 // Selects the level you want to load.
98 fn level_selection(
99 mut commands: Commands,
100 keyboard: Res<ButtonInput<KeyCode>>,
101 level_data: Res<LevelData>,
102 loading_state: Res<LoadingState>,
103 ) {
104 // Only trigger a load if the current level is fully loaded.
105 if let LoadingState::LevelReady = loading_state.as_ref() {
106 if keyboard.just_pressed(KeyCode::Digit1) {
107 commands.run_system(level_data.unload_level_id);
108 commands.run_system(level_data.level_1_id);
109 } else if keyboard.just_pressed(KeyCode::Digit2) {
110 commands.run_system(level_data.unload_level_id);
111 commands.run_system(level_data.level_2_id);
112 }
113 }
114 }
115
116 // Marker component for easier deletion of entities.
117 #[derive(Component)]
118 struct LevelComponents;
119
120 // Removes all currently loaded level assets from the game World.
121 fn unload_current_level(
122 mut commands: Commands,
123 mut loading_state: ResMut<LoadingState>,
124 entities: Query<Entity, With<LevelComponents>>,
125 ) {
126 *loading_state = LoadingState::LevelLoading;
127 for entity in entities.iter() {
128 commands.entity(entity).despawn();
129 }
130 }
131
132 fn load_level_1(
133 mut commands: Commands,
134 mut loading_data: ResMut<LoadingData>,
135 asset_server: Res<AssetServer>,
136 ) {
137 // Spawn the camera.
138 commands.spawn((
139 Camera3d::default(),
140 Transform::from_xyz(155.0, 155.0, 155.0).looking_at(Vec3::new(0.0, 40.0, 0.0), Vec3::Y),
141 LevelComponents,
142 ));
143
144 // Save the asset into the `loading_assets` vector.
145 let fox = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb"));
146 loading_data.loading_assets.push(fox.clone().into());
147 // Spawn the fox.
148 commands.spawn((
149 WorldAssetRoot(fox.clone()),
150 Transform::from_xyz(0.0, 0.0, 0.0),
151 LevelComponents,
152 ));
153
154 // Spawn the light.
155 commands.spawn((
156 DirectionalLight {
157 shadow_maps_enabled: true,
158 ..default()
159 },
160 Transform::from_xyz(3.0, 3.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y),
161 LevelComponents,
162 ));
163 }
164
165 fn load_level_2(
166 mut commands: Commands,
167 mut loading_data: ResMut<LoadingData>,
168 asset_server: Res<AssetServer>,
169 ) {
170 // Spawn the camera.
171 commands.spawn((
172 Camera3d::default(),
173 Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::new(0.0, 0.2, 0.0), Vec3::Y),
174 LevelComponents,
175 ));
176
177 // Spawn the helmet.
178 let helmet_scene = asset_server
179 .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"));
180 loading_data
181 .loading_assets
182 .push(helmet_scene.clone().into());
183 commands.spawn((WorldAssetRoot(helmet_scene.clone()), LevelComponents));
184
185 // Spawn the light.
186 commands.spawn((
187 DirectionalLight {
188 shadow_maps_enabled: true,
189 ..default()
190 },
191 Transform::from_xyz(3.0, 3.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y),
192 LevelComponents,
193 ));
194 }
195
196 // Monitors current loading status of assets.
197 fn update_loading_data(
198 mut loading_data: ResMut<LoadingData>,
199 mut loading_state: ResMut<LoadingState>,
200 asset_server: Res<AssetServer>,
201 pipelines_ready: Res<PipelinesReady>,
202 ) {
203 if !loading_data.loading_assets.is_empty() || !pipelines_ready.0 {
204 // If we are still loading assets / pipelines are not fully compiled,
205 // we reset the confirmation frame count.
206 loading_data.confirmation_frames_count = 0;
207
208 loading_data.loading_assets.retain(|asset| {
209 asset_server
210 .get_recursive_dependency_load_state(asset)
211 .is_none_or(|state| !state.is_loaded())
212 });
213
214 // If there are no more assets being monitored, and pipelines
215 // are compiled, then start counting confirmation frames.
216 // Once enough confirmations have passed, everything will be
217 // considered to be fully loaded.
218 } else {
219 loading_data.confirmation_frames_count += 1;
220 if loading_data.confirmation_frames_count == loading_data.confirmation_frames_target {
221 *loading_state = LoadingState::LevelReady;
222 }
223 }
224 }
225
226 // Marker tag for loading screen components.
227 #[derive(Component)]
228 struct LoadingScreen;
229
230 // Spawns the necessary components for the loading screen.
231 fn load_loading_screen(mut commands: Commands) {
232 let text_style = TextFont {
233 font_size: FontSize::Px(67.0),
234 ..default()
235 };
236
237 // Spawn the UI and Loading screen camera.
238 commands.spawn((
239 Camera2d,
240 Camera {
241 order: 1,
242 ..default()
243 },
244 LoadingScreen,
245 ));
246
247 // Spawn the UI that will make up the loading screen.
248 commands
249 .spawn((
250 Node {
251 height: percent(100),
252 width: percent(100),
253 justify_content: JustifyContent::Center,
254 align_items: AlignItems::Center,
255 ..default()
256 },
257 BackgroundColor(Color::BLACK),
258 LoadingScreen,
259 ))
260 .with_child((Text::new("Loading..."), text_style.clone()));
261 }
262
263 // Determines when to show the loading screen
264 fn display_loading_screen(
265 mut loading_screen: Single<&mut Visibility, (With<LoadingScreen>, With<Node>)>,
266 loading_state: Res<LoadingState>,
267 ) {
268 let visibility = match loading_state.as_ref() {
269 LoadingState::LevelLoading => Visibility::Visible,
270 LoadingState::LevelReady => Visibility::Hidden,
271 };
272
273 **loading_screen = visibility;
274 }
275
276 mod pipelines_ready {
277 use bevy::{
278 prelude::*,
279 render::{render_resource::*, *},
280 };
281
282 pub struct PipelinesReadyPlugin;
283 impl Plugin for PipelinesReadyPlugin {
284 fn build(&self, app: &mut App) {
285 app.insert_resource(PipelinesReady::default());
286
287 // In order to gain access to the pipelines status, we have to
288 // go into the `RenderApp`, grab the resource from the main App
289 // and then update the pipelines status from there.
290 // Writing between these Apps can only be done through the
291 // `ExtractSchedule`.
292 app.sub_app_mut(RenderApp)
293 .add_systems(ExtractSchedule, update_pipelines_ready);
294 }
295 }
296
297 #[derive(Resource, Debug, Default)]
298 pub struct PipelinesReady(pub bool);
299
300 fn update_pipelines_ready(mut main_world: ResMut<MainWorld>, pipelines: Res<PipelineCache>) {
301 if let Some(mut pipelines_ready) = main_world.get_resource_mut::<PipelinesReady>() {
302 pipelines_ready.0 = pipelines.waiting_pipelines().count() == 0;
303 }
304 }
305 }