8
\$\begingroup\$

I am trying to launch an object at a target, given its position, its target position, the launch speed, and the gravity. I am following this formula from Wikipedia:

$$ \theta = arctan \bigg( \frac{v^2 \pm \sqrt{v^4-g(gx^2 + 2yv^2)}}{gx} \bigg) $$

I have simplified the code to the best of my ability, but I still cannot consistently hit the target. I am only considering the taller trajectory, of the two available from the +- choice in the formula.

Does anyone know what I am doing wrong?

using UnityEngine;

public class Launcher : MonoBehaviour
{
    public float speed = 10.0f;

    void Start()
    {
        Launch(GameObject.Find("Target").transform);
    }

    public void Launch(Transform target)
    {
        float angle = GetAngle(transform.position, target.position, speed, -Physics2D.gravity.y);
        var forceToAdd = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * speed;
        GetComponent<Rigidbody2D>().AddForce(forceToAdd, ForceMode2D.Impulse);
    }

    private float GetAngle(Vector2 origin, Vector2 destination, float speed, float gravity)
    {
        float angle = 0.0f;

        //Labeling variables to match formula
        float x = Mathf.Abs(destination.x - origin.x);
        float y = Mathf.Abs(destination.y - origin.y);
        float v = speed;
        float g = gravity;

        //Formula seen above
        float valueToBeSquareRooted = Mathf.Pow(v, 4) - g * (g * Mathf.Pow(x, 2) + 2 * y * Mathf.Pow(v, 2));
        if (valueToBeSquareRooted >= 0)
        {
            angle = Mathf.Atan((Mathf.Pow(v, 2) + Mathf.Sqrt(valueToBeSquareRooted)) / g * x);
        }
        else
        {
            //Destination is out of range
        }

        return angle;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Two things stand out to me. -Physics2D.gravity.y, and angle = Mathf.Atan((Mathf.Pow(v, 2) + Mathf.Sqrt(valueToBeSquareRooted)) / g * x);, the formula expects gravity to be a positive value such as 9.81, the second is the denominator gx, the way you have it, you divide by g, then multiply time x, you should have the denominator (g*x) so the multiply occurs before the division. \$\endgroup\$ – Mike White Dec 19 '16 at 21:05
11
\$\begingroup\$

I'm a bit skeptical of using atan here, because the tangent ratio shoots off to infinity at certain angles, and may lead to numerical errors (even outside of the undefined/divide by zero case for shooting straight up/down).

Using the formulae worked out in this answer, we can parametrize this in terms of the (initially unknown) time to impact, T, using the initial speed of the projectile:

// assuming x, y are the horizontal & vertical offsets from source to target,
// and g is the (positive) gravitational acceleration downwards
// and speed is the (maximum) launch speed of the projectile...

b = speed*speed - y * g
discriminant = b*b - g*g * (x*x + y*y)

if(discriminant < 0)
  return CANNOT_REACH_TARGET; // Out of range, need higher shot velocity.

discRoot = sqrt(discriminant);

// Impact time for the most direct shot that hits.
T_min = sqrt((b - discRoot) * 2 / (g * g));

// Impact time for the highest shot that hits.
T_max = sqrt((b + discRoot) * 2 / (g * g));

You can choose either T_min or T_max (or something in-between if you want to fire with speeds up to but not necessarily equal to some maximum)

Example trajectories

(T_min is the shallow red trajectory at the bottom, and T_max is the tall green one. Any trajectory between them is viable at some feasible speed. When the two merge into the yellow trajectory, the object is out of range.)

Now that we've calculated a value for T, the rest is straightforward:

vx = x/T;
vy = y/T + T*g/2;

velocity = (vx, vy);

You can use this velocity directly (it has a length equal to speed by construction), or if you really need to know the angle, you can use atan2(vy, vx)


Edit: to make this applicable to more cases, here's a 3D version:

Vector3 toTarget = target.position - transform.position;

// Set up the terms we need to solve the quadratic equations.
float gSquared = Physics.gravity.sqrMagnitude;
float b = speed * speed + Vector3.Dot(toTarget, Physics.gravity);    
float discriminant = b * b - gSquared * toTarget.sqrMagnitude;

// Check whether the target is reachable at max speed or less.
if(discriminant < 0) {
    // Target is too far away to hit at this speed.
    // Abort, or fire at max speed in its general direction?
}

float discRoot = Mathf.Sqrt(discriminant);

// Highest shot with the given max speed:
float T_max = Mathf.Sqrt((b + discRoot) * 2f / gSquared);

// Most direct shot with the given max speed:
float T_min = Mathf.Sqrt((b - discRoot) * 2f / gSquared);

// Lowest-speed arc available:
float T_lowEnergy = Mathf.Sqrt(Mathf.Sqrt(toTarget.sqrMagnitude * 4f/gSquared));

float T = // choose T_max, T_min, or some T in-between like T_lowEnergy

// Convert from time-to-hit to a launch velocity:
Vector3 velocity = toTarget / T - Physics.gravity * T / 2f;

// Apply the calculated velocity (do not use force, acceleration, or impulse modes)
projectileBody.AddForce(velocity, ForceMode.VelocityChange);
\$\endgroup\$
  • \$\begingroup\$ Right, I have found solutions by plugging in time as a known, but I want force to be the known. \$\endgroup\$ – Evorlor Jan 9 '16 at 15:28
  • 1
    \$\begingroup\$ Yea, Jost Petrie and @DMGregory are the gaints inthis forum. :) no doubt \$\endgroup\$ – Hamza Hasan Jan 9 '16 at 16:26
  • 1
    \$\begingroup\$ Aw, shucks, thank you both! :) @Evorlor discRoot is the square root of the discriminant, which is the portion that appears under the square root sign in the quadratic formula. b is -1 times the variable b in the quadratic formula. Unfortunately I don't know a more descriptive name for it. (I multiplied it by -1 when assigning to neaten the later steps, since the leading minus is already baked-in and doesn't affect the squaring). See the other answer for a full derivation, though a couple of squares are missing (will fix shortly) \$\endgroup\$ – DMGregory Jan 9 '16 at 16:51
  • 1
    \$\begingroup\$ What do the blue and yellow curve represent? \$\endgroup\$ – Slipp D. Thompson Jan 10 '16 at 9:04
  • 3
    \$\begingroup\$ @SlippD.Thompson the yellow curve is the most efficient trajectory (least launch velocity needed), and the blue curve is the highest trajectory within a fixed ceiling (useful if you need to avoid playfield bounds or arcing off-screen). Equations for these time values are in the linked answer \$\endgroup\$ – DMGregory Jan 10 '16 at 14:04
2
\$\begingroup\$

Thanks to DMGregory, I now have a C# extension script which can be used for this. Most recent version can be found on GitHub.

using UnityEngine;

public static class Rigidbody2DExtensions
{
    /// <summary>
    /// Applies the force to the Rigidbody2D such that it will land, if unobstructed, at the target position.  The arch [0, 1] determines the percent of arch to provide between the minimum and maximum arch.  If target is out of range, it will fail to launch and return false; otherwise, it will launch and return true.  This only takes the Y gravity into account, and X gravity will not affect the trajectory.
    /// </summary>
    public static bool SetTrajectory(this Rigidbody2D rigidbody2D, Vector2 target, float force, float arch = 0.5f)
    {
        Mathf.Clamp(arch, 0, 1);
        var origin = rigidbody2D.position;
        float x = target.x - origin.x;
        float y = target.y - origin.y;
        float gravity = -Physics2D.gravity.y;
        float b = force * force - y * gravity;
        float discriminant = b * b - gravity * gravity * (x * x + y * y);
        if (discriminant < 0)
        {
            return false;
        }
        float discriminantSquareRoot = Mathf.Sqrt(discriminant);
        float minTime = Mathf.Sqrt((b - discriminantSquareRoot) * 2) / Mathf.Abs(gravity);
        float maxTime = Mathf.Sqrt((b + discriminantSquareRoot) * 2) / Mathf.Abs(gravity);
        float time = (maxTime - minTime) * arch + minTime;
        float vx = x / time;
        float vy = y / time + time * gravity / 2;
        var trajectory = new Vector2(vx, vy);
        rigidbody2D.AddForce(trajectory, ForceMode2D.Impulse);
        return true;
    }
}
\$\endgroup\$
-6
\$\begingroup\$

Personally, I wouldn't even bother using any type of complicated formula.

GetComponent<Rigidbody2D>.AddForce((target.transform.position - transform.position) * someSortOfMultiplier());

It just shoots it in the direction of the target. And if you want to compensate for gravity, distance, etc, set someSortOfMultiplier() to be a function that returns a float which will compensate when multiplied with the above code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.