Trying to throw an object along a parabola over a specific duration (a specific way)

Pretty sure I ended up picking a very stupid way to try to solve this, but here was my stab at it.

  • Throw an object {a cupcake} over an arc, to any given destination, but it must reach that destination over a given duration.
  • “My” throw code (not mine) does not account for predicting how long it will take to reach that duration.
  • So my idea was to fire a sort-of tracer (empty gameObject) along the same throw arc, really really fast, before the throw itself.
  • Take the duration that ‘tracer’ takes, divide it into the original throw speed, and that’s the new throw speed. Then throw. Simple enough.

Conceptually it’s kinda like this:

// let the tracer's time from starting to reaching a destination be, say, 0.037
// let intended throwTime be 0.5

float throwSpeed = 100f;
throwSpeed *= (tracerLifeTime / (throwTime);

// 100 *= (0.037 /  (0.5)
// 100 *= 0.074
// throwSpeed gets adjusted to 7.4, or 13.51x slower.
// resulting throw should now take (0.037 * 13.51) sec, or ~0.5 sec
// (the total time the tracer + throw takes would be ~0.537, but that can just be adjusted)

I’ve hit a wall trying to re-configure that calculation. It only sort of works, because -

  1. If you aim really closeby so the tracer takes like 0.000012 sec, the throw speed becomes nearly 0.
  2. Even if you aim further away, properly, the throw time will always take way longer than it should. An intended 0.5 can take anywhere from 0.5 to well over a second. What the hell am I missing?

At this point, all I know is:
1(a). With that calculation, the more the initial tracer speed is reduced (to a point) (e.g. 100 → 90 → 80…), the more the throw’s actual duration matches up with its intended duration.
1(b). But vastly reducing the initial tracer’s speed makes the total time (for the tracer + throw) nowhere close to the intended throw time, which kinda defeats the whole purpose.
2. I can’t find any hiccups anywhere except with the calculation for adjusting throwSpeed.
3. This idea was terrible.

It feels like I’m missing something very fundamental here. Any assistance is appreciated (including better ideas, of course).

Here’s my messy applicable method snippets. Hopefully they makes sense. Thanks.

IEnumerator TracerAdjustsThrowSpeed(Vector3 destination)
    {
        if (aimingCupcake)
        {
            aimingCupcake = false;
            tracerHasFinished = false;
            tracerStartTime = Time.realtimeSinceStartup;
            tracerPos.position = spawnPoint;
        }
      

        while (!tracerHasFinished)
        {
            float startPos_Z = spawnPoint.z;
            float destinationPos_Z = destination.z;
            float stepDistance_Z = destinationPos_Z - startPos_Z;

            Vector3 nextStep = Vector3.MoveTowards(tracerPos.position, destination, throwSpeed * Time.deltaTime);
         
            float arc = arcHeight * (nextStep.z - startPos_Z) * (nextStep.z - destinationPos_Z) / (-0.40f * stepDistance_Z * stepDistance_Z);          

            float baseY = Mathf.Lerp(spawnPoint.y, destination.y, (nextStep.z - startPos_Z) / stepDistance_Z);          

            Vector3 nextPos = new Vector3(nextStep.x, baseY + arc, nextStep.z);
            tracerPos.position = nextPos;


            if (tracerPos.position == destination) {
                tracerArrivedTime = Time.realtimeSinceStartup;
                var airTime = Mathf.Abs((tracerArrivedTime - tracerStartTime));
              

                var speedMultiplier = airTime / throwTime;   // ADJUST FOR TRACER TIME LATER.

                throwSpeed *= Mathf.Abs(speedMultiplier);

                tracerHasFinished = true;
                throwTo = StartCoroutine(ThrowTo(destination));
                StopCoroutine(adjustThrowSpeedWithTracer);
                adjustThrowSpeedWithTracer = null;
            }
            yield return null;
        }
        yield return null;  
    }



    IEnumerator ThrowTo(Vector3 destination)
    {     
        while (tracerHasFinished)
        {
            if (adjustThrowSpeedWithTracer != null)
                StopCoroutine(adjustThrowSpeedWithTracer);

            if (!cupcakeThrown)
            {
                throwStartTime = Time.realtimeSinceStartup;              
                cupcakeThrown = true;

                spawnedCupcake = Instantiate(cupcakePrefab, spawnPoint, Quaternion.identity);
                allActiveCupcakes.Add(spawnedCupcake);
            }

            else if (spawnedCupcake != null)
            {
                float startPos_Z = spawnPoint.z;
                float destinationPos_Z = destination.z;
                float stepDistance_Z = destinationPos_Z - startPos_Z;
              
                Vector3 nextStep = Vector3.MoveTowards(spawnedCupcake.transform.position, destination, throwSpeed * Time.deltaTime);
              
                // set to use Z-axis

                float arc = arcHeight * (nextStep.z - startPos_Z) * (nextStep.z - destinationPos_Z) / (-0.40f * stepDistance_Z * stepDistance_Z);
              
                float baseY = Mathf.Lerp(spawnPoint.y, destination.y, (nextStep.z - startPos_Z) / stepDistance_Z);          

                Vector3 nextPos = new Vector3(nextStep.x, baseY + arc, nextStep.z);
                spawnedCupcake.transform.position = nextPos;

                if (spawnedCupcake.transform.position == destination) {
                    // tracerAndThrowCompletedTime = Time.realtimeSinceStartup;
                    // Debug.Log("full time for throw = " + (tracerAndThrowCompletedTime - startedTracerTime));
                    // throwEndTime = Time.realtimeSinceStartup;
                    // var animationThrowTime = Mathf.Abs(throwEndTime - throwStartTime);
                    // Debug.Log("animation throw time = " + animationThrowTime);
                    // Debug.Log("throw took " + Mathf.Round(throwEndTime - throwStartTime) + " seconds");

                    throwSpeed = originalThrowSpeed;
                    cupcakeThrown = false;
                    spawnedCupcake = null;
                    tracerCanSpawn = true;
                    tracerHasFinished = false;

                    yield break;
                }
            }
            yield return null;
        }

        yield return null;
    }

I only read your long post quickly and I am only on my mobile phone because I am in vacation, but this pseudocode could potentially help you out:

float timetoend = Time.time + duration;

while(Time.time < timetoend)
{
    yield return null;
    float lerp = 1 - (timetoend - Time.time) / duration;
    Vector3 targetpos = Vector3.Lerp(startpos, endpos lerp);
}

The Lerp is exchangeable with a SmoothDamp. If you need an alternate version based on additive deltapositions instead, let me know :).

1 Like

Thanks for the input.

I’m not sure I understand how this works, or know to interject this. As far as I can tell, “duration” is “airTime”, and if it takes less than a frame, the throw wouldn’t happen at all (and it needs to).

I wrote you a complete code sample below.
To test it, just attach the script to a GameObject and assign a prefab to m_throwPrefab.
This example uses the GameObjects forward vector as the throw direction.
It’s not physically correct but it does the job :).
Hope this helps.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Thrower : MonoBehaviour
{
    [SerializeField] GameObject m_throwPrefab;
    [SerializeField] float m_maxThrowDistance = 30.0f;
    [SerializeField] float m_arcFactor = 0.5f;
    [SerializeField] float m_throwDurationInSeconds = 0.5f;

    // Start is called before the first frame update
    void Start()
    {
       
    }

    // Update is called once per frame
    void Update()
    {
        if(UnityEngine.InputSystem.Keyboard.current[UnityEngine.InputSystem.Key.E].wasReleasedThisFrame)
        {
            Vector3? destination = findDestination();
            if (destination.HasValue)
            {
                GameObject throwObjectInstance = Instantiate(m_throwPrefab, this.transform.position, this.transform.rotation, null);
                StartCoroutine(throwFromTo(throwObjectInstance.transform, this.transform.position, destination.Value, m_throwDurationInSeconds));
            }
        }
    }

    Vector3? findDestination()
    {
        RaycastHit hit;
        Ray ray = new Ray(this.transform.position, this.transform.forward);

        if(Physics.Raycast(ray, out hit, m_maxThrowDistance)) return hit.point;

        ray = new Ray(this.transform.position + this.transform.forward * m_maxThrowDistance * Vector3.Dot(this.transform.up, Vector3.up), -Vector3.up);
        if (Physics.Raycast(ray, out hit, m_maxThrowDistance)) return hit.point;

        return null;
    }

    IEnumerator throwFromTo(Transform target, Vector3 start, Vector3 destination, float duration)
    {
        Vector3 arcVector = computeArcVector(start, destination, m_arcFactor);
        Vector3 halfDestination = start + (destination - start) * 0.5f;

        float halfDuration = duration * 0.5f;
        float timeToEnd = Time.time + duration;
        float timeToEndHalf = Time.time + halfDuration;
        while (Time.time < timeToEndHalf)
        {
            yield return null;

            float lerp = 1 - (timeToEndHalf - Time.time) / halfDuration;
            lerp = Mathf.Clamp(lerp, 0, 1);

            Vector3 arc = Vector3.Lerp(Vector3.zero, arcVector, Mathf.Sin(lerp * Mathf.PI * 0.5f));
            target.transform.position = Vector3.Lerp(start, halfDestination, lerp) + arc;
        }

        while (Time.time < timeToEnd)
        {
            yield return null;

            float lerp = 1 - (timeToEnd - Time.time) / halfDuration;
            lerp = Mathf.Clamp(lerp, 0, 1);

            Vector3 arc = Vector3.Lerp(Vector3.zero, arcVector, Mathf.Cos(lerp * Mathf.PI * 0.5f));
            target.transform.position = Vector3.Lerp(halfDestination, destination, lerp) + arc;
        }

        Destroy(target.gameObject);
    }

    Vector3 computeArcVector(Vector3 start, Vector3 destination, float arcFactor)
    {
        Vector3 distVec = destination - start;
        Vector3 forward = distVec.normalized;
        Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
        Vector3 up = Vector3.Cross(forward, right).normalized;
        return up * distVec.magnitude * 0.5f * arcFactor;
    }
}
1 Like

Wow, that works perfectly. Thanks a bunch! :slight_smile:

1 Like