rustbevy

Calculating acceleration to stop at a destination point in a given amount of time


I'm programming a game in the Bevy engine, and I'm trying to move an object from a starting point to a destination point in a certain amount of time. The starting position, destination position, initial speed, and time the move should take are all passed in to build_move_to. That function builds a MoveTo struct that holds the destination point and velocity, both Vec2s, and an acceleration vector that is calculated when the MoveTo struct is built.

pub struct MoveTo {
    pub destination: Vec2,
    pub velocity: Vec2,
    pub acceleration: Vec2,
}

pub fn build_move_to(builder: MoveToBuilder) -> MoveTo {
    let acceleration = find_accel_to_stop_at_destination(
        builder.start,
        builder.destination,
        builder.speed,
        builder.time
    );
    let diff = builder.destination - builder.start;
    let angle = diff.y.atan2(diff.x);
    let movement_direction = Vec3::new(angle.cos(), angle.sin(), 0.0);
    MoveTo {
        destination: builder.destination,
        velocity: builder.speed * movement_direction.truncate(),
        acceleration,
    }
}

fn find_accel_to_stop_at_destination(start: Vec2, dest: Vec2, speed: f32, time: f32) -> Vec2 {
    let displacement = dest - start;
    let velocity = displacement.normalize() * speed;
    let acceleration = (2.0 * (displacement - (velocity * time))) / (time * time);
    acceleration
}

Finally, I have a do_move function that runs every frame and moves the object's Transform (its position in the world) towards the destination point:

impl MovementPattern for MoveTo {
    fn do_move(&mut self, transform: &mut Transform, time: &Res<Time>) -> () {
        // stop object when it gets close enough to the destination
        if transform.translation.truncate().abs_diff_eq(self.destination, 1.0) {
            self.velocity = Vec2::ZERO;
            return;
        }

        let delta_time = time.delta_secs();
        self.velocity += self.acceleration * delta_time;
        let translation_delta = self.velocity * delta_time;
        transform.translation += translation_delta.extend(0.0);
    }
}

The problem is that the velocity is well above zero when the object reaches the destination. For example, say builder.start is [-128.0, 241.0], the builder.destination is [-128.0, 166.0], builder.speed is 100, and the builder.time is 10.0 (in seconds). The acceleration is calculated as [0, 18.5], and the velocity starts at [-0, -100] (or very close to it). I believe the acceleration is correct, but the object reaches the destination point way too soon, after only two seconds or so, and it's velocity then is [-0, -85]. I want it to reach the destination at [0, 0] velocity at exactly ten seconds. What am I doing wrong?


Solution

  • In constant acceleration motion, you have variable: distance, initial velocity, final velocity, acceleration, duration. Due to the mathematical property of the motion, you can only constrain 3 variables to make a motion.

    by constraining distance (start and end), inital velocity and time, you gave up control over final velocity.

    Example

    depend on your desired outcome, you can pick two more variables to constrain (since start and end already exists). i.e. from_dt_v0, from_dt_vf, from_v0_vf.

    use bevy::prelude::*;
    
    fn main() {
        App::new()
            .add_plugins(DefaultPlugins)
            .add_systems(Startup, startup)
            .add_systems(Update, update)
            .run();
    }
    
    #[derive(Debug, Component)]
    pub struct MoveTo {
        pub direction: Vec3,
        pub velocity: f32,
        pub acceleration: f32,
    
        pub duration: f32,
        pub elapsed: f32,
    }
    
    impl MoveTo {
        // create a movement by constraining duration and inital velocity
        fn from_dt_v0(src: Transform, dst: Transform, dt: f32, v0: f32) -> MoveTo {
            let elapsed = 0.0;
    
            let diff = dst.translation - src.translation;
            let dist = diff.length();
            let direction = diff.normalize();
    
            let velocity = v0;
            let duration = dt;
            let acceleration = (dist - velocity * dt) * 2.0 / dt / dt;
    
            MoveTo {
                direction,
                velocity,
                acceleration,
                duration,
                elapsed,
            }
        }
    
        // create a movement by constraining inital and final velocity
        fn from_dt_vf(src: Transform, dst: Transform, dt: f32, vf: f32) -> MoveTo {
            let elapsed = 0.0;
    
            let diff = dst.translation - src.translation;
            let dist = diff.length();
            let direction = diff.normalize();
    
            let velocity = dist * 2.0 / dt - vf;
            let duration = dt;
            let acceleration = (vf - velocity) / dt;
    
            MoveTo {
                direction,
                velocity,
                acceleration,
                duration,
                elapsed,
            }
        }
    
        // create a movement by constraining duration and final velocity
        fn from_v0_vf(src: Transform, dst: Transform, v0: f32, vf: f32) -> MoveTo {
            let elapsed = 0.0;
    
            let diff = dst.translation - src.translation;
            let dist = diff.length();
            let direction = diff.normalize();
    
            let velocity = v0;
            let duration = dist * 2.0 / (vf + v0);
            let acceleration = (vf - v0) / duration;
    
            MoveTo {
                direction,
                velocity,
                acceleration,
                duration,
                elapsed,
            }
        }
    }
    
    fn src() -> Transform {
        Transform::from_xyz(-200.0, -160.0, 0.0)
    }
    fn dst() -> Transform {
        Transform::from_xyz(200.0, 160.0, 0.0)
    }
    
    fn startup(
        mut cmd: Commands,
        mut meshs: ResMut<Assets<Mesh>>,
        mut materials: ResMut<Assets<ColorMaterial>>,
    ) {
        cmd.spawn(Camera2d::default());
        // spawn destination marker
        cmd.spawn((
            Mesh2d(meshs.add(Circle::new(10.0))),
            MeshMaterial2d(materials.add(Color::hsl(0.0, 0.8, 0.8))),
            dst(),
        ));
        // spawn source marker
        cmd.spawn((
            Mesh2d(meshs.add(Circle::new(10.0))),
            MeshMaterial2d(materials.add(Color::hsl(0.0, 0.8, 0.8))),
            src(),
        ));
        
    
        // cmd.spawn((
        //     Mesh2d(meshs.add(Circle::new(5.0))),
        //     MeshMaterial2d(materials.add(Color::hsl(180.0, 0.8, 0.8))),
        //     src(),
        //     MoveTo::from_dt_v0(src(), dst(), 5.0, 10.0),
        // ));
    
        cmd.spawn((
            Mesh2d(meshs.add(Circle::new(5.0))),
            MeshMaterial2d(materials.add(Color::hsl(180.0, 0.8, 0.8))),
            src(),
            MoveTo::from_dt_vf(src(), dst(), 5.0, 0.0),
        ));
    
        // cmd.spawn((
        //     Mesh2d(meshs.add(Circle::new(5.0))),
        //     MeshMaterial2d(materials.add(Color::hsl(180.0, 0.8, 0.8))),
        //     src(),
        //     MoveTo::from_v0_vf(src(), dst(), 50.0, 0.0),
        // ));
    }
    
    fn update(
        mut cmd: Commands,
        mut query: Query<(&mut Transform, &mut MoveTo, Entity)>,
        time: Res<Time>,
    ) {
        let dt = time.delta_secs();
        for (mut t, mut m, id) in query.iter_mut() {
            if m.elapsed > m.duration {
                cmd.entity(id).despawn();
                continue;
            }
            m.velocity += m.acceleration * dt;
            t.translation += m.direction * m.velocity * dt;
            m.elapsed += dt;
        }
    }