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().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;
}
}
Answer
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)
(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);
No comments:
Post a Comment