13
\$\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\$
2
  • \$\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
    Commented Dec 19, 2016 at 21:05
  • \$\begingroup\$ The "answer" depends on how realistic you want to be. Not hitting a target "consistently" is consistent with using smoothbore (SB) artillery versus rifled artillery. As distance increases with SB, horizontal and vertical deviation increases exponentially; it's most effective when "skipped" over hard ground (solid shot vs shell). There is not enough information to commit to a single "calculation". Firing at targets involves probabilities; which vary with circumstances (e.g. "multipliers") \$\endgroup\$ Commented Mar 28 at 20:32

4 Answers 4

19
\$\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\$
12
  • \$\begingroup\$ Right, I have found solutions by plugging in time as a known, but I want force to be the known. \$\endgroup\$
    – Evorlor
    Commented Jan 9, 2016 at 15:28
  • 1
    \$\begingroup\$ Yea, Jost Petrie and @DMGregory are the gaints inthis forum. :) no doubt \$\endgroup\$ Commented Jan 9, 2016 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
    Commented Jan 9, 2016 at 16:51
  • 1
    \$\begingroup\$ What do the blue and yellow curve represent? \$\endgroup\$ Commented Jan 10, 2016 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
    Commented Jan 10, 2016 at 14:04
3
\$\begingroup\$

Thanks to DMGregory, I now have a C# extension script which can be used for this.

using UnityEngine;

/// <summary>
/// These are extensions for Rigidbody2Ds
/// </summary>
public static class Rigidbody2DExtensions
{
    private const float DefaultGravity = -9.8f;

    /// <summary>
    /// Applies a force to the Rigidbody2D such that it will land, if unobstructed, at the target position.
    /// The arch [0.0f, 1.0f] determines the percent of arch to provide between the minimum and maximum arch.
    /// An arch or 0 or 1 will set the trajectory such that the projectile is launched at max speed.
    /// Any value between 0 and 1 will have a reduced speed.
    /// If target is out of range, it will still fire, but not reach the target.
    /// Use rigidbody2D.IsWithinRange(Vector2, float) to determine if the target is within range ahead of time, if desired.
    /// This only takes the Y gravity into account, and X gravity will not affect the trajectory.
    /// If there is no gravity and no ConstantForce2D is attached, a ConstantForce2D with a gravity of (0.0f, -9.8f) will be attached.
    /// </summary>
    /// <param name="rigidbody2D">The rigidbody2D to apply the force to</param>
    /// <param name="target">The destination for the rigidbody2D</param>
    /// <param name="maxSpeed">The maximum speed to launch at</param>
    /// <param name="arch">The arch to apply to the rigidbody's trajectory</param>
    /// <returns></returns>
    public static void SetTrajectory(this Rigidbody2D rigidbody2D, Vector2 target, float maxSpeed, float arch = 0.5f)
    {
        arch = 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;
        if (gravity == 0)
        {
            var constantForce2D = rigidbody2D.GetComponent<ConstantForce2D>();
            if (!constantForce2D)
            {
                Debug.LogWarning("There is no gravity and " + rigidbody2D.name + " does not have a ConstantForce2D attached.  A ConstantForce2D with the default gravity of -9.8f will be added.");
                constantForce2D = rigidbody2D.gameObject.AddComponent<ConstantForce2D>();
                constantForce2D.force = new Vector2(0, DefaultGravity);
            }
            gravity = -constantForce2D.force.y;
        }
        float b = maxSpeed * maxSpeed - y * gravity;
        float discriminant = b * b - gravity * gravity * (x * x + y * y);
        discriminant = Mathf.Max(0, discriminant);
        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 velocity = new Vector2(vx, vy);
        velocity = Vector2.ClampMagnitude(velocity, maxSpeed);
        var impulse = velocity * rigidbody2D.mass;
        rigidbody2D.AddForce(impulse, ForceMode2D.Impulse);
    }

    /// <summary>
    /// Checks if the Rigidbody2D is within range such that if rigidbody2D.SetTrajectory(Vector2, float) is called on it, it will be able to reach its target.
    /// If there is no gravity and no ConstantForce2D is attached, a default gravity of -9.8f will be assumed.
    /// </summary>
    /// <param name="rigidbody2D">The rigidbody2D whose range will be checked</param>
    /// <param name="target">The target used while checking the range</param>
    /// <param name="maxSpeed">The maximum allowed launch speed</param>
    /// <param name="arch">The arch that would be used for the range check</param>
    /// <returns>Whether or not the Rigidbody2D would be within range with the supplied parameters</returns>
    public static bool IsWithinRange(this Rigidbody2D rigidbody2D, Vector2 target, float maxSpeed, float arch = 0.5f)
    {
        arch = 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;
        if (gravity == 0)
        {
            var constantForce2D = rigidbody2D.GetComponent<ConstantForce2D>();
            if (!constantForce2D)
            {
                Debug.LogWarning("There is no gravity and " + rigidbody2D.name + " does not have a ConstantForce2D attached.  The default gravity of -9.8f will be assumed.");
                gravity = -DefaultGravity;
            }
            else
            {
                gravity = -constantForce2D.force.y;
            }
        }
        float b = maxSpeed * maxSpeed - y * gravity;
        float discriminant = b * b - gravity * gravity * (x * x + y * y);
        return discriminant >= 0;
    }
}
\$\endgroup\$
0
2
\$\begingroup\$

I modified DMGregory and Evorlor's solution a bit as I wanted a 3D arc and wanted to be able to apply a minimum speed if the provided speed wouldn't reach.

    /// <summary>
    /// Applies an impulse that will have the rigidbody land at the specified target. The arch is the percent 
    /// of arch to provide between the min/max (0 to 1).  
    /// </summary>
    public bool ApplyTargetedForce(Rigidbody rRigidBody, Vector3 rTarget, float rSpeed, float rArc = 0.5f, bool rUseMinSpeed = true)
    {
        float lGravity = Physics.gravity.magnitude;
        Vector3 lObjectPosition = rRigidBody.position;
        Vector3 lToTarget = rTarget - lObjectPosition;

        // Find the minimum speed to hit the target
        float lDiscriminantSqrRt = Mathf.Sqrt((lGravity * lGravity) * lToTarget.sqrMagnitude);
        float lMinSpeed = Mathf.Sqrt(lDiscriminantSqrRt) - Vector3.Dot(lToTarget, Physics.gravity);
        if (rSpeed == 0f || (rUseMinSpeed && rSpeed < lMinSpeed)) { rSpeed = lMinSpeed + 0.5f; }

        // Using the speed, our factor for reaching the target
        float b = (rSpeed * rSpeed) + Vector3.Dot(lToTarget, Physics.gravity);
        float lDiscriminant = (b * b) - (lGravity * lGravity) * lToTarget.sqrMagnitude;
        if (lDiscriminant < 0) 
        {
            Debug.Log("Not enough force to reach target");
            return false; 
        }

        // Determine the min and max time it will take to reach the object.
        lDiscriminantSqrRt = Mathf.Sqrt(lDiscriminant);
        float lMinTime = Mathf.Sqrt((b - lDiscriminantSqrRt) * 2f) / lGravity;
        float lMaxTime = Mathf.Sqrt((b + lDiscriminantSqrRt) * 2f) / lGravity;

        // Determine the force based on our arc value
        Mathf.Clamp(rArc, 0f, 1f);
        float lTime = lMinTime + ((lMaxTime - lMinTime) * rArc);
        Vector3 lForce = new Vector3(lToTarget.x / lTime, (lToTarget.y / lTime) + (lTime * lGravity / 2f), lToTarget.z / lTime);

        // Apply the force
        rRigidBody.AddForce(lForce, ForceMode.Impulse);

        return true;
    }
\$\endgroup\$
-8
\$\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\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .