Unity Splines Plugin

Keywords

Unity
Splines
Game Tools
Game Math
Vector Math
Unity 2017

Background Context

I began working on a custom spline tool for my friend’s MOBA project. One thing that was needed in the project was to add minions the player can last hit to earn gold and exp. At first, it may look something very straightforward to do; however, as I broke down the feature into smaller chunks to work on, it turned out to be a lot bigger than expected.

You might be wondering, “Why create Splines in Unity when there exists an official Splines package available and 3rd party plugins available to download?”. To answer that question, the project is locked to Unity version 2017 which doesn’t have support from Unity’s official Splines package as it is only available in Unity version 2022 or higher. Also, it gives me the opportunity to learn more about game math and to showcase my thinking process of creating something I haven’t done before.

To give an idea of what it would take to have a functional minions like in League of Legends, here are some of the things minions do:

  1. Minions spawn at a set interval. It could be that 1 wave of minions consisting of 3 melee and 3 caster minions spawn every 30 seconds for example (As of 2026, Riot Games changed the timing and logic slightly as it this now refers to how they did it before).
  2. Minions have logic that can probably be modeled after a finite state machine as to when they start targeting an enemy player, enemy minions, or enemy structures. This may need some sort of prioritization logic setup so in the event a minion needs to decide who to start attacking, it would know which enemy type to target first.
  3. Minions lose aggro (or aggression) towards an enemy player when the enemy player moves too far away from the minion’s designated lane.
  4. Minions follow a path they are assigned to on spawn.

This isn’t an exhaustive list of what a minion can and cannot do but as you can imagine, the list can become super long depending on the rules the game designers might set.

For this article, I’ll be focusing on how I built a custom spline tool in Unity version 2017 and how it is used for the “minions” or beans to move from one point to the next.

Given the attempts sectioned below, I will be using Unity’s pathfinding related APIs via AI NavMesh agents as the approaches I implemented use it in code.

Attempt #1 - list of way points

My 1st attempt to create AI pathfinding similar to League of Legends minions was to create a list of way points that the bean will follow. It looked something like this:

Code Snippet


// BeanFollower.cs
private void PrecomputeNavMeshPaths()
{
  Vector3 originalPosition = transform.position;

  foreach (Transform checkpoint in _checkpoints)
  {
    var path = new NavMeshPath();
    // CalculatePath is a built in function in Unity that will compute a path from a starting position to an end position. Here the end position is checkpoint.position
    bool isPathFound = _agent.CalculatePath(checkpoint.position, path);
    if (!isPathFound)
    {
      Debug.LogWarning(string.Format("checkpoint position {0} could not resolve a path", checkpoint));
      transform.position = originalPosition;
      return;
    }

    transform.position = checkpoint.position;
    _paths.Add(path);
    _corners.Add(path.corners);
  }
  
  // here the bean's position is reset back to where it was located originally
  transform.position = originalPosition;
}

When does PrecomputeNavMeshPaths() get called?

In Awake() where it gets called when the game first loads up with this game object instance owning the BeanFollower component script.


// BeanFollower.cs
void Awake()
{
   // other code removed for brevity
   PrecomputeNavMeshPaths();
}

Issues with this approach?

In the video, notice how the bean stops momentarily for a few frames, then continues marching forward. This is because it needed to recalculate its path as the path it pre-computed in the beginning became no longer valid. This could be due to floating point imprecision as Vector3 uses floating point for x,y,z axes.

Can I do better?

Attempt #2 - Cubic Hermite Spline

The Math behind the Splines

The idea behind this approach is to use a Cubic Hermite Spline to have a bean follow the curve a designer creates. A Spline is a collection of curves where each curve is a parametric equation:

Cubic Hermite Curve equations thanks to Cubic Hermite Spline article on Wikipedia


1. s(t) = (1 + 2*t)(1 - t)^2
2. e(t) = t^2(t - 1)
3. d'(t) = t(1 - t)^2
4. f'(t) = t^2(t - 1)
  • One Input: t is a floating point value ranging between 0 and 1 that represents the progress a bean travels along the curve. 0 means a bean is at the beginning of the curve, 0.25 means the bean is 25% along the path of the curve and 1 means the bean reached the end of the curve.
  • s(t) represents the starting Vector3 position at input t
  • e(t) represents the ending Vector3 position at input t
  • d’(t) represents the first derivative or starting velocity at input t
  • f’(t) represents the first derivative or ending velocity at input t

This might all look overwhelming at first but the idea behind using these formulas is to mathematically approximate the curvature of the lines. In order to achieve that, Hermite Curve will need additional inputs:

  • Vector3 p0 - starting position
  • Vector3 p1 - ending position
  • Vector3 m0 - first derivative or starting velocity
  • Vector3 m1 - first derivative or ending velocity

Obtaining the starting and ending positions is easy where you pick 2 positions in the current Scene in Unity by creating empty gameobjects but how do you pick the first derivatives for the starting and ending velocities? I use the forward vectors of the starting and ending point’s transforms as those are unit vectors and set the starting and ending velocities for any bean traveling at the point to be 1 unit per second. The benefit of using each transform’s forward vectors is that I can multiply it by a speed value so that I can change the starting or ending or even both velocities at those points as the bean traverses through them. There’s probably other tangents I could have used but am open to alternatives. Putting all of the above knowledge together, I get:


/// https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Representations
/// </summary>
/// <param name="p0">Vector3 representing start position of curve</param>
/// <param name="p1">Vector3 representing end position of curve</param>
/// <param name="m0">Vector3 representing the tangent of the start position of curve. Can set this as the start transform's forward vector.</param>
/// <param name="m1">Vector3 representing the tangent of the start position of curve. Can set this as the end transform's forward vector.</param>
/// <param name="t">floating point value between 0 and 1 that is used to obtain a Vector3 position between p0 and p1</param>
/// <returns>Vector3 position between p0 and p1</returns>
public static Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 m0, Vector3 m1, float t)
{
    t = Mathf.Clamp01(t);

    float oneMinusT = (1 - t);
    float startPointInterpolationFormula = (1 + 2 * t) * oneMinusT * oneMinusT;
    float endPointInterpolationFormula = t * t * (3 - 2 * t);
    float startPointFirstDerivativeFormula = t * oneMinusT * oneMinusT;
    float endPointFirstDerivativeFormula = t * t * (t - 1);

    Vector3 interpolatedResult =
        startPointInterpolationFormula * p0 +
        startPointFirstDerivativeFormula * m0 +
        endPointInterpolationFormula * p1 +
        endPointFirstDerivativeFormula * m1;


    return interpolatedResult;
}

As you can see, the formulas above are treated like weights where if you multiply each one to their respective Vector3 variable, it forms a new Vector3 called interpolatedResult that represents a position between the starting and ending point

Example Usage of GetPoint() function:


// minimal example
Transform startPoint;
Transform endPoint;
Vector3 startPointVelocity;
Vector3 endPointVelocity;
float speed;
float t;


startPointVelocity = -startPoint.forward * speed;
endPointVelocity = -endPoint.forward * speed;
Vector3 point = HermiteSplineUtils.GetPoint(startPoint.position, endPoint.position, startPointVelocity, endPointVelocity, t);

Applying the Math to move the beans

To get the beans to move along the spline. The minimal code snippet demonstrates its use case:


[SerializeField]
HermiteSpline _splinePath; // path to assign to bean in the Unity Editor
NavMeshAgent _agent;
int pointIndex; // ranges between 0 and length of Points
int prevPointIndex;

void Awake()
{
  _agent = GetComponent<NavMeshAgent>();
}

void Update()
{
  if (pointIndex >= _splinePath.Points.Count)
  {
    return;
  }

  if (!_agent.pathPending)
  {
    // Points is a list of Vector3 that gets computed to approximate
    // curves. Think of it as the in betweens 2 endpoints of a Hermite Curve.
    _agent.SetDestination(_splinePath.Points[pointIndex]);
  }

  // if close enough to the point on a spline, move towards the next point
  // on spline
  if (_agent.remainingDistance < _agent.stoppingDistance)
  {
    prevPointIndex = pointIndex;
    pointIndex += 1;
  }
}

Beans Moving in Action

Closing Thoughts

There is a lot more code / concepts such as how I created the custom tooling for creating the splines such as how Gauss-Legendre Coefficients is used to approximate length of a Hermite Curve. If you would like to learn more about it in more detail, feel free to DM me on LinkedIn. If I get enough interest, I can write a follow up technical article on it.

Resources