Skip to content

Commit 9831e5e

Browse files
committed
rapier integration
1 parent 416b39d commit 9831e5e

File tree

8 files changed

+825
-134
lines changed

8 files changed

+825
-134
lines changed

Cargo.lock

Lines changed: 428 additions & 110 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ description = "A fixed update for bevy, unthrottled from bevy's default update l
55
edition = "2021"
66
license = "MIT OR Apache-2.0"
77
keywords = ["bevy", "task", "performance", "update", "simulation"]
8+
repository = "https://github.com/Vrixyz/bevy_fixed_update_task"
89

910
[features]
1011
default = ["x11"]
1112
x11 = ["bevy/x11"]
1213

14+
[profile.dev]
15+
# Use slightly better optimization by default, as examples otherwise seem laggy.
16+
opt-level = 1
17+
1318
[dependencies]
1419
bevy = { version = "0.15", default-features = false }
1520
crossbeam-channel = "0.5"
@@ -25,8 +30,11 @@ bevy = { version = "0.15", default-features = false, features = [
2530
"bevy_winit",
2631
"default_font",
2732
"bevy_gizmos",
33+
"bevy_dev_tools",
2834
# Without multi threading, this crate adds a frame delay on the fixed update.
2935
"multi_threaded",
3036
] }
3137
rand = "0.8"
3238
bevy_transform_interpolation = { version = "0.1", git = "https://github.com/Vrixyz/bevy_transform_interpolation.git", branch = "background_task" }
39+
# bevy_rapier2d = { version = "0.28", git = "https://github.com/dimforge/bevy_rapier.git", branch = "master" }
40+
bevy_rapier2d = { path = "../bevy_rapier/bevy_rapier2d" }

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,29 @@ so you can improve your time budget.
77

88
[Read more about it.](docs/physics_timestep_loop.md)
99

10+
## Practical advantage
11+
12+
Regain control over your frame per seconds: know when your expensive tasks are making you fall behind, and be able to adapt accordingly.
13+
14+
### Practical example
15+
16+
Using [rapier](https://rapier.rs/), simulation step can be expensive, leading to lags, like this example:
17+
18+
./docs/no_background_task.mp4
19+
20+
Using this crate, you can simulate the expensive task in background, allowing to keep a steady visual frame per second.
21+
22+
./docs/no_background_task.mp4
23+
24+
Both these recordings are simulating 80 000 bodies targeting a fixed update of 30 frames per second.
25+
26+
Pair this technique with interpolation or other visual feedbacks to obtain a very responsive feeling.
27+
1028
## How it works
1129

12-
:warning: this crate makes most sense when using bevy's `multi_threaded` feature. Otherwise, this just adds unnecessary overhead.
30+
:warning: This crate makes most sense when using bevy's `multi_threaded` feature. Otherwise, this just adds unnecessary overhead.
1331

14-
It's quite similar to how bevy's fixed update works, but eagerly extracts ECS data into a background task, to synchronize it only when `Time<Virtual>` catches back to the "simulated time".
32+
This crate adds a custom scheduling comparable to bevy's fixed update, but eagerly extracts ECS data into a background task, to synchronize it only when `Time<Virtual>` catches back to the "simulated time".
1533

1634
The implementation doesn't use `Time<Fixed>` but a component approach `TimeStep`, `SubstepCount`, `TaskToRender`.
1735

docs/background_task.mp4

194 KB
Binary file not shown.

docs/no_background_task.mp4

491 KB
Binary file not shown.

examples/minimal.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
33
use bevy::prelude::*;
44
use bevy_fixed_update_task::{
5-
BackgroundFixedUpdatePlugin, TaskResults, TaskWorker, TaskWorkerTrait, Timestep,
5+
BackgroundFixedUpdatePlugin, TaskResults, TaskToRenderTime, TaskWorker, TaskWorkerTrait,
6+
Timestep,
67
};
78

89
use std::time::Duration;
@@ -34,11 +35,16 @@ fn setup_worker(mut commands: Commands) {
3435
));
3536
}
3637

37-
fn print_simulation_time(simulation_time: Res<SimulationTime>, time: Res<Time>) {
38+
fn print_simulation_time(
39+
simulation_time: Res<SimulationTime>,
40+
time: Res<Time>,
41+
task_to_render_time: Query<&TaskToRenderTime>,
42+
) {
3843
println!(
39-
"Simulation time: {:?} ; time: {:?}",
44+
"Simulation time: {:?} ; time: {:?} ; task to render time: {:?}s",
4045
simulation_time.time,
41-
time.elapsed()
46+
time.elapsed(),
47+
task_to_render_time.single().diff
4248
);
4349
}
4450

@@ -61,8 +67,7 @@ impl TaskWorkerTrait for TaskWorkerTraitImpl {
6167
timestep: Duration,
6268
_substep_count: u32,
6369
) -> Self::TaskResultPure {
64-
std::thread::sleep(Duration::from_secs_f32(0.1));
65-
input.time += timestep;
70+
input.time += timestep * _substep_count;
6671
input.time
6772
}
6873

examples/rapier_integration.rs

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
//! This is a minimal example to show how synchronized fixed update works.
2+
3+
use bevy::dev_tools::fps_overlay::FpsOverlayPlugin;
4+
use bevy::prelude::*;
5+
use bevy_fixed_update_task::{
6+
BackgroundFixedUpdatePlugin, SpawnTaskSet, TaskResults, TaskToRenderTime, TaskWorker,
7+
TaskWorkerTrait, Timestep,
8+
};
9+
use bevy_rapier2d::prelude::*;
10+
11+
use std::{mem, time::Duration};
12+
13+
fn main() {
14+
let mut app = App::new();
15+
16+
app.add_plugins((
17+
DefaultPlugins,
18+
FpsOverlayPlugin::default(),
19+
BackgroundFixedUpdatePlugin::<TaskWorkerTraitImpl>::default(),
20+
RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0)
21+
.with_custom_initialization(RapierContextInitialization::NoAutomaticRapierContext)
22+
.in_schedule(bevy_fixed_update_task::FixedMain)
23+
.set_physics_sets_to_initialize([].into()),
24+
RapierDebugRenderPlugin::default(),
25+
));
26+
app.add_systems(Startup, (setup_worker, (setup_info, setup_physics)).chain());
27+
app.add_systems(Update, update_info);
28+
// TODO: SyncBackend before [`SpawnTask`].
29+
app.add_systems(
30+
bevy_fixed_update_task::SpawnTask,
31+
RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsSet::SyncBackend)
32+
.in_set(SpawnTaskSet::PreSpawn),
33+
);
34+
// TODO: StepSimulation removed, that's our spawn task + handle task.
35+
// TODO: Writeback before [`PostWriteBack`].
36+
app.add_systems(
37+
bevy_fixed_update_task::PostWriteBack,
38+
RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsSet::Writeback),
39+
);
40+
41+
// Run the app.
42+
app.run();
43+
}
44+
45+
fn setup_worker(mut commands: Commands) {
46+
commands.spawn((
47+
Timestep {
48+
timestep: Duration::from_secs_f32(1.0 / 25.0),
49+
},
50+
TaskResults::<TaskWorkerTraitImpl>::default(),
51+
TaskWorker {
52+
worker: TaskWorkerTraitImpl {},
53+
},
54+
RapierContextSimulation::default(),
55+
DefaultRapierContext,
56+
RapierConfiguration {
57+
gravity: Vect::Y * -9.81 * 100.0,
58+
physics_pipeline_active: true,
59+
query_pipeline_active: true,
60+
scaled_shape_subdivision: 10,
61+
force_update_from_transform_changes: false,
62+
},
63+
));
64+
}
65+
66+
#[derive(Component)]
67+
pub struct SimToRenderText;
68+
69+
pub fn setup_info(mut commands: Commands) {
70+
// Simulation to render time
71+
commands
72+
.spawn((
73+
Text::new("simulation to render time: "),
74+
Node {
75+
position_type: PositionType::Absolute,
76+
top: Val::Px(50.0),
77+
left: Val::Px(15.0),
78+
..default()
79+
},
80+
))
81+
.with_child((TextSpan::default(), SimToRenderText));
82+
}
83+
pub fn update_info(
84+
task_to_render_time: Query<&TaskToRenderTime>,
85+
mut query: Query<&mut TextSpan, With<SimToRenderText>>,
86+
) {
87+
for mut span in query.iter_mut() {
88+
**span = format!("{:.2}s", task_to_render_time.single().diff);
89+
}
90+
}
91+
92+
pub fn setup_physics(mut commands: Commands) {
93+
let num = 80;
94+
let rad = 10.0;
95+
96+
let shift = rad * 2.0 + rad;
97+
let centerx = shift * (num / 2) as f32;
98+
let centery = shift / 2.0;
99+
/*
100+
* Camera
101+
*/
102+
commands.spawn((
103+
Camera2d::default(),
104+
OrthographicProjection {
105+
scale: 6f32,
106+
..OrthographicProjection::default_2d()
107+
},
108+
Transform::from_xyz(-2500.0, 2080.0, 0.0),
109+
));
110+
/*
111+
* Ground
112+
*/
113+
let ground_size = 13500.0;
114+
let ground_height = 100.0;
115+
116+
commands.spawn((
117+
Transform::from_xyz(-centerx, 0.0 * -ground_height - 100.0, 0.0),
118+
Collider::cuboid(ground_size, ground_height),
119+
));
120+
121+
/*
122+
* Create the cubes
123+
*/
124+
let mut offset = -(num as f32) * (rad * 2.0 + rad) * 0.5;
125+
126+
for j in 0usize..100 {
127+
for i in 0..num {
128+
let x = i as f32 * shift - centerx + offset;
129+
let y = j as f32 * shift + centery + 30.0;
130+
131+
commands.spawn((
132+
// Mesh2d(mesh.clone()),
133+
//MeshMaterial2d(material.clone()),
134+
Transform::from_xyz(x, y, 0.0),
135+
RigidBody::Dynamic,
136+
Collider::cuboid(rad, rad),
137+
));
138+
}
139+
140+
offset -= 0.05 * rad * ((num as f32 * 1.0) - 1.0);
141+
}
142+
}
143+
144+
#[derive(Debug, Clone, Default)]
145+
pub struct TaskWorkerTraitImpl;
146+
147+
impl TaskWorkerTrait for TaskWorkerTraitImpl {
148+
type TaskExtractedData = TaskExtractedData;
149+
type TaskResultPure = TaskResult;
150+
151+
fn work(
152+
&self,
153+
_worker: Entity,
154+
mut input: TaskExtractedData,
155+
timestep: Duration,
156+
substep_count: u32,
157+
) -> Self::TaskResultPure {
158+
input.rapier_context.step_simulation(
159+
&mut input.colliders,
160+
&mut input.joints,
161+
&mut input.bodies,
162+
input.configuration.gravity,
163+
TimestepMode::Fixed {
164+
dt: timestep.as_secs_f32(),
165+
substeps: substep_count as usize,
166+
},
167+
None, // FIXME: change `None` to `true` (see bevy's integration from Thierry)
168+
&(), // FIXME: &hooks_adapter,
169+
&input.time,
170+
&mut input.sim_to_render_time,
171+
None,
172+
);
173+
TaskResult {
174+
rapier_context: input.rapier_context,
175+
colliders: input.colliders,
176+
bodies: input.bodies,
177+
joints: input.joints,
178+
query_pipeline: input.query_pipeline,
179+
sim_to_render_time: input.sim_to_render_time,
180+
}
181+
}
182+
183+
fn extract(&self, worker_entity: Entity, world: &mut World) -> TaskExtractedData {
184+
// Time is not actually used as we're only using `TimestepMode::Fixed`,
185+
// but rapier API requires it.
186+
let time = world.get_resource::<Time>().unwrap();
187+
188+
let time = time.clone();
189+
let mut rapier_context_query = world.query::<(
190+
&mut RapierContextSimulation,
191+
&RapierContextColliders,
192+
&RapierRigidBodySet,
193+
&RapierContextJoints,
194+
&RapierQueryPipeline,
195+
&RapierConfiguration,
196+
&mut SimulationToRenderTime,
197+
)>();
198+
let (
199+
mut context_ecs,
200+
colliders,
201+
bodies,
202+
joints,
203+
query_pipeline,
204+
config,
205+
sim_to_render_time,
206+
) = rapier_context_query.get_mut(world, worker_entity).unwrap();
207+
208+
// FIXME: Clone this properly?
209+
let mut rapier_context = RapierContextSimulation::default();
210+
mem::swap(&mut rapier_context, &mut *context_ecs);
211+
// TODO: use a double buffering system to avoid this more expensive (to verify) cloning.
212+
let colliders = colliders.clone();
213+
let bodies = bodies.clone();
214+
let joints = joints.clone();
215+
let query_pipeline = query_pipeline.clone();
216+
// let mut context: RapierContext =
217+
// unsafe { mem::transmute_copy::<RapierContext, RapierContext>(&*context_ecs) };
218+
let configuration = config.clone();
219+
220+
let sim_to_render_time = sim_to_render_time.clone();
221+
222+
TaskExtractedData {
223+
time,
224+
rapier_context,
225+
colliders,
226+
bodies,
227+
joints,
228+
query_pipeline,
229+
configuration,
230+
sim_to_render_time,
231+
}
232+
}
233+
234+
fn write_back(
235+
&self,
236+
worker_entity: Entity,
237+
result: bevy_fixed_update_task::TaskResult<Self>,
238+
world: &mut World,
239+
) {
240+
let mut rapier_context_query = world.query::<(
241+
&mut RapierContextSimulation,
242+
&mut RapierContextColliders,
243+
&mut RapierRigidBodySet,
244+
&mut RapierContextJoints,
245+
&mut RapierQueryPipeline,
246+
&mut SimulationToRenderTime,
247+
)>();
248+
let (
249+
mut context_ecs,
250+
mut colliders,
251+
mut bodies,
252+
mut joints,
253+
mut query_pipeline,
254+
mut sim_to_render_time,
255+
) = rapier_context_query.get_mut(world, worker_entity).unwrap();
256+
257+
*context_ecs = result.result_raw.result.rapier_context;
258+
*colliders = result.result_raw.result.colliders;
259+
*bodies = result.result_raw.result.bodies;
260+
*joints = result.result_raw.result.joints;
261+
*query_pipeline = result.result_raw.result.query_pipeline;
262+
*sim_to_render_time = result.result_raw.result.sim_to_render_time;
263+
}
264+
}
265+
266+
#[derive(Component)]
267+
pub struct TaskExtractedData {
268+
pub time: Time,
269+
pub rapier_context: RapierContextSimulation,
270+
pub colliders: RapierContextColliders,
271+
pub bodies: RapierRigidBodySet,
272+
pub joints: RapierContextJoints,
273+
pub query_pipeline: RapierQueryPipeline,
274+
pub configuration: RapierConfiguration,
275+
pub sim_to_render_time: SimulationToRenderTime,
276+
}
277+
278+
#[derive(Component)]
279+
pub struct TaskResult {
280+
pub rapier_context: RapierContextSimulation,
281+
pub colliders: RapierContextColliders,
282+
pub bodies: RapierRigidBodySet,
283+
pub joints: RapierContextJoints,
284+
pub query_pipeline: RapierQueryPipeline,
285+
pub sim_to_render_time: SimulationToRenderTime,
286+
}

0 commit comments

Comments
 (0)