Skip to main content

loading_screen/
loading_screen.rs

1//! Shows how to create a loading screen that waits for assets to load and render.
2use bevy::{ecs::system::SystemId, prelude::*};
3
4use 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
18fn 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)]
35enum LoadingState {
36    #[default]
37    LevelReady,
38    LevelLoading,
39}
40
41// A resource that holds the current loading data.
42#[derive(Resource, Debug, Default)]
43struct 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
54impl 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)]
66struct LevelData {
67    unload_level_id: SystemId,
68    level_1_id: SystemId,
69    level_2_id: SystemId,
70}
71
72fn 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.
98fn 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)]
118struct LevelComponents;
119
120// Removes all currently loaded level assets from the game World.
121fn 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
132fn 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
165fn 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.
197fn 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)]
228struct LoadingScreen;
229
230// Spawns the necessary components for the loading screen.
231fn 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
264fn 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
276mod 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}