Skip to content

Avian2d: Sleeping body not being awoken by force #901

@bread-mountain4

Description

@bread-mountain4

avian2d: 0.4.1
bevy: 0.17.3
cargo 1.90.0 (840b83a10 2025-07-30)

I'm running into an issue where a dynamicbody is not being woken up by a waking force. The scenario that I'm expecting the dynamicbody to wake up in is the following: game starts and shows black loading screen, physics time is paused, user presses button when ready, physics time is unpaused and a countdown timer begins, when the timer ends a one-time force is applied to the body (which has gone to sleep). I've confirmed that the body is in fact sleeping after the force is applied using the egui inspector. The physics helper is the same as what's in the examples.

I created a test program which tests a couple of variants on this. The results are

| Delay  | Show loading screen (pauses physics time) | Works as expected (the body moves) |
| ------------- | ------------- |
| None  | Yes  | Yes |
| 2 sec  | Yes  | No |
| None  | No  | Yes |
| 2 sec  | No  | No |

Here is the test program. If there's something not ideal I'm doing in here, please let me know

use avian2d::prelude::*;
use bevy_inspector_egui::prelude::*;
use clap::{ArgAction, Parser};
use core::f32;
use physics_helper::HelperPlugin;
use std::path::Path;

use avian2d::math::{Vector, PI};
use avian2d::prelude::{CollisionEventsEnabled, RigidBody};
use bevy::{
    app::AppExit,
    prelude::*,
    window::{PresentMode, WindowResolution},
};

// consts
const WINDOW_WIDTH: u32 = 1200;
const WINDOW_HEIGHT: u32 = 800;
const UNIT_LENGTH: f32 = 32.0;
const CAPSULE_WIDTH: f32 = UNIT_LENGTH;
const CAPSULE_HEIGHT: f32 = UNIT_LENGTH * 2.5;

const PHYSICS_BODIES_Z_INDEX: f32 = 0.0;
const POP_IN_COVER_Z_INDEX: f32 = 1.0;
const TEXT_Z_INDEX: f32 = 2.0;

const HALF_WALL_THICKNESS: f32 = 2.0;
const LEFT_WALL: f32 = -(WINDOW_WIDTH as f32) / 2.0;
const RIGHT_WALL: f32 = (WINDOW_WIDTH as f32) / 2.0;

// y coordinates
const BOTTOM_WALL: f32 = -(WINDOW_HEIGHT as f32) / 2.0;
const TOP_WALL: f32 = (WINDOW_HEIGHT as f32) / 2.0;

/// the computed mass of having a default colliderdensity of 1.0
const MASS_UNITS: f32 = ((CAPSULE_WIDTH / 2.0) * (CAPSULE_WIDTH / 2.0) * PI)
    + ((CAPSULE_HEIGHT - CAPSULE_WIDTH) * CAPSULE_WIDTH);

const TEST_ACCELERATION: f32 = WINDOW_WIDTH as f32 / 2.0;

// cli args (debugging)
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// how long to wait before applying the test force
    #[arg(short = 'd', long, default_value = None)]
    delay_sec: Option<u32>,

    /// whether or not to use a pop-in cover (loading screen)
    #[arg(short = 'r', long, action=ArgAction::SetTrue)]
    remove_popin_cover: bool,
}

// game states
#[derive(States, Debug, Default, Clone, PartialEq, Eq, Hash)]
enum AppState {
    #[default]
    Loading,
    WaitForUser,
    SimulationRunning,
}

// components
#[derive(Component, Reflect, Debug, InspectorOptions)]
#[reflect(Component, InspectorOptions)]
struct Wall;

#[derive(Component, Reflect, Debug, InspectorOptions)]
#[reflect(Component, InspectorOptions)]
struct ReadyScreenText;

#[derive(Component, Reflect, Debug, InspectorOptions)]
#[reflect(Component, InspectorOptions)]
struct PopInCover;

#[derive(Component, Reflect, Debug, Default, InspectorOptions)]
#[reflect(Component, InspectorOptions)]
struct InitialForceToApply {
    force: Vec2,
    application_delay_time: Option<Timer>,
}

impl InitialForceToApply {
    fn new_with_delay(force: Vec2, application_delay_time: Timer) -> Self {
        Self {
            force,
            application_delay_time: Some(application_delay_time),
        }
    }
    fn new(force: Vec2) -> Self {
        Self {
            force,
            application_delay_time: None,
        }
    }
}

impl std::ops::Deref for InitialForceToApply {
    type Target = Option<Timer>;

    fn deref(&self) -> &Self::Target {
        &self.application_delay_time
    }
}

// resources

#[derive(Resource, Reflect, Clone, Debug, Default, InspectorOptions)]
#[reflect(Resource, InspectorOptions)]
struct SimulationAssets {
    font: Handle<Font>,
}

#[derive(Resource, Reflect, Clone, Debug, Default, InspectorOptions)]
#[reflect(Resource, InspectorOptions)]
struct SimulationParams {
    force_delay_sec: Option<u32>,
    use_popin_cover: bool,
}

impl From<Args> for SimulationParams {
    fn from(value: Args) -> Self {
        Self {
            force_delay_sec: value.delay_sec,
            use_popin_cover: !value.remove_popin_cover,
        }
    }
}

fn main() {
    let filename_only = Path::new(file!())
        .parent()
        .unwrap()
        .parent()
        .and_then(|s| s.to_str())
        .unwrap();

    let args = Args::parse();

    let mut app = App::new();

    app.add_plugins(
        DefaultPlugins
            .set(WindowPlugin {
                primary_window: Some(Window {
                    title: filename_only.into(),
                    resolution: WindowResolution::new(WINDOW_WIDTH, WINDOW_HEIGHT)
                        .with_scale_factor_override(1.0),
                    present_mode: PresentMode::AutoVsync,
                    resizable: false,
                    ..default()
                }),
                ..default()
            })
            .set(ImagePlugin::default_nearest()),
    )
    .init_state::<AppState>()
    .add_plugins(PhysicsPlugins::default().with_length_unit(UNIT_LENGTH))
    .add_plugins(HelperPlugin)
    .add_plugins(PhysicsDebugPlugin::default())
    .insert_resource(Gravity(Vector::ZERO))
    .insert_resource(SimulationAssets::default())
    .insert_resource(Into::<SimulationParams>::into(args))
    .add_systems(Startup, startup)
    .add_systems(
        Update,
        (
            wait_for_assets.run_if(in_state(AppState::Loading)),
            wait_for_user_to_start.run_if(in_state(AppState::WaitForUser)),
            apply_test_force.run_if(in_state(AppState::SimulationRunning)),
            close_on_q,
            close_on_esc,
        ),
    );

    app.run();
}

fn wait_for_user_to_start(
    mut next_state: ResMut<NextState<AppState>>,
    mouse_input: Res<ButtonInput<MouseButton>>,
    keyboard_input: Res<ButtonInput<KeyCode>>,
    ready_screen_text_ent: Single<Entity, With<ReadyScreenText>>,
    pop_in_cover: Single<Entity, With<PopInCover>>,
    mut commands: Commands,
    mut physics_time: ResMut<Time<Physics>>,
) {
    if keyboard_input.just_pressed(KeyCode::KeyS) || mouse_input.just_pressed(MouseButton::Left) {
        commands
            .get_entity(*ready_screen_text_ent)
            .unwrap()
            .despawn();
        commands.get_entity(*pop_in_cover).unwrap().despawn();
        next_state.set(AppState::SimulationRunning);
        physics_time.unpause();
    }
}

fn wait_for_assets(
    mut next_state: ResMut<NextState<AppState>>,
    simulation_assets: Res<SimulationAssets>,
    asset_server: Res<AssetServer>,
) {
    if let Some(load_state) = asset_server.get_load_state(simulation_assets.font.id()) {
        match load_state {
            bevy::asset::LoadState::Loaded => {
                next_state.set(AppState::WaitForUser);
            }
            bevy::asset::LoadState::Failed(asset_load_error) => {
                error!("failed to load asset {}", asset_load_error);
            }
            _ => (),
        }
    } else {
        warn!("could not find simulation assets on asset server");
    }
}

fn startup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    mut simulation_assets: ResMut<SimulationAssets>,
    simulation_params: Res<SimulationParams>,
    asset_server: Res<AssetServer>,
    mut physics_time: ResMut<Time<Physics>>,
    mut next_state: ResMut<NextState<AppState>>,
) {
    debug!("test from startup");
    commands.spawn(Camera2d);

    // the pop-in cover will hide the capsule being
    // spawned in, if not using then start the simulation immediately
    if simulation_params.use_popin_cover {
        physics_time.pause();
    }

    commands.spawn((
        Name::new("top wall"),
        Transform::from_xyz(0.0, TOP_WALL + HALF_WALL_THICKNESS, 0.0),
        Collider::rectangle(WINDOW_WIDTH as f32, HALF_WALL_THICKNESS * 2.0),
        CollisionEventsEnabled,
        Wall,
        RigidBody::Static,
    ));

    commands.spawn((
        Name::new("bottom wall"),
        CollisionEventsEnabled,
        Transform::from_xyz(0.0, BOTTOM_WALL - HALF_WALL_THICKNESS, 0.0),
        Collider::rectangle(WINDOW_WIDTH as f32, HALF_WALL_THICKNESS * 2.0),
        Wall,
        RigidBody::Static,
    ));

    commands.spawn((
        Name::new("left wall"),
        CollisionEventsEnabled,
        Wall,
        Transform::from_xyz(LEFT_WALL - HALF_WALL_THICKNESS, 0.0, 0.0),
        Collider::rectangle(HALF_WALL_THICKNESS * 2.0, WINDOW_HEIGHT as f32),
        RigidBody::Static,
    ));

    commands.spawn((
        Name::new("right wall"),
        Wall,
        Transform::from_xyz(RIGHT_WALL + HALF_WALL_THICKNESS, 0.0, 0.0),
        Collider::rectangle(HALF_WALL_THICKNESS * 2.0, WINDOW_HEIGHT as f32),
        CollisionEventsEnabled,
        RigidBody::Static,
    ));

    let capsule_radius = CAPSULE_WIDTH / 2.0;
    let mass = MASS_UNITS * 2.0;

    let mesh_handle = meshes.add(Capsule2d::new(capsule_radius, CAPSULE_HEIGHT));
    let pop_in_cover_color_handle = materials.add(Color::BLACK);
    let pop_in_cover_mesh_handle = meshes.add(Rectangle::new(
        (WINDOW_WIDTH + 1) as f32,
        (WINDOW_HEIGHT + 1) as f32,
    ));
    let material_handle = materials.add(Color::srgb(0.2, 0.7, 0.9));

    let initial_force = simulation_params
        .force_delay_sec
        .map(|delay| {
            InitialForceToApply::new_with_delay(
                Vec2::new(mass * -TEST_ACCELERATION, 0.0),
                Timer::from_seconds(delay as f32, TimerMode::Once),
            )
        })
        .unwrap_or(InitialForceToApply::new(Vec2::new(
            mass * -TEST_ACCELERATION,
            0.0,
        )));

    commands.spawn((
        Mesh2d(mesh_handle),
        Collider::capsule(capsule_radius, CAPSULE_HEIGHT),
        Mass(MASS_UNITS),
        CollisionEventsEnabled,
        Name::new("capsule"),
        initial_force,
        Transform::from_xyz(0.0, 0.0, PHYSICS_BODIES_Z_INDEX),
        RigidBody::Dynamic,
        Friction::ZERO.with_combine_rule(CoefficientCombine::Min),
        Restitution::ZERO.with_combine_rule(CoefficientCombine::Min),
        MeshMaterial2d(material_handle),
    ));

    if simulation_params.use_popin_cover {
        commands.spawn((
            Mesh2d(pop_in_cover_mesh_handle),
            Name::new("pop-in-cover"),
            MeshMaterial2d(pop_in_cover_color_handle),
            Transform::from_xyz(0.0, 0.0, POP_IN_COVER_Z_INDEX),
            PopInCover,
        ));

        simulation_assets.font = asset_server.load("Square.ttf");
        commands.spawn((
            Text::new("Click or press s to start"),
            Transform::from_xyz(0.0, 0.0, TEXT_Z_INDEX),
            TextFont {
                font: simulation_assets.font.clone(),
                font_size: 42.0,
                ..default()
            },
            ReadyScreenText,
        ));
    } else {
        next_state.set(AppState::SimulationRunning);
    }
}

fn apply_test_force(
    query_test_capsule: Query<(Entity, &Name, &mut InitialForceToApply, Forces)>,
    time: Res<Time>,
    mut commands: Commands,
) {
    for (entity, name, mut force_to_apply, mut forces) in query_test_capsule {
        let mut should_apply = true;
        if force_to_apply.application_delay_time.is_some() {
            force_to_apply
                .application_delay_time
                .as_mut()
                .unwrap()
                .tick(time.delta());
            should_apply = force_to_apply
                .application_delay_time
                .as_mut()
                .unwrap()
                .is_finished();
        }

        if !should_apply {
            trace!(
                "shouldn't apply the test force yet, timer is {:?}",
                force_to_apply.application_delay_time
            );
            return;
        }

        forces.apply_force(force_to_apply.force);
        debug!("applying force {0} to {1}", force_to_apply.force, name);
        commands.entity(entity).remove::<InitialForceToApply>();
    }
}

fn close_on_q(keyboard_input: Res<ButtonInput<KeyCode>>, mut exit: MessageWriter<AppExit>) {
    if keyboard_input.pressed(KeyCode::KeyQ) {
        exit.write(AppExit::Success);
    }
}

pub fn close_on_esc(
    mut commands: Commands,
    focused_windows: Query<(Entity, &Window)>,
    input: Res<ButtonInput<KeyCode>>,
) {
    for (window, focus) in focused_windows.iter() {
        if !focus.focused {
            continue;
        }

        if input.just_pressed(KeyCode::Escape) {
            commands.entity(window).despawn();
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-DynamicsRelates to rigid body dynamics: motion, mass, constraint solving, joints, CCD, and so onC-BugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions