Create basic interactive physics system
All checks were successful
CI / Formatting (push) Successful in 41s
CI / Clippy (push) Successful in 6m9s

This commit is contained in:
Michael Bradley 2025-03-22 22:32:08 -04:00
parent 6108f140b6
commit 6732f5575c
Signed by: MichaelBradley
SSH key fingerprint: SHA256:cj/YZ5VT+QOKncqSkx+ibKTIn0Obg7OIzwzl9BL8EO8
3 changed files with 190 additions and 129 deletions

91
Cargo.lock generated
View file

@ -85,7 +85,7 @@ checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"const-random", "const-random",
"getrandom", "getrandom 0.2.15",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy 0.7.35", "zerocopy 0.7.35",
@ -718,7 +718,7 @@ dependencies = [
"derive_more", "derive_more",
"glam", "glam",
"itertools", "itertools",
"rand", "rand 0.8.5",
"rand_distr", "rand_distr",
"serde", "serde",
"smallvec", "smallvec",
@ -1051,7 +1051,7 @@ checksum = "63c2174d43a0de99f863c98a472370047a2bfa7d1e5cec8d9d647fb500905d9d"
dependencies = [ dependencies = [
"ahash", "ahash",
"bevy_utils_proc_macros", "bevy_utils_proc_macros",
"getrandom", "getrandom 0.2.15",
"hashbrown 0.14.5", "hashbrown 0.14.5",
"thread_local", "thread_local",
"tracing", "tracing",
@ -1409,7 +1409,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
"once_cell", "once_cell",
"tiny-keccak", "tiny-keccak",
] ]
@ -1606,6 +1606,7 @@ dependencies = [
"avian2d", "avian2d",
"bevy", "bevy",
"log", "log",
"rand 0.9.0",
] ]
[[package]] [[package]]
@ -1882,10 +1883,22 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
version = "0.14.0" version = "0.14.0"
@ -1904,7 +1917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677" checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"rand", "rand 0.8.5",
"serde", "serde",
] ]
@ -3036,6 +3049,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]] [[package]]
name = "radsort" name = "radsort"
version = "0.1.1" version = "0.1.1"
@ -3049,8 +3068,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy 0.8.24",
] ]
[[package]] [[package]]
@ -3060,7 +3090,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
] ]
[[package]] [[package]]
@ -3069,7 +3109,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.2",
] ]
[[package]] [[package]]
@ -3079,7 +3128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"rand", "rand 0.8.5",
] ]
[[package]] [[package]]
@ -3833,7 +3882,7 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
"serde", "serde",
] ]
@ -3865,6 +3914,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.100" version = "0.2.100"
@ -4480,6 +4538,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.0",
]
[[package]] [[package]]
name = "xcursor" name = "xcursor"
version = "0.3.8" version = "0.3.8"

View file

@ -33,3 +33,4 @@ log = { version = "*", features = [
"max_level_debug", "max_level_debug",
"release_max_level_warn", "release_max_level_warn",
] } ] }
rand = "0.9.0"

View file

@ -1,152 +1,145 @@
//! This example showcases how `Transform` interpolation or extrapolation can be used use std::f32::consts::PI;
//! to make movement appear smooth at fixed timesteps.
//!
//! To produce consistent, frame rate independent behavior, physics by default runs
//! in the `FixedPostUpdate` schedule with a fixed timestep, meaning that the time between
//! physics ticks remains constant. On some frames, physics can either not run at all or run
//! more than once to catch up to real time. This can lead to visible stutter for movement.
//!
//! `Transform` interpolation resolves this issue by updating `Transform` at every frame in between
//! physics ticks to smooth out the visual result. The interpolation is done from the previous position
//! to the current physics position, which keeps movement smooth, but has the downside of making movement
//! feel slightly delayed as the rendered result lags slightly behind the true positions.
//!
//! `Transform` extrapolation works similarly, but instead of using the previous positions, it predicts
//! the next positions based on velocity. This makes movement feel much more responsive, but can cause
//! jumpy results when the prediction is wrong, such as when the velocity of an object is suddenly altered.
use avian2d::{math::*, prelude::*}; use avian2d::{math::Vector, prelude::*};
use bevy::{ use bevy::{
color::palettes::{ color::palettes::{
css::WHITE, css::WHITE,
tailwind::{CYAN_400, LIME_400, RED_400}, tailwind::{LIME_400, RED_400},
}, },
input::common_conditions::input_pressed, input::common_conditions::input_pressed,
prelude::*, prelude::*,
}; };
use rand::Rng;
const AREA_WIDTH: f32 = 750.;
const PLAYER_SIZE: f32 = 30.;
fn main() { fn main() {
let mut app = App::new(); App::new()
.add_plugins((
// Interpolation and extrapolation functionality is enabled by the `PhysicsInterpolationPlugin`. DefaultPlugins,
// It is included in the `PhysicsPlugins` by default. PhysicsPlugins::default()
app.add_plugins(( .with_length_unit(50.0)
DefaultPlugins, .set(PhysicsInterpolationPlugin::interpolate_all()),
PhysicsPlugins::default().with_length_unit(50.0), ))
)); .add_systems(
Startup,
// By default, interpolation must be enabled for each entity manually (setup_scene, setup_player, setup_balls, setup_walls),
// by adding the `TransformInterpolation` component. )
//
// It can also be enabled for all rigid bodies with `PhysicsInterpolationPlugin::interpolate_all()`:
//
// app.add_plugins(PhysicsPlugins::default().set(PhysicsInterpolationPlugin::interpolate_all()));
// Set gravity.
app.insert_resource(Gravity(Vector::NEG_Y * 900.0));
// Set the fixed timestep to just 10 Hz for demonstration purposes.
app.insert_resource(Time::from_hz(10.0));
// Setup the scene and UI, and update text in `Update`.
app.add_systems(Startup, (setup_scene, setup_balls))
.add_systems( .add_systems(
Update, Update,
( (move_player, quit.run_if(input_pressed(KeyCode::KeyQ))),
change_timestep, )
// Reset the scene when the 'R' key is pressed. .insert_resource(Gravity(Vector::ZERO))
reset_balls.run_if(input_pressed(KeyCode::KeyR)), .run();
),
);
// Run the app.
app.run();
} }
#[derive(Component)] fn setup_scene(mut commands: Commands) {
struct Ball;
fn setup_scene(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
// Spawn a camera.
commands.spawn(Camera2d); commands.spawn(Camera2d);
// Spawn the ground.
commands.spawn((
Name::new("Ground"),
RigidBody::Static,
Collider::rectangle(500.0, 20.0),
Restitution::new(0.99).with_combine_rule(CoefficientCombine::Max),
Transform::from_xyz(0.0, -300.0, 0.0),
Mesh2d(meshes.add(Rectangle::new(500.0, 20.0))),
MeshMaterial2d(materials.add(Color::from(WHITE))),
));
} }
#[derive(Component, Default)]
#[require(Collider, Mesh2d, MeshMaterial2d<ColorMaterial>, Restitution(|| Restitution::new(1.0)), RigidBody, TransformInterpolation, Transform)]
struct GameObject;
#[derive(Component, Default)]
#[require(GameObject, RigidBody(|| RigidBody::Dynamic))]
struct Ball;
fn setup_balls( fn setup_balls(
mut commands: Commands, mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>, mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
) { ) {
let circle = Circle::new(30.0); let mut rng = rand::rng();
let mesh = meshes.add(circle); for _ in 0..20 {
let circle = Circle::new(rng.random_range(10.0..(PLAYER_SIZE - 5.)));
let mut transform = Transform::from_xyz(
0.,
rng.random_range((PLAYER_SIZE + 5.)..(AREA_WIDTH / 2. - PLAYER_SIZE)),
0.,
);
transform.rotate_around(
Vec3::ZERO,
Quat::from_rotation_z(rng.random_range(0.0..(PI * 2.))),
);
commands.spawn((
Ball,
Collider::from(circle),
Mesh2d(meshes.add(circle)),
MeshMaterial2d(materials.add(Color::from(RED_400))),
transform,
));
}
}
// This entity uses transform interpolation. #[derive(Component, Default)]
commands.spawn(( #[require(Ball, RigidBody(|| RigidBody::Dynamic))]
Name::new("Interpolation"), struct Player;
Ball,
RigidBody::Dynamic,
Collider::from(circle),
TransformInterpolation,
Transform::from_xyz(-100.0, 300.0, 0.0),
Mesh2d(mesh.clone()),
MeshMaterial2d(materials.add(Color::from(CYAN_400)).clone()),
));
// This entity uses transform extrapolation. fn setup_player(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
let circle = Circle::new(PLAYER_SIZE);
commands.spawn(( commands.spawn((
Name::new("Extrapolation"), Player,
Ball,
RigidBody::Dynamic,
Collider::from(circle), Collider::from(circle),
TransformExtrapolation, Mesh2d(meshes.add(circle)),
Transform::from_xyz(0.0, 300.0, 0.0), MeshMaterial2d(materials.add(Color::from(LIME_400))),
Mesh2d(mesh.clone()),
MeshMaterial2d(materials.add(Color::from(LIME_400)).clone()),
));
// This entity is simulated in `FixedUpdate` without any smoothing.
commands.spawn((
Name::new("No Interpolation"),
Ball,
RigidBody::Dynamic,
Collider::from(circle),
Transform::from_xyz(100.0, 300.0, 0.0),
Mesh2d(mesh.clone()),
MeshMaterial2d(materials.add(Color::from(RED_400)).clone()),
)); ));
} }
/// Despawns all balls and respawns them. #[derive(Component, Default)]
fn reset_balls(mut commands: Commands, query: Query<Entity, With<Ball>>) { #[require(GameObject, RigidBody(|| RigidBody::Static))]
for entity in &query { struct Wall;
commands.entity(entity).despawn();
}
commands.run_system_cached(setup_balls); fn setup_walls(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
let thickness = 20.;
let width = AREA_WIDTH + thickness;
for i in 0..4 {
let mut transform = Transform::from_xyz(0., AREA_WIDTH / 2., 0.);
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_z((i as f32) * PI / 2.));
commands.spawn((
Wall,
Collider::rectangle(width, thickness),
Mesh2d(meshes.add(Rectangle::new(width, thickness))),
MeshMaterial2d(materials.add(Color::from(WHITE))),
transform,
));
}
} }
/// Changes the timestep of the simulation when the up or down arrow keys are pressed. fn move_player(
fn change_timestep(mut time: ResMut<Time<Fixed>>, keyboard_input: Res<ButtonInput<KeyCode>>) { time: Res<Time>,
if keyboard_input.pressed(KeyCode::ArrowUp) { keyboard_input: Res<ButtonInput<KeyCode>>,
let new_timestep = (time.delta_secs_f64() * 0.975).max(1.0 / 255.0); mut player: Query<&mut LinearVelocity, With<Player>>,
time.set_timestep_seconds(new_timestep); ) {
let acceleration = 500.;
let mut velocity = player.single_mut();
let delta_time = time.delta_secs();
if keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]) {
velocity.y += acceleration * delta_time;
} }
if keyboard_input.pressed(KeyCode::ArrowDown) { if keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]) {
let new_timestep = (time.delta_secs_f64() * 1.025).min(1.0 / 5.0); velocity.y -= acceleration * delta_time;
time.set_timestep_seconds(new_timestep); }
if keyboard_input.any_pressed([KeyCode::KeyA, KeyCode::ArrowLeft]) {
velocity.x -= acceleration * delta_time;
}
if keyboard_input.any_pressed([KeyCode::KeyD, KeyCode::ArrowRight]) {
velocity.x += acceleration * delta_time;
} }
} }
fn quit(mut exit: EventWriter<AppExit>) {
exit.send(AppExit::Success);
}