Unity 2D Curves using Triangles


Hi Xander here….

I know I shouldn’t be spending time doing this sort of stuff when I got games to make but I got really sidetracked with this little brain boiler. I got the idea while doing some maths research and came across an image of a cat’s cradle spun in a triangle. The way the lines joined made a perfect curve and I really liked the idea of doing something like that for making custom curves in games. I know the idea is probably not original and there has got to be some better implementations out there but once my noodle started working on this I got a little obsessed with seeing it through to the end.

I have written before about making curved movement by using sin functions and still think that’s a pretty cool way to do it. You can read about it here: (Fly Birdy Fly! 2D Curved Movement in Unity). But this is a much more intuitive way to get the perfect curve you want and very easy to plot and track the path of movement without having to guess.

This is how it works…

You take the three points of a triangle. I was thinking of something like a cannon shot, or lobbed object, or a flying arrow to start with so I called them Source, Height and Target. You measure the distance between those points and make lines to form a triangle. Then you cut those lines into equal points and start joining one point on one line to another point on the other line all the way down the length. It’s easier to explain in an image:

Building a Triangle and “Cat’s Cradle” lines to make a curve!

Now for the Mathy part… Once you get those lines drawn you use algebra to find the intersection point of one line and the next to get your curved path! Every additional line crosses the one before and by finding that point where they cross you get a list of points that make a curve.

Simple curves only need a few lines.

Five Lines

The more lines you use the smoother your line is…

Ten Lines
Twenty Lines

Start moving around those points of the triangle and it becomes really easy in the Unity Editor to Map and draw custom curves. This kind of blew my mind.

Different Types of Curves

Here we have a number of different curves all just by making a few tweaks to the position of those three points of the triangle. I’ve used the intersecting points to draw a parabolic line on the game scene below.

Here are few of the same images zoomed in (in case you are reading on your phone).

The Code

I’ll put the full script at the bottom of the post but for now I’ll work through the code a little bit.

If you want to copy the script you need to attach it to a GameObject that you want to move (or if you want to draw lines you need to attach it to a GameObject with a Line Renderer).

The script has a number of check boxes exposed in the editor which lets you control the movement and drawing functions as well as resetting and applying changes after moving the triangle’s points.

The only other variable that you can play with is the timeToHit float. This number controls how many lines you want to use to create the curve. Remember: The more lines the smoother the movement but the higher processing. (That said I’ve yet to do any serious profiling of the script but haven’t found any real performance hits yet).

Much of everything else is public so you can see what’s going on inside all the Lists and Arrays.

… … … (Editor View)

Defining the Triangle

First of all we get the positions of the three triangle points and find the length (Magnitude) of the lines between them using normal Vector maths.

Then we divide those lines by the number of strings we want to have (timeToHit) and work out the relative size of each one:

        Vector3 X_line = source - target;  
        X_line_length = Vector3.Magnitude(X_line);
        Vector3 Y_line = height - source;
        Y_line_length = Vector3.Magnitude(Y_line);
        Vector3 Y_Negline = target - height;
        Y_Negline_length = Vector3.Magnitude(Y_Negline);

        X_line_bit_x = (height.x - source.x ) / timeToHit;
        X_line_bit_y = (height.y - source.y) / timeToHit;
        Negline_bit_x = (target.x - height.x) / timeToHit;
        Negline_bit_y = (height.y - target.y) / timeToHit;

Get the Points Along Each Line

Next we iterate through all the points on the lines and make a pair of Lists (one for the forward or positively sloping line and one for the negatively sloped line):

        for (int i = 0; i < timeToHit + 1; i++)
        {
            P_lines.Add(new Vector3(Px, Py, 0f));
            Px += X_line_bit_x;
            Py += X_line_bit_y;

            Q_lines.Add(new Vector3(Qx, Qy, 0f));
            Qx += Negline_bit_x;
            Qy -= Negline_bit_y;
        }

Get Intersection Points

Getting the intersection points was much easier to do in 2D but is totally achievable if you wanted to extend it to 3D. We pass in our start and end points on each line (x and y coordinates) and return the intersection point (and convert it back to a Vector3):

            myPoint = findIntersectionPoints(
                (new Vector2(P_lines[i].x, P_lines[i].y)), 
                (new Vector2(Q_lines[i].x, Q_lines[i].y)),
                (new Vector2(P_lines[bc].x, P_lines[bc].y)), 
                (new Vector2 (Q_lines[bc].x, Q_lines[bc].y)));
            Vector3 myPoint_3 = new Vector3(myPoint.x, myPoint.y, 0f);
            IntersectionPoints.Add(myPoint_3);

(If you want to do more than idly read about this stuff have a look at Math Open Ref for more information on the functions for finding the intersection of two lines. I promise it’s actually really interesting.)

The maths bit:

float P1 =(Line2Point2.x - Line2Point1.x) * (Line1Point2.y - Line1Point1.y)
        - (Line2Point2.y - Line2Point1.y) * (Line1Point2.x - Line1Point1.x);

float P2 = ((Line1Point1.x - Line2Point1.x) * (Line1Point2.y -Line1Point1.y)
 - (Line1Point1.y - Line2Point1.y) * (Line1Point2.x - Line1Point1.x)) / P1;

return new Vector2(
            Line2Point1.x + (Line2Point2.x - Line2Point1.x) * P2,
            Line2Point1.y + (Line2Point2.y - Line2Point1.y) * P2);

That’s about it for the tricky stuff. There is a function to draw a line along the curved path and a function to move the attached object along the path as well. Add in a few Gui functions for displaying the pretty stuff in the scene view and you are done.

Moving the Green Sphere

This is an example of the script running in the editor that shows the scene view with the OnGui helper lines and then switches to the game view where I use the function to draw a curve and then move the green sphere along that path.

Full Script:

Here is the full script…enjoy!

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

public class CurveFunction : MonoBehaviour {

    public bool resetMe;    // Use these to manage the screen display
    public bool updateMe;
    public bool drawMe;
    public bool moveMe;

    public GameObject Source;  // The three points of the triangle
    public GameObject Target;
    public GameObject Height;

    public Vector3 source;  // The three points of the triangle
    public Vector3 target;
    public Vector3 height;

    public float timeToHit;     // A variable use to split the lines of the triangle into equal parts
    public int targetreached = 0;

    public float X_line_length;     // The length of the horizontal line between source and target
    public float Y_line_length;     // length from source to height
    public float Y_Negline_length;  // length from height to target (Negative slope of the triangle)
    public float X_line_bit_x;      // the x and (below) y points of the X_Line.  
    public float X_line_bit_y;
    public float Negline_bit_x;     // the x and (below) y points of the Negline.
    public float Negline_bit_y;
    public float[] X_line_bit_xs;
    public float[] X_line_bit_ys;
    public float[] Negline_bit_ys;
    public List<Vector3> P_lines = new List<Vector3>();     // A List of points on the Y_Line 
    public List<Vector3> Q_lines = new List<Vector3>();     // Same for the Negline
    public List<Vector3> IntersectionPoints = new List<Vector3>();  // Where two lines cross

    public float Px;        // Used as shorthand for points on the lines when calculating
    public float Py;
    public float Qx;
    public float Qy;
    public bool isFound;
    public float speed;         // Used for Draw function
    public LineRenderer lineRend;
    public int bc;

    // Use this for initialization
    void Start () {

        source = Source.transform.position;
        height = Height.transform.position;
        target = Target.transform.position;
        getPointsOnTriangle();
        Px = source.x;
        Py = source.y;
        Qx = height.x;
        Qy = height.y;
        makeLineArrays();
    }

// Update is called once per frame
void Update () {

        if (updateMe)
        {
            getPointsOnTriangle();
            makeLineArrays();
            updateMe = false;
        }

        if (moveMe)
        {
            MoveMe();
        }

        if (drawMe)
        {
            drawLines();
        }

        if (resetMe)
        {
            ResetMe();    
        }
    }

    void getPointsOnTriangle ()
    {
        source = Source.transform.position;
        height = Height.transform.position;
        target = Target.transform.position;

        // Define the lines of the triangle and get their lengths
        Vector3 X_line = source - target; 

        X_line_length = Vector3.Magnitude(X_line);
        Vector3 Y_line = height - source;
        Y_line_length = Vector3.Magnitude(Y_line);
        Vector3 Y_Negline = target - height;
        Y_Negline_length = Vector3.Magnitude(Y_Negline);

        // Time to hit is not really a time but an increment of how many times we want to cut the line into 
        // chunks to make the lines from. The more lines the better the curve points but more processing.
        X_line_bit_x = (height.x - source.x ) / timeToHit;
        X_line_bit_y = (height.y - source.y) / timeToHit;
        Negline_bit_x = (target.x - height.x) / timeToHit;
        Negline_bit_y = (height.y - target.y) / timeToHit;

        // Handy handlers of the x and y values of the source and height.
        Px = source.x;
        Py = source.y;
        Qx = height.x;
        Qy = height.y;
    }

    void makeLineArrays()
    {
        for (int i = 0; i < timeToHit + 1; i++)
        {
            P_lines.Add(new Vector3(Px, Py, 0f));
            Px += X_line_bit_x;
            Py += X_line_bit_y;

            Q_lines.Add(new Vector3(Qx, Qy, 0f));
            Qx += Negline_bit_x;
            Qy -= Negline_bit_y;
        }

        makeIntersectionPoints();
    }

    public void makeIntersectionPoints()
    {

        bc = 0;
        Vector2 myPoint = Vector2.zero;   // It's a bit easier to do 2D. So convert.
        for (int i = 0; i < timeToHit; i++)
        {
            if (bc < timeToHit)
            {
                bc++;
            }

            myPoint = findIntersectionPoints(
                (new Vector2(P_lines[i].x, P_lines[i].y)), 
                (new Vector2(Q_lines[i].x, Q_lines[i].y)),
                (new Vector2(P_lines[bc].x, P_lines[bc].y)), 
                (new Vector2 (Q_lines[bc].x, Q_lines[bc].y)));
            Vector3 myPoint_3 = new Vector3(myPoint.x, myPoint.y, 0f);
            IntersectionPoints.Add(myPoint_3);
        }
        IntersectionPoints.Add(target);
    }

    public Vector2 findIntersectionPoints(Vector2 Line1Point1, Vector2 Line1Point2, Vector2 Line2Point1, Vector2 Line2Point2)
    {
        float P1 = (Line2Point2.x - Line2Point1.x) * (Line1Point2.y - Line1Point1.y)
            - (Line2Point2.y - Line2Point1.y) * (Line1Point2.x - Line1Point1.x);

        float P2 = ((Line1Point1.x - Line2Point1.x) * (Line1Point2.y - Line1Point1.y)
            - (Line1Point1.y - Line2Point1.y) * (Line1Point2.x - Line1Point1.x)) / P1;

        return new Vector2(
            Line2Point1.x + (Line2Point2.x - Line2Point1.x) * P2,
            Line2Point1.y + (Line2Point2.y - Line2Point1.y) * P2
        );
        /// Code modified from: https://blog.dakwamine.fr/?p=1943  
        /// (Thanks for the leg up!)
    }

    public void drawLines()
    {
        lineRend.positionCount = 0;
        Vector3[] positions = new Vector3[Mathf.RoundToInt(timeToHit) + 1];
        for (int i = 0; i < timeToHit + 1; i++)
        {
            positions[i] = IntersectionPoints[i];  // Draws the path
        }

        lineRend.positionCount = positions.Length;
        lineRend.SetPositions(positions);
        drawMe = false;
    }

    public void MoveMe()
    {
        if (transform.position != IntersectionPoints[targetreached])
        {
            float step = speed * Time.deltaTime;
            transform.position = Vector3.MoveTowards(transform.position, IntersectionPoints[targetreached], step);
        }
        else
        {
            if (targetreached != IntersectionPoints.Count)
            {
                targetreached++;
            }
        }

        if (transform.position == Target.transform.position)
        {
            moveMe = false;
        }
    }

    public void ResetMe()
    {
        transform.position = source;
        targetreached = 0;
        X_line_length = 0;
        Y_line_length = 0;
        Y_Negline_length = 0;
        X_line_bit_x = 0;
        X_line_bit_y = 0;
        Negline_bit_x = 0;
        Negline_bit_y = 0;
        X_line_bit_xs.Initialize();
        X_line_bit_ys.Initialize();
        Negline_bit_ys.Initialize();
        P_lines.Clear();
        Q_lines.Clear();
        IntersectionPoints.Clear();
        Px = 0;
        Py = 0;
        Qx = 0;
        Qy = 0;
        moveMe = false;
        resetMe = false;
    }

    void OnGUI()
    {
        GUI.Label(new Rect(10, 10, 140, 20), "Source: " + source);
        GUI.Label(new Rect(10, 30, 140, 20), "Target: " + target);
        GUI.Label(new Rect(10, 50, 140, 20), "Height: " + height);
    }


    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(source, 0.2f);
        Gizmos.DrawWireSphere(target, 0.2f);
        Gizmos.DrawWireSphere(height, 0.2f);
        Gizmos.color = Color.green;
        Gizmos.DrawLine(source, target);
        Gizmos.DrawLine(source, height);
        Gizmos.DrawLine(height, target);
        UnityEditor.Handles.Label(source, "SOURCE");
        UnityEditor.Handles.Label(target, "TARGET");
        UnityEditor.Handles.Label(height, "HEIGHT");
        Gizmos.color = Color.yellow;
        // Uncomment to see lines in editor
        for (int i = 0; i < timeToHit + 1; i++)
        {
            Gizmos.DrawLine(P_lines[i], Q_lines[i]);
        }
    }
}

Xander out.


2 responses to “Unity 2D Curves using Triangles”

Leave a Reply

Your email address will not be published. Required fields are marked *