I am currently working on a 3D voxel-based game and I want to have procedural generated chunks based on player movement.
But running the chunk generation in a simple system results in huge FPS drops.
I already tried to create a task pool that runs independent from everything else using std::sync::mpsc::channel
, to generate the chunks data and mesh, which then gets requested by a normal bevy system, buffered and then spawned using commands.spawn(PbrBundle{...})
.
fn chunk_loader(
pool: Res<Pool>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut chunkmap: ResMut<ChunkMap>,
mut buffer: ResMut<Buffer>, // buffer of chunk data
) {
let mut chunks = pool.request_chunks();
buffer.0.append(&mut chunks);
for _ in 0..CHUNK_UPDATES_PER_FRAME {
if let Some( (chunk, mesh) ) = buffer.0.pop() {
chunkmap.map.insert([chunk.x, chunk.y, chunk.z], chunk);
let mesh = mesh.clone();
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(mesh),
transform: Transform::from_matrix(Mat4::from_scale_rotation_translation(
Vec3::splat(1.0),
Quat::from_rotation_x(0.0),
Vec3::new((chunk.x * CHUNK_SIZE as i64) as f32, (chunk.y * CHUNK_SIZE as i64) as f32, (chunk.z * CHUNK_SIZE as i64) as f32),
)),
material: materials.add(StandardMaterial {
base_color: Color::hex("993939").unwrap(),
perceptual_roughness: 0.95,
..default()
}),
..default()
}).insert(super::LoadedChunk{x: chunk.x, y: chunk.y, z: chunk.z, should_unload: false});
}
}
}
But this does not help, because it still takes up too much time.
What I need is a way to execute the chunk generation and spawning in an async fashion, which does not affect frame rate, but I do not know how I should approach this.
bevy::prelude::AsyncComputeTaskPool
might do the job, but I can't find any documentation or examples, so I do not know what exactly it does, other than async an parallel iteration over queries.
I have never written any async code, can anyone help me?
EDIT
I found out that the system above is actually working quite nice.
The problem is the system I use to check, which chunks to load.
I use a HashMap
to store every Chunk and everytime the player moves I test multiple chunks if they are already spawned and if not send a request to the task pool to do so.
fn chunk_generation(
mut query: Query<(&Transform, &mut Player)>,
mut chunks: ResMut<ChunkMap>,
pool: Res<Pool>,
) {
let mut player_pos = Vec3::ZERO;
let mut player_moved: bool = false;
for (transform, mut player) in query.iter_mut().next() {
player_pos = transform.translation;
if player.chunks_moved_since_update > 0 {
player_moved = true;
player.chunks_moved_since_update = 0;
}
}
let chunk_x = (player_pos.x / CHUNK_SIZE as f32).round() as i64;
let chunk_y = (player_pos.y / CHUNK_SIZE as f32).round() as i64;
let chunk_z = (player_pos.z / CHUNK_SIZE as f32).round() as i64;
// loading
if player_moved {
let mut chunks_to_load: Vec<[i64; 3]> = Vec::new();
for x in -RENDER_DISTANCE_HOR..RENDER_DISTANCE_HOR {
for y in -RENDER_DISTANCE_VER..RENDER_DISTANCE_VER {
for z in -RENDER_DISTANCE_HOR..RENDER_DISTANCE_HOR {
let chunk_x = chunk_x as i64 + x;
let chunk_y = chunk_y as i64 + y;
let chunk_z = chunk_z as i64 + z;
let chunk_coords = [chunk_x, chunk_y, chunk_z];
// check if chunk is not in world
if !chunks.map.contains_key(&chunk_coords) {
println!("generating new chunk");
let chunk = Chunk::new(chunk_x, chunk_y, chunk_z);
chunks.map.insert(chunk_coords, chunk);
chunks_to_load.push([chunk_x, chunk_y, chunk_z]);
}
}
}
}
pool.send_chunks(chunks_to_load);
}
}
Is it possible to make this async too?
player_moved
check, you are potentially traversing the entire allocated map. A more complicated approach (but magnitudes faster) would be creating a 2d linked list, where you store all neighboring chunks and check only if those are spawned. Custom linked links in Rust are usually a nightmare to implement though. There is a book on this if you want to take a closer look: rust-unofficial.github.io/too-many-lists – Palacio