In the previous post we had a look at what Bézier curves are and how they can form a spline. In this post we will have a look at how the code storing such a spline is going to work. For this part I will be using C# and making use of Unity’s Vector2 classes.
As I mentioned before, I will only need open paths. Moreover, I will only need 2D (horizontal plane) paths as the height will be given by the terrain rather than the path. This is a fundamentaly different approach to achieving the end goal. You can either generate flat paths and rely on the terrain for the height (what I will be doing) or you could create a 3D path and make the terrain fit the path.
I prefer the first approach because it makes things easier to deal with at each step as far as I can tell. Let’s get to it, below are the first two classes we’ll be using.
C# – CubicBézierPath
This is the general implementation. It is able to store a continous path made up of points, anchors and controls, that when evaluated would be a collection of Bézier curves connected at the ends.
The storage format I went for is as described in my previous post:
Anchor – StartControl – EndControl – Anchor – StartControl – EndControl – Anchor
This means that our Anchor’s indexes are divisble by 3, StartControl’s indexes are divisble by three after having 1 subtracted from them and EndControl’s indexes are divisble by three after having 1 added to them.
Moving forward you will see references to path segments. A segment is a collection of 4 points, starting and ending with an anchor. As such, the number of segments within a path is equal to the number of points divided by 4 even though some of those points are reused within adjacent segments.
Since when creating the path we’ll most likely create it in segments, we have a method that adds said segment. It doesn’t require a start anchor for it because the start anchor of each segment is the end anchor of the previous one .
Finally, we have a protected getter and setter for a point. This is because we’ll want to extend that functionality in our more specialized implementations.
using System.Collections.Generic;
using UnityEngine;
// Keep everything organized under namespaces so that the code is reusable/importable
namespace HercTech.Bezier
{
public class CubicBezierPath
{
// Stores all the points of the path, anchors and controls
protected List<Vector2> Points;
// This is an array-like accessor for the points of our path
public Vector2 this[int i]
{
get
{
return GetPoint(i);
}
set
{
UpdatePoint(i, value);
}
}
// This returns the number of points in the path
public int Count
{
get
{
return Points.Count;
}
}
// Constructs the CubicBezierPath object from a starting segment
public CubicBezierPath(Vector2 startAnchor, Vector2 startControl, Vector2 endControl, Vector2 endAnchor)
{
Points = new List<Vector2>();
Points.Add(startAnchor);
Points.Add(startControl);
Points.Add(endControl);
Points.Add(endAnchor);
}
// This will remove all the path's points
public void Clear()
{
Points = new List<Vector2>();
}
// Returns the number of segments in the path
public int GetSegmentCount()
{
return Points.Count / 4;
}
// Gets all the points of a path segment
public Vector2[] GetSegmentPoints(int segIndex)
{
return Points.GetRange(segIndex * 3, 4).ToArray();
}
// Adds a segment to our path
public void AddSegment(Vector2 startControl, Vector2 endControl, Vector2 endAnchor)
{
Points.Add(startControl);
Points.Add(endControl);
Points.Add(endAnchor);
}
// Defining a implicit type conversion from our object to a Vector2 array
public static implicit operator Vector2[](CubicBezierPath path)
{
return path.Points.ToArray();
}
protected Vector2 GetPoint(int index)
{
// If the point we're trying to set does not exist, throw an exception
if (index < 0 || index > Points.Count - 1)
throw new System.ArgumentException("Index out of range!");
return Points[index];
}
protected void UpdatePoint(int index, Vector2 value)
{
// If the point we're trying to set does not exist, throw an exception
if (index < 0 || index > Points.Count - 1)
throw new System.ArgumentException("Index out of range!");
Points[index] = value;
}
}
}
C# – CubicBézierSpline
Building on the previous, general implementation this is a specific spline oriented class. If you look through the code you’ll see there’s quite a few things happening differently in here.
First, the AddSegment method takes only the end control and anchor as opposed to the start one too. This assures that the curve around the anchors is continuous.
Second, when we’re updating a point, if it is a control point, we also update its counter part so that the curve remains continuous around the anchor. If the point we’re updating is an anchor, we move it along with the control points around it, essentially moving the entire section of the spline.
using UnityEngine;
// Keep everything organized under namespaces so that the code is reusable/importable
namespace HercTech.Bezier
{
class CubicBezierSpline : CubicBezierPath
{
// Inheirt base constructor
public CubicBezierSpline(Vector2 startAnchor, Vector2 startControl, Vector2 endControl, Vector2 endAnchor)
: base(startAnchor, startControl, endControl, endAnchor) {}
// This simplifies imput as our first control is calculated from our previous one
public void AddSegment(Vector2 endControl, Vector2 endAnchor)
{
Vector2 startControl = FlipPointAroundReference(base.Points[base.Count - 2], base.Points[base.Count - 1]);
// Use the general method to add the points
base.AddSegment(startControl, endControl, endAnchor);
}
// Override the point updater from base so that control points are connected
protected new void UpdatePoint(int index, Vector2 value)
{
// If the point we're trying to set does not exist, throw an exception
if (index < 0 || index > Points.Count - 1)
throw new System.ArgumentException("Index out of range!");
// If this is a startControl point we need to update the previous endControl (except for the first one)
if ((index - 1) % 3 == 0 && index > 2)
{
Points[index] = value;
Points[index - 2] = FlipPointAroundReference(value, Points[index - 1]);
}
// If this is a endControl point we need to update the next startControl (except for the last one)
else if ((index + 1) % 3 == 0 && index < base.Count - 2)
{
Points[index] = value;
Points[index + 2] = FlipPointAroundReference(value, Points[index + 1]);
}
// Otherwise update the current point (Anchors and edge controls) plus its adjacent control points
else
{
Vector2 diff = value - Points[index];
Points[index] = value;
if (index > 0)
{
Points[index - 1] += diff;
}
if (index < Count - 1)
{
Points[index + 1] += diff;
}
}
}
// Helper method to get the opposite position from point A relative to a reference
private Vector2 FlipPointAroundReference(Vector2 point, Vector2 reference)
{
// Calculate startControl by getting the endControl and endAnchor of the previous segment then getting
// the opposite of endControl around endAnchor
Vector2 diff = reference - point; // Get the distance vector
// Reverse the magnitude of the distance and add it to previous end anchor to get opposite point
return reference + diff.normalized * -1 * diff.magnitude;
}
}
}