docs/bevy/examples/async_tasks/async_channel_pattern.rs 5.8 K · 169 lines · raw · history

1 //! A minimal example showing how to perform asynchronous work in Bevy
2 //! using [`AsyncComputeTaskPool`] for parallel task execution and a crossbeam channel
3 //! to communicate between async tasks and the main ECS thread.
4 //!
5 //! This example demonstrates how to spawn detached async tasks, send completion messages via channels,
6 //! and dynamically spawn ECS entities (cubes) as results from these tasks. The system processes
7 //! async task results in the main game loop, all without blocking or polling the main thread.
8
9 use bevy::{
10 math::ops::{cos, sin},
11 prelude::*,
12 tasks::AsyncComputeTaskPool,
13 };
14 use crossbeam_channel::{Receiver, Sender};
15 use futures_timer::Delay;
16 use rand::RngExt;
17 use std::time::Duration;
18
19 const NUM_CUBES: i32 = 6;
20 const LIGHT_RADIUS: f32 = 8.0;
21
22 fn main() {
23 App::new()
24 .add_plugins(DefaultPlugins)
25 .add_systems(
26 Startup,
27 (
28 setup_env,
29 setup_assets,
30 setup_channel,
31 // Ensure the channel is set up before spawning tasks.
32 spawn_tasks.after(setup_channel),
33 ),
34 )
35 .add_systems(Update, (handle_finished_cubes, rotate_light))
36 .run();
37 }
38
39 /// Spawns async tasks on the compute task pool to simulate delayed cube creation.
40 ///
41 /// Each task is executed on a separate thread and sends the result (cube position)
42 /// back through the `CubeChannel` once completed. The tasks are detached to
43 /// run asynchronously without blocking the main thread.
44 ///
45 /// In this example, we don't implement task tracking or proper error handling.
46 fn spawn_tasks(channel: Res<CubeChannel>) {
47 let pool = AsyncComputeTaskPool::get();
48
49 for x in -NUM_CUBES..NUM_CUBES {
50 for z in -NUM_CUBES..NUM_CUBES {
51 let sender = channel.sender.clone();
52 // Spawn a task on the async compute pool
53 pool.spawn(async move {
54 let delay = Duration::from_secs_f32(rand::rng().random_range(2.0..8.0));
55 // Simulate a delay before task completion
56 Delay::new(delay).await;
57 let _ = sender.send(CubeFinished {
58 transform: Transform::from_xyz(x as f32, 0.5, z as f32),
59 });
60 })
61 .detach();
62 }
63 }
64 }
65
66 /// Handles the completion of async tasks and spawns ECS entities (cubes)
67 /// based on the received data. The function reads from the `CubeChannel`'s
68 /// receiver to get the results (cube positions) and spawns cubes accordingly.
69 fn handle_finished_cubes(
70 mut commands: Commands,
71 channel: Res<CubeChannel>,
72 box_mesh: Res<BoxMeshHandle>,
73 box_material: Res<BoxMaterialHandle>,
74 ) {
75 for msg in channel.receiver.try_iter() {
76 // Spawn cube entity
77 commands.spawn((
78 Mesh3d(box_mesh.clone()),
79 MeshMaterial3d(box_material.clone()),
80 msg.transform,
81 ));
82 }
83 }
84
85 /// Sets up a communication channel (`CubeChannel`) to send data between
86 /// async tasks and the main ECS thread. The sender is used by async tasks
87 /// to send the result (cube position), while the receiver is used by the
88 /// main thread to retrieve and process the completed data.
89 fn setup_channel(mut commands: Commands) {
90 let (sender, receiver) = crossbeam_channel::unbounded();
91 commands.insert_resource(CubeChannel { sender, receiver });
92 }
93
94 /// A channel for communicating between async tasks and the main thread.
95 #[derive(Resource)]
96 struct CubeChannel {
97 sender: Sender<CubeFinished>,
98 receiver: Receiver<CubeFinished>,
99 }
100
101 /// Represents the completion of a cube task, containing the cube's transform
102 #[derive(Debug)]
103 struct CubeFinished {
104 transform: Transform,
105 }
106
107 /// Resource holding the mesh handle for the box (used for spawning cubes)
108 #[derive(Resource, Deref)]
109 struct BoxMeshHandle(Handle<Mesh>);
110
111 /// Resource holding the material handle for the box (used for spawning cubes)
112 #[derive(Resource, Deref)]
113 struct BoxMaterialHandle(Handle<StandardMaterial>);
114
115 /// Sets up the shared mesh and material for the cubes.
116 fn setup_assets(
117 mut commands: Commands,
118 mut meshes: ResMut<Assets<Mesh>>,
119 mut materials: ResMut<Assets<StandardMaterial>>,
120 ) {
121 // Create and store a cube mesh
122 let box_mesh_handle = meshes.add(Cuboid::new(0.4, 0.4, 0.4));
123 commands.insert_resource(BoxMeshHandle(box_mesh_handle));
124
125 // Create and store a red material
126 let box_material_handle = materials.add(Color::srgb(1.0, 0.2, 0.3));
127 commands.insert_resource(BoxMaterialHandle(box_material_handle));
128 }
129
130 /// Sets up the environment by spawning the ground, light, and camera.
131 fn setup_env(
132 mut commands: Commands,
133 mut meshes: ResMut<Assets<Mesh>>,
134 mut materials: ResMut<Assets<StandardMaterial>>,
135 ) {
136 // Spawn a circular ground plane
137 commands.spawn((
138 Mesh3d(meshes.add(Circle::new(1.618 * NUM_CUBES as f32))),
139 MeshMaterial3d(materials.add(Color::WHITE)),
140 Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
141 ));
142
143 // Spawn a point light with shadows enabled
144 commands.spawn((
145 PointLight {
146 shadow_maps_enabled: true,
147 ..default()
148 },
149 Transform::from_xyz(0.0, LIGHT_RADIUS, 4.0),
150 ));
151
152 // Spawn a camera looking at the origin
153 commands.spawn((
154 Camera3d::default(),
155 Transform::from_xyz(-6.5, 5.5, 12.0).looking_at(Vec3::ZERO, Vec3::Y),
156 ));
157 }
158
159 /// Rotates the point light around the origin (0, 0, 0)
160 fn rotate_light(mut query: Query<&mut Transform, With<PointLight>>, time: Res<Time>) {
161 for mut transform in query.iter_mut() {
162 let angle = 1.618 * time.elapsed_secs();
163 let x = LIGHT_RADIUS * cos(angle);
164 let z = LIGHT_RADIUS * sin(angle);
165
166 // Update the light's position to rotate around the origin
167 transform.translation = Vec3::new(x, LIGHT_RADIUS, z);
168 }
169 }