[SOLVED] Trajectory Approximation (Line Renderer)

Hi,

The goal is to paint the flight path before the player launches the projectile.

My first idea was to use Unity itself to draw the path for me so I don’t have to mess with the formula but I have not found any API for doing that. If there is any I have overlooked, please help me out here.

So I started looking and I have found many posts here and all over the internet. I tried different equations and code I found and finally wrote my own implementation but I never got the result Unity does. One reason for my own formula was that the others I found didn’t include mass and drag where I could remove mass (I think) but not drag.

The flight path is drawn properly, input and control feels kinda good, but when you actually launch the projectile it either flies a tiny bit too far (mostly) or not far enough.

So I am pretty close and have no idea why I can’t have nearly exact same results.

To be precise the algorithm takes the rigidbodys properties (velocity, mass, center of mass and drag) and the gravity vector into account.

public void SimulatePath(GameObject gameObject, Vector3 forceDirection, float mass, float drag)
    {
        Rigidbody rigidbody = gameObject.GetComponent<Rigidbody>();

        //float timestep = Time.fixedDeltaTime / Physics.defaultSolverIterations * stepSize;
        float timestep = Time.fixedDeltaTime * stepSize;
        //float timestep = stepSize;

        float stepDrag = 1 - drag * timestep;
        Vector3 velocity = forceDirection / mass * timestep;
        Vector3 gravity = Physics.gravity * stepDrag * timestep;
        Vector3 position = gameObject.transform.position + rigidbody.centerOfMass;


        Vector3[] segments = new Vector3[maxSegmentCount];
        int numSegments = 0;

        segments[0] = position;
        numSegments++;

        for (int i = 1; i < maxSegmentCount && position.y > 0f; i++)
        {
            velocity *= stepDrag;

            //position += velocity * 1.28f; // ????? - This came pretty close at one configuration
            position += velocity;
            position += gravity;

            segments[i] = position;

            numSegments++;
        }

        Draw(gameObject.transform, segments, numSegments);
    }

I pass mass and drag as parameter because they might be affected before I then launch the projectile but will be set before launch. The simulation values and the launch values match.

maxSegmentCount can be set in the editor as well as stepSize.

The Draw(…) call will then set up the line renderer.

Disclaimer: The code has changed over time but this is the current version with the best results. There are things about this formula which I am not sure about and which I would think of being different.

For example messing with the timesteps makes often things go worse. I’d assume changing the timestep should not change the drawn path except for it to become more bumpy or more smooth.

I have a headache looking at how I use gravity. I found approaches having timestep ^ 2 which could make sense but the value gets too small for a float and ends up being 0.0000xx and the path looks like shooting linear into the sky. In my code the gravity is linear. I think that gravity is a constant force over time where the loop takes care of the time. What I did not try is instead of timestep ^ 2 using elapsedTime ^ 2 or have a gravitational velocity which would make more sense right now.
Gravity is also affected by drag as you can see in my code so I apply constant gravity with drag but velocity decreases over time.

Update while writing the post: I wanted to try the gravitational velocity:

Vector3 gravitationalVelocity = new Vector3(0f, 0f, 0f);
Vector3 gravity = Physics.gravity * stepDrag * timestep * timestep;

for (int i = 1; i < maxSegmentCount && position.y > 0f; i++)
{
    gravitationalVelocity += gravity;
    position += gravitationalVelocity;
}

But same thing here.

It made so much sense writing the initial formula but the more I look at that the more I would try change.

It took me hours so far searching for problems and at the moment I am kinda blind right now. I cant see straight and started to just try around because of despair and need some help.

Thank you so much for opening my eyes.

I solved one problem. It is now 100% accurate. But when I change stepSize to reduce the amount of calculations, the projectile flies way further than predicted.

public void SimulatePath(GameObject gameObject, Vector3 forceDirection, float mass, float drag)
    {
        Rigidbody rigidbody = gameObject.GetComponent<Rigidbody>();

        float timestep = Time.fixedDeltaTime * stepSize;

        float stepDrag = 1 - drag * timestep;
        Vector3 velocity = forceDirection / mass * timestep;
        Vector3 gravity = Physics.gravity * timestep * timestep;
        Vector3 position = gameObject.transform.position + rigidbody.centerOfMass;

        if (segments == null || segments.Length != maxSegmentCount)
        {
            segments = new Vector3[maxSegmentCount];
        }
        int numSegments = 0;

        segments[0] = position;
        numSegments++;

        for (int i = 1; i < maxSegmentCount && position.y > 0f; i++)
        {
            velocity += gravity;
            velocity *= stepDrag;

            position += velocity;

            segments[i] = position;

            numSegments++;
        }

        Draw(segments, numSegments);
    }

Timesteps now just skipped via modulo.

Just to have it complete and if anyone else needs this:

public class TrajectorySimulation : MonoBehaviour
{
    public LineRenderer lineRenderer;

    public int maxIterations = 10000;
    public int maxSegmentCount = 300;
    public float segmentStepModulo = 10f;

    private Vector3[] segments;
    private int numSegments = 0;

    public bool Enabled
    {
        get
        {
            return lineRenderer.enabled;
        }
        set
        {
            lineRenderer.enabled = value;
        }
    }

    public void Start()
    {
        Enabled = false;
    }

    public void SimulatePath(GameObject gameObject, Vector3 forceDirection, float mass, float drag)
    {
        Rigidbody rigidbody = gameObject.GetComponent<Rigidbody>();

        float timestep = Time.fixedDeltaTime;

        float stepDrag = 1 - drag * timestep;
        Vector3 velocity = forceDirection / mass * timestep;
        Vector3 gravity = Physics.gravity * timestep * timestep;
        Vector3 position = gameObject.transform.position + rigidbody.centerOfMass;

        if (segments == null || segments.Length != maxSegmentCount)
        {
            segments = new Vector3[maxSegmentCount];
        }

        segments[0] = position;
        numSegments = 1;

        for (int i = 0; i < maxIterations && numSegments < maxSegmentCount && position.y > 0f; i++)
        {
            velocity += gravity;
            velocity *= stepDrag;

            position += velocity;

            if (i % segmentStepModulo == 0)
            {
                segments[numSegments] = position;
                numSegments++;
            }
        }

        Draw();
    }

    private void Draw()
    {
        Color startColor = Color.magenta;
        Color endColor = Color.magenta;
        startColor.a = 1f;
        endColor.a = 1f;

        lineRenderer.transform.position = segments[0];

        lineRenderer.startColor = startColor;
        lineRenderer.endColor = endColor;

        lineRenderer.positionCount = numSegments;
        for (int i = 0; i < numSegments; i++)
        {
            lineRenderer.SetPosition(i, segments[i]);
        }
    }
}
5 Likes

@Silberling this was fantastic!

The only problem for me was that the velocity is a bit too large. I figured that it must be somewhat related to the scale of my thrower object.
Even though I ended up using a mix of gameObject.transform.localScale.x and a magic number (i.e. `localScale.x / 10f), it now works perfectly!

Thanks :smile:

Hi, I absolutely love this snippet but while testing it out I found a mistake which lead me to this unity documentation sentence:

A common mistake is to assume that heavy objects fall faster than light ones. This is not true as the speed is dependent on gravity and drag. (Unity - Scripting API: Rigidbody.mass)

You should not use the mass for the curve at all because the mass does not effect the flight at all (it just comes in to place for collisions):

Line 36:

Vector3 velocity = forceDirection / mass * timestep;

to

Vector3 velocity = forceDirection * timestep;

or remove mass in total.

Then it started working out for me :slight_smile:

Here is my final snippet (just create a sphere with a rigidbody and drag it as prefab to enable shooting):

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

[RequireComponent(typeof(LineRenderer))]
public class ParabolicProjection : MonoBehaviour
{
    [Header("Check 'Enable Test Shoot' and press [Space] in Play Mode")]
    public bool enableTestShoot = false;
    public Rigidbody projectilePrefab;

    [Space]
    public float force = 50;

    private Vector3[] segments;
    private int numSegments = 0;
    private int maxIterations = 10000;
    private int maxSegmentCount = 300;
    private float segmentStepModulo = 10f;

    private Rigidbody currentProjectilePrefab;
    private LineRenderer lineRenderer;

    public void ChangeProjectile(Rigidbody newProjectile)
    {
        currentProjectilePrefab = newProjectile;
    }

    public void Shoot()
    {
        Rigidbody projectile = Instantiate(currentProjectilePrefab);
        projectilePrefab.transform.position = transform.position;
        projectile.transform.rotation = transform.rotation;
        projectile.velocity = transform.forward * force;
    }

    private void Awake()
    {
        lineRenderer = GetComponent<LineRenderer>();
        currentProjectilePrefab = projectilePrefab;
    }


    private void Update()
    {
        SimulatePath(transform.gameObject, transform.forward * force, currentProjectilePrefab.drag);

        if (Input.GetKeyDown(KeyCode.Space) && enableTestShoot)
        {
            Shoot();
        }
    }

    private void SimulatePath(GameObject obj, Vector3 forceDirection, float drag)
    {
        float timestep = Time.fixedDeltaTime;

        float stepDrag = 1 - drag * timestep;
        Vector3 velocity = forceDirection * timestep;
        Vector3 gravity = Physics.gravity * timestep * timestep;
        Vector3 position = obj.transform.position;

        if (segments == null || segments.Length != maxSegmentCount)
        {
            segments = new Vector3[maxSegmentCount];
        }

        segments[0] = position;
        numSegments = 1;

        for (int i = 0; i < maxIterations && numSegments < maxSegmentCount; i++)
        {
            velocity += gravity;
            velocity *= stepDrag;

            position += velocity;

            if (i % segmentStepModulo == 0)
            {
                segments[numSegments] = position;
                numSegments++;
            }
        }

        Draw();
    }

    private void Draw()
    {
        Color startColor = Color.magenta;
        Color endColor = Color.magenta;
        startColor.a = 1f;
        endColor.a = 1f;

        lineRenderer.transform.position = segments[0];

        lineRenderer.startColor = startColor;
        lineRenderer.endColor = endColor;

        lineRenderer.positionCount = numSegments;
        for (int i = 0; i < numSegments; i++)
        {
            lineRenderer.SetPosition(i, segments[i]);
        }
    }
}
1 Like