Skip to main content

async_channel_pattern/
async_channel_pattern.rs

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
9use bevy::{
10    math::ops::{cos, sin},
11    prelude::*,
12    tasks::AsyncComputeTaskPool,
13};
14use crossbeam_channel::{Receiver, Sender};
15use futures_timer::Delay;
16use rand::RngExt;
17use std::time::Duration;
18
19const NUM_CUBES: i32 = 6;
20const LIGHT_RADIUS: f32 = 8.0;
21
22fn 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.
46fn 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.
69fn 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.
89fn 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)]
96struct 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)]
103struct CubeFinished {
104    transform: Transform,
105}
106
107/// Resource holding the mesh handle for the box (used for spawning cubes)
108#[derive(Resource, Deref)]
109struct BoxMeshHandle(Handle<Mesh>);
110
111/// Resource holding the material handle for the box (used for spawning cubes)
112#[derive(Resource, Deref)]
113struct BoxMaterialHandle(Handle<StandardMaterial>);
114
115/// Sets up the shared mesh and material for the cubes.
116fn 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.
131fn 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)
160fn 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}