docs/bevy/examples/asset/multi_asset_sync.rs 8.7 K · 280 lines · raw · history

1 //! This example illustrates how to wait for multiple assets to be loaded.
2
3 use std::{
4 f32::consts::PI,
5 ops::Drop,
6 sync::{
7 atomic::{AtomicBool, AtomicU32, Ordering},
8 Arc,
9 },
10 };
11
12 use bevy::{gltf::Gltf, prelude::*, tasks::AsyncComputeTaskPool};
13 use event_listener::Event;
14 use futures_lite::Future;
15
16 fn main() {
17 App::new()
18 .add_plugins(DefaultPlugins)
19 .init_state::<LoadingState>()
20 .insert_resource(GlobalAmbientLight {
21 color: Color::WHITE,
22 brightness: 2000.,
23 ..default()
24 })
25 .add_systems(Startup, setup_assets)
26 .add_systems(Startup, setup_scene)
27 .add_systems(Startup, setup_ui)
28 // This showcases how to wait for assets using sync code.
29 // This approach polls a value in a system.
30 .add_systems(Update, wait_on_load.run_if(assets_loaded))
31 // This showcases how to wait for assets using async
32 // by spawning a `Future` in `AsyncComputeTaskPool`.
33 .add_systems(
34 Update,
35 get_async_loading_state.run_if(in_state(LoadingState::Loading)),
36 )
37 // This showcases how to react to asynchronous world mutation synchronously.
38 .add_systems(
39 OnExit(LoadingState::Loading),
40 despawn_loading_state_entities,
41 )
42 .run();
43 }
44
45 /// [`States`] of asset loading.
46 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, States, Default)]
47 pub enum LoadingState {
48 /// Is loading.
49 #[default]
50 Loading,
51 /// Loading completed.
52 Loaded,
53 }
54
55 /// Holds a bunch of [`Gltf`]s that takes time to load.
56 #[derive(Debug, Resource)]
57 pub struct OneHundredThings([Handle<Gltf>; 100]);
58
59 /// This is required to support both sync and async.
60 ///
61 /// For sync only the easiest implementation is
62 /// [`Arc<()>`] and use [`Arc::strong_count`] for completion.
63 /// [`Arc<Atomic>`] is a more robust alternative.
64 #[derive(Debug, Resource, Deref)]
65 pub struct AssetBarrier(Arc<AssetBarrierInner>);
66
67 /// This guard is to be acquired by
68 /// [`LoadBuilder::with_guard`](bevy::asset::LoadBuilder::with_guard) and dropped once finished.
69 #[derive(Debug, Deref)]
70 pub struct AssetBarrierGuard(Arc<AssetBarrierInner>);
71
72 /// Tracks how many guards are remaining.
73 #[derive(Debug, Resource)]
74 pub struct AssetBarrierInner {
75 count: AtomicU32,
76 /// This can be omitted if async is not needed.
77 notify: Event,
78 }
79
80 /// State of loading asynchronously.
81 #[derive(Debug, Resource)]
82 pub struct AsyncLoadingState(Arc<AtomicBool>);
83
84 /// Entities that are to be removed once loading finished
85 #[derive(Debug, Component)]
86 pub struct Loading;
87
88 /// Marker for the "Loading..." Text component.
89 #[derive(Debug, Component)]
90 pub struct LoadingText;
91
92 impl AssetBarrier {
93 /// Create an [`AssetBarrier`] with a [`AssetBarrierGuard`].
94 pub fn new() -> (AssetBarrier, AssetBarrierGuard) {
95 let inner = Arc::new(AssetBarrierInner {
96 count: AtomicU32::new(1),
97 notify: Event::new(),
98 });
99 (AssetBarrier(inner.clone()), AssetBarrierGuard(inner))
100 }
101
102 /// Returns true if all [`AssetBarrierGuard`] is dropped.
103 pub fn is_ready(&self) -> bool {
104 self.count.load(Ordering::Acquire) == 0
105 }
106
107 /// Wait for all [`AssetBarrierGuard`]s to be dropped asynchronously.
108 pub fn wait_async(&self) -> impl Future<Output = ()> + 'static {
109 let shared = self.0.clone();
110 async move {
111 loop {
112 // Acquire an event listener.
113 let listener = shared.notify.listen();
114 // If all barrier guards are dropped, return
115 if shared.count.load(Ordering::Acquire) == 0 {
116 return;
117 }
118 // Wait for the last barrier guard to notify us
119 listener.await;
120 }
121 }
122 }
123 }
124
125 // Increment count on clone.
126 impl Clone for AssetBarrierGuard {
127 fn clone(&self) -> Self {
128 self.count.fetch_add(1, Ordering::AcqRel);
129 AssetBarrierGuard(self.0.clone())
130 }
131 }
132
133 // Decrement count on drop.
134 impl Drop for AssetBarrierGuard {
135 fn drop(&mut self) {
136 let prev = self.count.fetch_sub(1, Ordering::AcqRel);
137 if prev == 1 {
138 // Notify all listeners if count reaches 0.
139 self.notify.notify(usize::MAX);
140 }
141 }
142 }
143
144 fn setup_assets(mut commands: Commands, asset_server: Res<AssetServer>) {
145 let (barrier, guard) = AssetBarrier::new();
146 commands.insert_resource(OneHundredThings(std::array::from_fn(|i| {
147 let builder = asset_server.load_builder().with_guard(guard.clone());
148 match i % 5 {
149 0 => builder.load("models/GolfBall/GolfBall.glb"),
150 1 => builder.load("models/AlienCake/alien.glb"),
151 2 => builder.load("models/AlienCake/cakeBirthday.glb"),
152 3 => builder.load("models/FlightHelmet/FlightHelmet.gltf"),
153 4 => builder.load("models/torus/torus.gltf"),
154 _ => unreachable!(),
155 }
156 })));
157 let future = barrier.wait_async();
158 commands.insert_resource(barrier);
159
160 let loading_state = Arc::new(AtomicBool::new(false));
161 commands.insert_resource(AsyncLoadingState(loading_state.clone()));
162
163 // await the `AssetBarrierFuture`.
164 AsyncComputeTaskPool::get()
165 .spawn(async move {
166 future.await;
167 // Notify via `AsyncLoadingState`
168 loading_state.store(true, Ordering::Release);
169 })
170 .detach();
171 }
172
173 fn setup_ui(mut commands: Commands) {
174 // Display the result of async loading.
175
176 commands.spawn((
177 LoadingText,
178 Text::new("Loading...".to_owned()),
179 Node {
180 position_type: PositionType::Absolute,
181 left: px(12),
182 top: px(12),
183 ..default()
184 },
185 ));
186 }
187
188 fn setup_scene(
189 mut commands: Commands,
190 mut meshes: ResMut<Assets<Mesh>>,
191 mut materials: ResMut<Assets<StandardMaterial>>,
192 ) {
193 // Camera
194 commands.spawn((
195 Camera3d::default(),
196 Transform::from_xyz(10.0, 10.0, 15.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
197 ));
198
199 // Light
200 commands.spawn((
201 DirectionalLight {
202 shadow_maps_enabled: true,
203 ..default()
204 },
205 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
206 ));
207
208 // Plane
209 commands.spawn((
210 Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))),
211 MeshMaterial3d(materials.add(Color::srgb(0.7, 0.2, 0.2))),
212 Loading,
213 ));
214 }
215
216 // A run condition for all assets being loaded.
217 fn assets_loaded(barrier: Option<Res<AssetBarrier>>) -> bool {
218 // If our barrier isn't ready, return early and wait another cycle
219 barrier.map(|b| b.is_ready()) == Some(true)
220 }
221
222 // This showcases how to wait for assets using sync code and systems.
223 //
224 // This function only runs if `assets_loaded` returns true.
225 fn wait_on_load(
226 mut commands: Commands,
227 foxes: Res<OneHundredThings>,
228 gltfs: Res<Assets<Gltf>>,
229 mut meshes: ResMut<Assets<Mesh>>,
230 mut materials: ResMut<Assets<StandardMaterial>>,
231 ) {
232 // Change color of plane to green
233 commands.spawn((
234 Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))),
235 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
236 Transform::from_translation(Vec3::Z * -0.01),
237 ));
238
239 // Spawn our scenes.
240 for i in 0..10 {
241 for j in 0..10 {
242 let index = i * 10 + j;
243 let position = Vec3::new(i as f32 - 5.0, 0.0, j as f32 - 5.0);
244 // All gltfs must exist because this is guarded by the `AssetBarrier`.
245 let gltf = gltfs.get(&foxes.0[index]).unwrap();
246 let scene = gltf.scenes.first().unwrap().clone();
247 commands.spawn((WorldAssetRoot(scene), Transform::from_translation(position)));
248 }
249 }
250 }
251
252 // This showcases how to wait for assets using async.
253 fn get_async_loading_state(
254 state: Res<AsyncLoadingState>,
255 mut next_loading_state: ResMut<NextState<LoadingState>>,
256 mut text: Query<&mut Text, With<LoadingText>>,
257 ) {
258 // Load the value written by the `Future`.
259 let is_loaded = state.0.load(Ordering::Acquire);
260
261 // If loaded, change the state.
262 if is_loaded {
263 next_loading_state.set(LoadingState::Loaded);
264 if let Ok(mut text) = text.single_mut() {
265 "Loaded!".clone_into(&mut **text);
266 }
267 }
268 }
269
270 // This showcases how to react to asynchronous world mutations synchronously.
271 fn despawn_loading_state_entities(mut commands: Commands, loading: Query<Entity, With<Loading>>) {
272 // Despawn entities in the loading phase.
273 for entity in loading.iter() {
274 commands.entity(entity).despawn();
275 }
276
277 // Despawn resources used in the loading phase.
278 commands.remove_resource::<AssetBarrier>();
279 commands.remove_resource::<AsyncLoadingState>();
280 }