Lightning Gun VFX: Curved waypoint path

Ok, this one was a bit fiddly, this is due to having to manage each particle on a per frame basis and ensuring you have a valid reference to that particle's position in the path during its lifetime.

After trying multiple methods, which didn't work; ended up on deciding that the only way I could think for managing a waypoint system on a per particle basis, would be to do it based on distance, and each particle is to move towards the next waypoint from their closest one.

This I found, really reduced the complexity in the script, and in my logical process to figure out a working method.

First we need to generate the waypoints to the target, we already have a reference to the target, so we can use that in the generation of the path. Now, the whole point of this for me is to make its a curved path, so we have to do more than a simple lerp (as that would result in a straight line).

Let's create a List of Vector3's in the Global Variables, to keep track of all the waypoints.

// Global Variables
private List<Vector3> waypoints = new List<Vector3>();

This function generates the path that all particles will use to make their way to the target.
The way this function works, is that it calculates a Lerp between points A and B, and also B and C.
These get stored in a local dir variable.
Then in another local variable called path, we lerp between the two previous lerps. This is what gives us the curve.
This is a quadratic polynomial (if you would like to look it up, the real math behind it is a bit more complex than this).
At each step of the for loop, we add the current value of path to the waypoints list.
And finally we add in the actual enemy position after the loop, to ensure the waypoint path reaches the enemy.

    // To be called once per frame while the lightning gun is active and has a valid target.
    void ArcPath()
    {
        waypoints.Clear(); // we clear the list at the start as we want to generate a new path each time the function is called.

        var time = 0f;

        // for now I have decided to use 10 waypoints
        for (int i = 0; i < 10; i++)
        {
            if (i == 0)
                time = 0f;

            var dir1 = Lerp(transform.position, pointB.position, time);
            var dir2 = Lerp(pointB.position, enemy.transform.position, time);
            var path = Lerp(dir1, dir2, time);

            waypoints.Add(path);
            time += 0.1f;
        }
        waypoints.Add(enemy.transform.position);
    }

Instead of using the built in Vector3.Lerp, I created my own, just so I had more control over it if I needed to (this shouldnt be necessary).

    Vector3 Lerp(Vector3 a, Vector3 b, float t)
    {
        return a + (b - a) * t;
    }

Secondly, we need to calculate which waypoint is closest to the particle that we reference.

    // returns an int to be used to specify which element in the list of waypoints is closest
    // in order for this to work, and be relative to the particle we're currently referencing, we need to pass in its current position.
    int GetClosestWaypointTarget(Vector3 pos)
    {
        // default values
        int closestWaypoint = 0;
        float dist = Mathf.Infinity;

        // this is very similar to the targeting script, for each waypoint we check which is currently closest and update dist and closestWaypoint each time.
        for (int i = 0; i < waypoints.Count; i++)
        {
            float d = Vector3.Distance(pos, waypoints[i]);
            if (d < dist)
            {
                dist = d;
                closestWaypoint = i;
            }
        }
        // then we return closestWaypoint 
        return closestWaypoint;
    }

Finally, we update our previous code to make use of these new functions. I have also put the main code in it's own function called Fire(), which is called from Update().

    public override void Fire()
    {
        //if no target, stop here
        if (!hasTarget)
            return;

        // generate waypoint path
        ArcPath();

        if (lightningGun.isEmitting)
        {
            lightningGun.GetParticles(particles);

            // lerp particle position to target position
            for (int i = 0; i < particles.Length; i++)
            {
                // we need to know which waypoint we are travelling from, this is where we use the get waypoint method and pass in the particles position.
                int fromwaypoint = GetClosestWaypointTarget(particles[i].position);
                
                // we need to know where to send the particle to, this will generally be fromWaypoint +1, but when we are already on the second last waypoint, we need to just reassign 
                // that way point, to ensure we do not cause an index out of range exception.
                int toWaypointIndex = fromwaypoint==waypoints.Count-1 ? fromwaypoint :fromwaypoint + 1;

                // this is effectively the same as before, move towards the the target waypoint.
                var target = waypoints[toWaypointIndex];
                particles[i].position = Vector3.MoveTowards(particles[i].position, target, Time.deltaTime*speed);
            }
            lightningGun.SetParticles(particles, particles.Length);
        }
    }

There a still some quirks in this system.
As mentioned before, this is based on a quadratic polynomial, which is a bezier curve with 1 control point. If you have experience with bezier curves you will understand that when you move the control point you sometimes get unexpected results, i.e. the curve snapping to an inverted direction.

To resolve this I will need to put in some min/max limitations on where the pointB can be, and how close to the enemy we are allowed to be and auto target. Likely to limited the minimum range to start from 3 meters, to remove these issues.

So we still have a bit of work ahead of ourselves with finalising this VFX, buts this is where we're at right now, and we think it's looking pretty good (other than the colours). We also, need to add in a straight forward line waypoints, so when we are not targeting an enemy, we can still use our lightning gun.

Further tweaks and optimisations to come in future posts.