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:
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.
The more lines you use the smoother your line is…
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.
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”
No need for finding intersection of lines: https://en.m.wikipedia.org/wiki/De_Casteljau's_algorithm
Thanks Pex but there was no way I was coding that algorithm without help 🙂
Here are some existing solutions:
https://forum.unity.com/threads/bezier-curve.5082/
https://answers.unity.com/questions/392606/line-drawing-how-can-i-interpolate-between-points.html
Some people on Reddit also noted the similarity of my project to making Bezier Curves.
If anyone want’s more info on bezier curves this was shared:
https://pomax.github.io/bezierinfo/
https://www.youtube.com/watch?v=RF04Fi9OCPc