如何在 bevy 游戏引擎中实现异步系统功能?
How to make async system function in the bevy game engine?
我目前正在开发一款基于 3D 体素的游戏,我希望根据玩家的移动程序生成块。
但是运行简单系统中的块生成会导致 FPS 大幅下降。
我已经尝试使用 std::sync::mpsc::channel
创建一个独立于其他所有运行的任务池,以生成块数据和网格,然后由普通的 bevy 系统请求,缓冲,然后使用 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});
}
}
}
但这并没有什么用,因为它仍然占用太多时间。
我需要的是一种以异步方式执行块生成和生成的方法,这不会影响帧速率,但我不知道该如何处理。
bevy::prelude::AsyncComputeTaskPool
可能会完成这项工作,但我找不到任何文档或示例,所以我不知道它到底做了什么,除了对查询进行异步并行迭代。
我从来没有写过任何异步代码,谁能帮帮我?
编辑
我发现上面的系统实际上工作得很好。
问题是我用来检查要加载哪些块的系统。
我使用 HashMap
来存储每个块,每次玩家移动时,我都会测试多个块是否已经生成,如果没有,则向任务池发送请求。
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);
}
}
是否也可以使这个异步?
bevy::tasks::AsyncComputeTaskPool
wrappend in bevy::tasks::Task
是我正在寻找的方法。
我现在有一个系统,可以将任务插入到每个块中以生成自身,然后在另一个函数中轮询此任务,如下所示:
#[derive(Component)]
pub struct GenTask {
pub task: Task<Vec<([i64; 3], ChunkData)>>
}
pub fn prepare_gen_tasks(
mut queue: ResMut<ChunkGenQueue>,
chunks: Res<ChunkMap>,
pool: Res<AsyncComputeTaskPool>,
mut cmds: Commands,
) {
while let Some(key) = queue.queue.pop() {
if let Some(entity) = chunks.entity(key) {
let task = pool.spawn(async move {
let chunk = Chunk::new(key);
super::generation::generate(chunk)
});
cmds.entity(entity).insert(GenTask{task});
}
}
}
pub fn apply_gen_tasks(
mut voxels: ResMut<VoxelMap>,
mut query: Query<(Entity, &mut GenTask)>,
mut mesh_queue: ResMut<ChunkMeshQueue>,
mut cmds: Commands,
) {
query.for_each_mut(|(entity, mut gen_task)| {
if let Some(chunks) = future::block_on(future::poll_once(&mut gen_task.task)) {
for (key, data) in chunks.iter() {
voxels.map.insert(*key, *data);
mesh_queue.queue.push(*key);
}
cmds.entity(entity).remove::<GenTask>();
}
return;
});
}
我目前正在开发一款基于 3D 体素的游戏,我希望根据玩家的移动程序生成块。
但是运行简单系统中的块生成会导致 FPS 大幅下降。
我已经尝试使用 std::sync::mpsc::channel
创建一个独立于其他所有运行的任务池,以生成块数据和网格,然后由普通的 bevy 系统请求,缓冲,然后使用 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});
}
}
}
但这并没有什么用,因为它仍然占用太多时间。
我需要的是一种以异步方式执行块生成和生成的方法,这不会影响帧速率,但我不知道该如何处理。
bevy::prelude::AsyncComputeTaskPool
可能会完成这项工作,但我找不到任何文档或示例,所以我不知道它到底做了什么,除了对查询进行异步并行迭代。
我从来没有写过任何异步代码,谁能帮帮我?
编辑
我发现上面的系统实际上工作得很好。
问题是我用来检查要加载哪些块的系统。
我使用 HashMap
来存储每个块,每次玩家移动时,我都会测试多个块是否已经生成,如果没有,则向任务池发送请求。
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);
}
}
是否也可以使这个异步?
bevy::tasks::AsyncComputeTaskPool
wrappend in bevy::tasks::Task
是我正在寻找的方法。
我现在有一个系统,可以将任务插入到每个块中以生成自身,然后在另一个函数中轮询此任务,如下所示:
#[derive(Component)]
pub struct GenTask {
pub task: Task<Vec<([i64; 3], ChunkData)>>
}
pub fn prepare_gen_tasks(
mut queue: ResMut<ChunkGenQueue>,
chunks: Res<ChunkMap>,
pool: Res<AsyncComputeTaskPool>,
mut cmds: Commands,
) {
while let Some(key) = queue.queue.pop() {
if let Some(entity) = chunks.entity(key) {
let task = pool.spawn(async move {
let chunk = Chunk::new(key);
super::generation::generate(chunk)
});
cmds.entity(entity).insert(GenTask{task});
}
}
}
pub fn apply_gen_tasks(
mut voxels: ResMut<VoxelMap>,
mut query: Query<(Entity, &mut GenTask)>,
mut mesh_queue: ResMut<ChunkMeshQueue>,
mut cmds: Commands,
) {
query.for_each_mut(|(entity, mut gen_task)| {
if let Some(chunks) = future::block_on(future::poll_once(&mut gen_task.task)) {
for (key, data) in chunks.iter() {
voxels.map.insert(*key, *data);
mesh_queue.queue.push(*key);
}
cmds.entity(entity).remove::<GenTask>();
}
return;
});
}