Search code examples
libgdxbox2dprojectile

How to calculate initial velocity of projectile motion by a given distance and angle from point A to B?


Example image (This graph is not accurate but the idea or the parabola is like in there)

In the below image, green curved line is the projectile motion of a bullet, gray line is the shadow motion, red circle is the starting position and the blue circle is the destination. Currently I am able to achieve the shadow moving, but unfortunately the projectile motion is not right just like the second image below. Please see the link this is how I currently move the bullet.

enter image description here

Example GIF (This is the result I wanted!)

In the below GIF, when the bullet is fired, the bullet has a projectile motion and still moving towards the cross-hair center point.

enter image description here

To move the bullet to its destination

In the below code, is the method to move the bullet to its destination with a constant given speed of 6 meters. To be able to achieve the above image trajectory I need to calculate the speed by a given distance.

float bulletSpeed = 6; // In the mean time the speed is constant, but this should be calculated base on distance and angle
Vector2 touchPoint = new Vector2(input.touchpoint.x, input.touchpoint.y);
Vector2 targetDirection = touchPoint.cpy().sub(bulletPosition).nor();

Vector2 targetVelocity = targetDirection.cpy().scl(bulletSpeed);
float targetAngle = targetDirection.angle();
float targetDistance = touchPoint.dst(bulletPosition);

body.setLinearVelocity(targetVelocity);

To draw the projected trajectory

There's no problem here, this is base on the iforce2d projected trajectory example.

startingPosition.set(originPoint); // Bullet Position
startingVelocity.set(targetDirection); // Calculated target direction

shape.setProjectionMatrix(CameraManager.getInstance().getCamera().combined);
shape.begin(ShapeRenderer.ShapeType.Point);
    for (int i = 0; i < 180; i++) { // three seconds at 60fps
        Vector2 trajectoryPosition = getTrajectoryPoint(startingPosition, startingVelocity, i);
        shape.point(trajectoryPosition.x, trajectoryPosition.y, 0);
    }
shape.end();

Getting the trajectory point

public Vector2 getTrajectoryPoint(Vector2 startingPosition, Vector2 startingVelocity, float n) {
    Vector2 gravity = WorldManager.getWorld().getGravity();
    float t = 1 / 60.0f; // seconds per time step (at 60fps)
    Vector2 stepVelocity = startingVelocity.cpy().scl(t); // m/s
    Vector2 stepGravity = gravity.cpy().scl(t * t); // m/s/s
    Vector2 trajectoryPoint = new Vector2();
    trajectoryPoint.x = (startingPosition.x + n * stepVelocity.x + 0.5f * (n * n + n) * stepGravity.x);
    trajectoryPoint.y = (startingPosition.y + n * stepVelocity.y + 0.5f * (n * n + n) * stepGravity.y);
    return trajectoryPoint;
}

The graphical results (As you can see the I am not getting the desired results..)

enter image description here

Possible solution (I found this solution Calculating initial velocities given a trajectory parabola, but I'm having hard time converting this to box2d.)

enter image description here

Update (8/6/2016) Additional details for projection reference

In the below image is the graphical projection of top-down (oblique).

enter image description here


Solution

  • This answer uses concepts common in game development, so it may be suitable to migrate this question to GameDev.StackExchange.


    1. Coordinate systems

    Because you're trying to imitate 3D movement in a 2D display, a helpful initial step is to separate your concepts of world space and screen space

    • World Space is the virtual coordinate system in which your game's world is laid out and in which its mechanics are simulated. To model arcing movement of a projectile that has a height off the 2D ground plane, you'd want to use a 3D coordinate system (x, y, z).

      To minimize conflicts with your current scheme, let's say z is the direction pointing "up" off the (x,y) floor plane you're currently using. If you don't need to model objects passing over/under each other, then you can keep simulating your physics in 2D with just the (x,y) components, implicitly treating the z coordinate as 0.

    • Screen Space is the coordinate system you actually use for rendering. To take a 3D world space coordinate and convert it to the 2D plane of your screen, you'll need to apply a projection. For a game like the one you've shown, you may want to use a top-down oblique projection, somemthing like....

      screenPos.x = (worldPos.x - cameraOffset.x) * zoomFactor; screenPos.y = (worldPos.y + worldPos.z - cameraOffset.y) * zoomFactor;

      This will represent your world with no foreshortening (so a circle on the floor becomes a circle on your screen, and jumping 1m up displaces the character the same as walking 1m "North").

    If you want a more realistic look, you can multiply worldPos.y & worldPos.z by some coefficients less than 1.0 to model foreshortening (so a circle on the floor becomes an ellipse on the screen)

    2. Modelling the problem in two parts

    With this distinction in mind, we can think of the projectile in your example as two separate parts:

    • The shadow travels along the 2D plane of the floor, (x, y, 0). Since your physics are 2D, it makes sense to treat the shadow as the "real" projectile, controlled by your Box2D physics, and base your collisions on its movement.

      ie. When the projectile's shadow intersects a target's footprint, the projectile has hit the target.

      (If you want higher fidelity, so a projectile can be lobbed over a short object to land on the floor on the other side, you'll either need to move to a 3D physics simulation, or use some height checks to selectively ignore collisions)

    • The ballistic bullet is the part that arcs through the air. We can think of this as a purely visual effect, with no associated physics collisions. (To see why, look at the example gif when the character fires downward: the bullet initially flies up behind him - but we wouldn't want the bullet to hit an enemy behind the player when they're trying to fire downward in front of their character)

    3. The shadow

    This behaves much the same way you're probably already handling straight-firing bullets in your game. Just point it in the direction from your muzzle to your target and set its velocity. Basic 2D physics without gravity will take it from there.

    float bulletSpeed = 6;
    Vector2 touchPoint = ScreenToWorldPoint(input.touchpoint.x, input.touchpoint.y);
    Vector2 targetOffset = touchPoint.cpy().sub(firingPosition);
    Vector2 targetDirection = targetOffset.cpy().nor();
    
    Vector2 targetVelocity = targetDirection.cpy().scl(bulletSpeed);
    
    shadowBody.setLinearVelocity(targetVelocity);
    

    Since the game mechanics here are 2D, I'd recommend keeping the firing speed constant as you have it above. This will make the behaviour of the weapon more consistent & predictable for the player. To shoot a real weapon further we'd often aim it upward, sacrificing some horizontal velocity for vertical to counter gravity, so as our target moves away our time-to-hit grows non-linearly. In the case of this game though the ballistic arc is really just an artificial visual effect, so it's debatable whether adding this physical realism actually makes it play better. I'd try it without and see whether you miss it. ;)

    4. The ballistic bullet's arc

    There are a few choices for how we style the arc. We could try to always hit a particular height, or maintain a particular launch velocity, etc. For what follows, I'm going to suggest using a constant gravity value, and choosing an initial upward velocity just sufficient to touch down when the shadow reaches input.touchPoint. This will tend to give shallower arcs/straighter shots at nearby targets, and higher lobs when shooting far away.

    First, some tunable constants:

    // Tune this to get the arcs the height/sharpness you want.
    float gravity = 9.8f;
    
    // Tune this until it matches the height of your character's muzzle off the ground.
    float launchHeight = 1.0f;
    

    Next, we spawn the bullet at the appropriate point above the spot on the floor where it should be fired:

    bullet.worldPos.x = firingPosition.x;
    bullet.worldPos.y = firingPosition.y;
    bullet.worldPos.z = launchHeight;
    

    Now, based on the values we calculated above for moving the shadow, we can calculate the initial upward velocity of the bullet:

    float distance = targetDirection.dot(targetOffset); // 2D range to target
    float duration = distance/bulletSpeed;              // Time until hit
    
    // Initialize its vertical (z) velocity to create the arc we want.
    // See section 5 below for how this formula is derived.
    bullet.verticalVelocity = duration * gravity/2 - launchHeight/duration;
    

    Now every frame (after each physics step) we can do the following to update the bullet to the appropriate position above its shadow:

    bullet.worldPos.x = shadow.worldPos.x;
    bullet.worldPos.y = shadow.worldPos.y;
    bullet.verticalVelocity -= gravity * deltaTime;
    bullet.worldPos.z += bullet.verticalVelocity * deltaTime;
    
    // Convert to screen space using a projection like the oblique style described above.
    bullet.screenPos = Project(bullet.worldPos);
    

    Here I'm using Euler integration to model the arc, which is simple, but could show approximation errors if you have a low/uneven framerate. Probably not a big deal for this kind of visual effect, but if you want higher precision you can track the time-of-fire or time-in-air and use the parametric equation for h(t) below to trace the arc exactly.

    5. Derivation (optional)

    In case you're curious how we calculate the initial velocity above:

    We know we want a parabola that hits zero at time t = duration, with a curvature due to gravity. That gives us a quadratic with the following factored form....

    h(t) = -g/2 * (t - duration) * (t - p)
    

    ...for some unknown p. Expanding...

    h(t) = (-g/2) * p * duration + (g/2)*(duration + p) * t - (g/2) * t*t
    

    setting t = 0 gives us the initial launch height, which we can solve for p

    h(0) = (-g/2) * p * duration
    p = -2 * h(0) / (g * duration)
    

    Substituting into the expanded form of h(t) above, we get...

    h(t) = h(0) + ((g/2)*duration - h(0)/duration) * t - (g/2) * t*t
    

    And that middle term that's linear in t is the initial vertical velocity. (The first term is the initial height and the last term is the acceleration due to gravity)