2
\$\begingroup\$

I'm trying to remake an old flash game using Unity. The main mechanic of the game is to make the basketball pass over the mouse.

Here's a video of the game: https://www.youtube.com/watch?v=6sArMC1l_Sc

I've been trying to get the hang of this mechanic for two whole days and I still haven't been able to reproduce it. This is what I've been able to make so far. (forgive me for the colors, my screen gives weird colors when recording it)

progress in unity

As you will see, I managed to calculate the trajectory of the ball, and calculate the speed to follow that trajectory. What bothers me is that the vertex of the drawn parabola is not placed on top of the mouse, but it is displaced to the left. The further left the mouse is from the ball, the more this delta between the vertex and the mouse is pronounced.

I think this is happening because I calculate the initial velocity U using only the angle α and the maximum height the ball will reach hmax. I have not found a formula that allows me to "force" the (x,y) point of the mouse as the vertex of the parabola and gives me a way to calculate the velocity I need to apply to the ball for it to reach that (x,y) a its highest peak.

I have written a list the parameters and formulas for the SUVAT equations I have found throughout my research on the internet, so I'll list them right below.

    /*
     * SUVAT EQUATIONS
     * S    | horizontal distance traveled
     * U    | initial velocity
     * Ux   | initial horizontal velocity
     * Uy   | initial vertical velocity
     * V    | final velocity
     * A    | standard acceleration due to gravity on the surface of the earth (≈ 9.807 m/s^2)
     * T    | travel time
     * h    | initial height
     * hmax | maximum height
     * α    | release angle relative to horizontal
     * 
     * 
     * When h = 0:
     * 
     * Ux = U * cos(α)
     * Uy = U * sin(α)
     * T = 2 * Uy / A
     * hmax = (v^2 sin^2(α))/(2 * A)
     * hmax = Uy^2 / (2 * A)
     * S = (v^2 sin(2 α))/A (old)
     * S = 2 * Ux * Uy / A
     * 
     * S  = Ux * T
     * Ux = S/T
     * Uy = (hmax/T + (1/2)) * A * T
     * T = (2 v sin(α))/A
     * 
     * When h != 0:
     * 
     * Ux = U * cos(α)
     * Uy = U * sin(α)
     * t = (Uy + sqrt(Uy^2 + 2 * A * h)) / A
     * S = Ux * [Uy + sqrt(Uy^2 + 2 * A * h)] / A
     * hmax = (h + Uy²) / (2 * A)
     * Uy = sqrt( (hmax - h) / (2*A))
     * U = sqrt(V - 2A * hmax) 
    */

Here's a recap of the equations I am using to have this result:

Angle Calculation

    private float GetAngleInDegreesBetweenMouseAndObject2()
    {
        _mouseWorldPosition = GetMousePositionFrom3DWorld();
        _mouseWorldPosition.z = transform.position.z;

        _distanceBetweenObjectAndMouse = Vector3.Distance(transform.position, _mouseWorldPosition);
        _mouseDirection = _mouseWorldPosition - transform.position;

        var angle = Vector2.SignedAngle(Vector2.left, _mouseDirection);
        Debug.Log($"angle: {angle}");
        return angle;
    }

    private Vector3 GetMousePositionFrom3DWorld()
    {
        // I have drawn a 2D plane in order to be able to get an accurate mouse position, with its Z axis being the same all the way.
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (_launchArcPlane.Raycast(ray, out float distance))
        {
            return ray.GetPoint(distance);
        }
        return transform.position;
    }

Maximum Height Calculation

    private float GetMaximumHeightForObject()
    {
        return _mouseWorldPosition.y - transform.position.y;
    }

Initial Velocity Calculation

    private void CalculateInitialVelocityUsingFinalVelocityMaximumHeightAndAngle()
    {
        _initialVelocity = Mathf.Sqrt(2 * _gravity * _maximumHeight) / Mathf.Sin(_angleInRadians);
    }

Initial Velocity Vector Calculation

    private void CalculateVelocityVectorUsingInitialVelocityAndAngle()
    {
        _initialVelocityVector = new Vector3(_initialVelocity * Mathf.Cos(_angleInDegrees * Mathf.Deg2Rad), _initialVelocity * Mathf.Sin(_angleInDegrees * Mathf.Deg2Rad), 0);
    }

Setting Velocity To Ball On Click

    private void SetVelocityAndGravityForObject()
    {
        _hasShot = true;
        _RigidBody.useGravity = true;
        _RigidBody.velocity = _initialVelocityVector;
    }

Rest Of The Code

Here's the rest of the code ^^ In case anyone needs any more information in order to get further into this. Beware though, it has some unused functions from the trial an error of different equations I have found throughout the week-end: https://gist.github.com/Juansero29/f3f5a0a7e6c68588dd97ea1fe5d9b59e

I'm an application developer, and I'm interested in the world of games, but I had never tried to achieve a physical mechanic like this, and it's giving me a lot of headaches ^^ (I can't read no more SUVAT equations this week-end lol). Any help from fellow physicians or game develops would be very much appreciated

Edit: Here's the result after using DMGregory's help! Thanks a lot! enter image description here

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I think we can do this more simply. This demo uses about 80 lines of code (the interesting ones being just the dozen below), instead of over 300, with no angles/trigonometry functions needed:

enter image description here

At the time \$t^*\$ when we reach the vertex, we know the acceleration due to gravity has completely wiped out our vertical launch velocity:

$$\begin{align} v_y + a_g t^* &= 0\\ v_y &= - a_g t^* \end{align}$$

We can substitute this into our equation for the position, and get a relation between \$t^*\$ and the height of our vertex above the launch position \$h = p_y(t^*) - p_y(0)\$:

$$\begin{align} p_y(t) &= p_y(0) + v_y t + \frac 1 2 a_g t^2\\ p_y(t^*) - p_y(0) &= (-a_g t^*) t^* + \frac 1 2 a_g {t^*}^2\\ h &= -a_g {t^*}^2 + \frac 1 2 a_g {t^*}^2\\ h &= \frac {-1} 2 a_g {t^*}^2\\ \frac {-2 h} {a_g} &= {t^*}^2 \end{align}$$

We can also use that same equation to get a formula for the vertical velocity (I express it this way because it lets us calculate the horizontal and vertical components together later):

$$\begin{align} h &= v_y t^* + \frac 1 2 a_g {t^*}^2\\ h - \frac 1 2 a_g {t^*}^2 &= v_y t^*\\ \frac {h - \frac 1 2 a_g {t^*}^2} {t^*} &= v_y \end{align}$$

Vector3? GetLaunchVelocityToVertex(Vector3 launchSite, Vector3 vertex) {
    Vector3 travelToVertex = vertex - launchSite;

    float timeToPeakSquared = -2f * travelToVertex.y / Physics.gravity.y;
    // If your gravity is not always vertical, you can use this instead:
    // Vector3 down = Physics.gravity.normalized;
    // float timeToPeakSquared = -2f * Vector3.Dot(travelToVertex, down)
    //                           / Vector3.Dot(Physics.gravity, down);

    // If this is zero then the peak is at the same height as the launch point.
    // If it's less than zero, the peak is below, so we have no valid arc there.
    if (timeToPeakSquared <= 0f) return null;

    float timeToPeak = Mathf.Sqrt(timeToPeakSquared);

    return (travelToVertex - 0.5f * Physics.gravity * timeToPeakSquared)
           / timeToPeak;   
}

Use this velocity to plot your parabolic arc with the equation of motion, something like...

float previewTimeSpan = 3f * timeToPeak;
float timeStep = previewTimeSpan / (_parabolaPoints.Length - 1);

for (int i = 0; i < _parabolaPoints.Length; i++) {
    float t = i * timeStep;
    _parabolaPoints[i] = launchSite
                       + launchVelocity * t
                       + 0.5f * Physics.gravity * t * t;
}

Now one little trick: the symplectic physics integrators used by Unity's PhysX and Box2D physics engines won't trace this arc exactly if you give them this launch velocity as-is. That's because of the way they update the velocity first, then update the position using the velocity as of the end of the frame, rather than its average value over the whole frame. In effect, they add an extra half a physics step's worth of acceleration to the initial velocity.

For most purposes this tiny time shift is unnoticeable, so it's a sensible optimization to make the physics simulation faster. For cases where we care about tracing a predicted arc perfectly, we can correct for it by just increasing our initial velocity ever so slightly:

projectile.velocity = launchVelocity
                    - 0.5f * Physics.gravity * Time.fixedDeltaTime;
\$\endgroup\$
8
  • \$\begingroup\$ This is a gem. Bookmarking this post! \$\endgroup\$ Oct 11, 2021 at 14:28
  • \$\begingroup\$ Thanks! Questions about projectile physics are one of my favourite genres, so you might find others you like in there. 😁 \$\endgroup\$
    – DMGregory
    Oct 11, 2021 at 14:41
  • \$\begingroup\$ I haven't tried it but your answer is so well crafted that its logical that it's correct. I mark it as valid answer! Thanks a lot DMGregory. I'll come back and post the result once I apply your technique. \$\endgroup\$
    – Juansero29
    Oct 11, 2021 at 14:50
  • 1
    \$\begingroup\$ Looks like you made an error when copying. It's not - 0.5f, it's - 0.5f * Physics.gravity * timeToPeakSquared. Physics.gravity is a vector, so this is a legal vector-vector subtraction. Because the x and z components of the gravity are (conventionally) zero, this leaves the horizontal part of the travel vector unchanged. So in those axes we get horizontal travel divided by time = horizontal velocity. The vertical component is the only one that gets adjusted, matching the formula I showed. That's how this form lets us calculate the vertical and horizontal components at once, as I alluded. \$\endgroup\$
    – DMGregory
    Oct 11, 2021 at 16:58
  • 1
    \$\begingroup\$ (As an fun bonus, this format also allows it to work with arbitrary gravity, not just vertical, but you'd need to make an adjustment to the timeToPeakSquared computation if you wanted to include gravity that's not purely vertical. I've shown this in an edit. Using that formulation, the entire solution becomes "coordinate free" - it works in any coordinate basis, without assuming gravity happens on the y or any other specific axis.) \$\endgroup\$
    – DMGregory
    Oct 11, 2021 at 16:59

You must log in to answer this question.

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