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

View file

@ -1,152 +1,145 @@
//! This example showcases how `Transform` interpolation or extrapolation can be used
//! 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 std::f32::consts::PI;
use avian2d::{math::*, prelude::*};
use avian2d::{math::Vector, prelude::*};
use bevy::{
color::palettes::{
css::WHITE,
tailwind::{CYAN_400, LIME_400, RED_400},
tailwind::{LIME_400, RED_400},
},
input::common_conditions::input_pressed,
prelude::*,
};
use rand::Rng;
const AREA_WIDTH: f32 = 750.;
const PLAYER_SIZE: f32 = 30.;
fn main() {
let mut app = App::new();
// Interpolation and extrapolation functionality is enabled by the `PhysicsInterpolationPlugin`.
// It is included in the `PhysicsPlugins` by default.
app.add_plugins((
DefaultPlugins,
PhysicsPlugins::default().with_length_unit(50.0),
));
// By default, interpolation must be enabled for each entity manually
// 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))
App::new()
.add_plugins((
DefaultPlugins,
PhysicsPlugins::default()
.with_length_unit(50.0)
.set(PhysicsInterpolationPlugin::interpolate_all()),
))
.add_systems(
Startup,
(setup_scene, setup_player, setup_balls, setup_walls),
)
.add_systems(
Update,
(
change_timestep,
// Reset the scene when the 'R' key is pressed.
reset_balls.run_if(input_pressed(KeyCode::KeyR)),
),
);
// Run the app.
app.run();
(move_player, quit.run_if(input_pressed(KeyCode::KeyQ))),
)
.insert_resource(Gravity(Vector::ZERO))
.run();
}
#[derive(Component)]
struct Ball;
fn setup_scene(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
// Spawn a camera.
fn setup_scene(mut commands: Commands) {
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(
mut commands: Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
let circle = Circle::new(30.0);
let mesh = meshes.add(circle);
let mut rng = rand::rng();
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.
commands.spawn((
Name::new("Interpolation"),
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()),
));
#[derive(Component, Default)]
#[require(Ball, RigidBody(|| RigidBody::Dynamic))]
struct Player;
// 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((
Name::new("Extrapolation"),
Ball,
RigidBody::Dynamic,
Player,
Collider::from(circle),
TransformExtrapolation,
Transform::from_xyz(0.0, 300.0, 0.0),
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()),
Mesh2d(meshes.add(circle)),
MeshMaterial2d(materials.add(Color::from(LIME_400))),
));
}
/// Despawns all balls and respawns them.
fn reset_balls(mut commands: Commands, query: Query<Entity, With<Ball>>) {
for entity in &query {
commands.entity(entity).despawn();
}
#[derive(Component, Default)]
#[require(GameObject, RigidBody(|| RigidBody::Static))]
struct Wall;
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 change_timestep(mut time: ResMut<Time<Fixed>>, keyboard_input: Res<ButtonInput<KeyCode>>) {
if keyboard_input.pressed(KeyCode::ArrowUp) {
let new_timestep = (time.delta_secs_f64() * 0.975).max(1.0 / 255.0);
time.set_timestep_seconds(new_timestep);
fn move_player(
time: Res<Time>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut player: Query<&mut LinearVelocity, With<Player>>,
) {
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) {
let new_timestep = (time.delta_secs_f64() * 1.025).min(1.0 / 5.0);
time.set_timestep_seconds(new_timestep);
if keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]) {
velocity.y -= acceleration * delta_time;
}
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);
}