Nested Lists!

Hi,

I’m trying to do a nested List of Vector3.

I start with …

var tempFlightPointsPositions : List.< Vector3 > = new List.< Vector3 >();

var mappedFlightPaths : List.<List.<List.<List.<List.<Vector3> > > > > = new List.<List.<List.<List.<List.<Vector3> > > > >();

for my two Lists, first one tempFlightPointsPositions is filled with 100 Vector3’s, then I want to pass that List into the mappedFlightPaths List, so I thought …

mappedFlightPaths.Add(tempFlightPointsPositions);

would do it, but does not add it, what is it that I’m doing wrong? Thanks.

When using nested lists you have to tell it which list in the list of lists you want to add the item to

List<List<Vector3>> listList = new List<List<Vector3>>();
listList.Add(new List<Vector3>());
listList[0].Add(Vector3.zero);

With that said - I’d probably rethink that data model :slight_smile:

2 Likes

Hi, thanks for the input, that worked great, why do you say “I’d probably rethink that data model”?

As I’m making 5 flightpaths in Start() (the yellow, white, blue and purple lines) consisting of a random amount of waypoints (the small yellow cubes) then a script calculates a bezier curve from waypoint 0 to 1, … 1 to 2 … ect …

Then I divide each section into 100 flight points for the flight craft to fly, 4 waypoints, 100 points in between each pair of waypoint so a small flightpath will have a round 400 flight points, I then save those points into the List above as flight path 0, 1, 2 … ect

Then the script will choose a random flightpath from the list for the craft to fly using each point in that List.

As I’m always looking to learn and progress in coding I’d really appreciate your input again if you think there is a simpler, more optimised, or just a better way of doing this, thanks.

2893457--212796--Screen Shot_00.jpg

A nested list five levels deep is a sure sign of a structural problem.

If you must nest this deep, I would suggest encapsulating each list.

Sounds like all you need is a List

2 Likes

I once used a nested list, it’s pretty useful if you want to keep a bunch of data that should be separated, but they are not equally long
My example is that I had a script generating a mesh, and I wanted to split it into chunks, and I just had a list of lists of points and a list of lists of ints (that being said I swithced to an array containing the lists pretty fast)

But for your example I personally would’ve made a class called Chunk which would have a List to represent the points. That gives a lot more flexibility, for example to extend chunks with extra properties, or even do other stuff on a chunk per chunk basis.

2 Likes

Yeah, I have a chunk class, but that wouldn’t really change the whole thing, it would be an array of classes with lists in them, and it wouldn’t allow me to generate the chunk meshes only if the player is close to that chunk

I of course don’t know what game you made or are making, so I can’t argue about why it would not be possible in your case, but in general using a Chunk class wouldn’t prevent anyone from generating and/or rendering it based on player distance, as you can just use the world position of that chunk for that (I have done that in a Minecraft clone I made, and Minecraft itself also uses that same basic logic).

It also isn’t necesary about changing the whole thing (though List with in that a List (or Chunks.Points) is a lot more readable then List<List> (or Chunks*[j]) in my opinion) but more about opening the implementation for extension without introducing unnecessary breaking changes.*
Edit: edited text 10000000000000000000 times, still gets reformatted in italic and random removal of text… ffs.

1 Like

::attempts to digest this information::

OK, 5 paths doesn’t necessarily mean a need for 5 dimensions in your jagged multi-dimensional collection.

That’s what you’re making when you have a nested lists like this… it’s a multi-dimensional data structure. And that one there is quite a doisy. Think of it like this, a data structure of length 2 in all 5 dimensions would be 32 nodes. But just in creasing it by 1 in all dimensions brings us to 243, and raising just one more brings us to 1024. It grows in size very quickly.

Nevermind the fact that it doesn’t sound like you need all those dimensions.

It sounds like you need:

This sounds like you need 5 lists for this. 5 flightpaths, 5 lists.

This sounds like yet another 5 lists, 1 for each flightpath.

So 10 lists in total.

Though… why are you calculating all these inbetween waypoints up front like this? That’s a lot of memory consumed for what?

The nice thing about bezier curves is that there is a formula for them based on the given control points (the initial 5 lists). You can predictably get a point anywhere on the curve with a relatively simple algorithm. So… why calculate 100’s of points and store them for later, rather than calculate any given point when you need it?

So yeah, have the 5 lists of flightpaths. Pick one at random. Then calculate waypoints as you progress over the spline.

This is how most geometry libraries would do it… it’s how I do it in my geometry library:

I have a lot more going on in here to meet the demands of the interfaces it implements so that it feets into my tween/animation engines.

using UnityEngine;
using System.Collections.Generic;

namespace com.spacepuppy.Waypoints
{

    public class BezierSplinePath : IConfigurableIndexedWaypointPath
    {

        #region Fields

        private bool _isClosed;
        private List<IWaypoint> _waypoints = new List<IWaypoint>();

        private Vector3[] _points;
        private CurveConstantSpeedTable _speedTable = new CurveConstantSpeedTable();

        #endregion
    
        #region CONSTRUCTOR

        public BezierSplinePath()
        {

        }

        public BezierSplinePath(IEnumerable<IWaypoint> waypoints)
        {
            _waypoints.AddRange(waypoints);
            this.Clean_Imp();
        }

        #endregion

        #region Properties

        #endregion

        #region Methods

        private void Clean_Imp()
        {
            if (_waypoints.Count == 0)
            {
                _speedTable.SetZero();
            }
            else if (_waypoints.Count == 1)
            {
                _speedTable.SetZero();
            }
            else
            {
                var arr = this.GetPointArray();
                float estimatedLength = 0f;
                for(int i = 1; i < arr.Length; i++)
                {
                    estimatedLength += (arr[i] - arr[i - 1]).magnitude;
                }
                int detail = Mathf.RoundToInt(estimatedLength / 0.1f);
            
                _speedTable.Clean(detail, this.GetRealPositionAt);
            }
        }

        private Vector3[] GetPointArray()
        {
            int l = _waypoints.Count;
            int cnt = l;
            if (_isClosed) cnt++;
            if (_points == null)
                _points = new Vector3[cnt];
            else if(_points.Length != cnt)
                System.Array.Resize(ref _points, cnt);

            for (int i = 0; i < l; i++ )
            {
                _points[i] = _waypoints[i].Position;
            }
            if(_isClosed)
            {
                _points[cnt - 1] = _waypoints[0].Position;
            }

            return _points;
        }

        private Vector3 GetRealPositionAt(float t)
        {
            var arr = this.GetPointArray();
            var c = arr.Length;
            while (c > 1)
            {
                for (int i = 1; i < c; i++)
                {
                    arr[i - 1] = Vector3.Lerp(arr[i - 1], arr[i], t);
                }

                c--;
            }
            return arr[0];
        }

        #endregion

        #region IWaypointPath Interface

        public bool IsClosed
        {
            get { return _isClosed; }
            set
            {
                if (_isClosed == value) return;
                _isClosed = value;
                _speedTable.SetDirty();
            }
        }

        public float GetArcLength()
        {
            if (_speedTable.IsDirty) this.Clean_Imp();
            return _speedTable.TotalArcLength;
        }

        public Vector3 GetPositionAt(float t)
        {
            if (_waypoints.Count == 0) return Vector3.zero;
            if (_waypoints.Count == 1) return _waypoints[0].Position;

            if (_speedTable.IsDirty) this.Clean_Imp();
            return this.GetRealPositionAt(_speedTable.GetConstPathPercFromTimePerc(t));
        }

        public Waypoint GetWaypointAt(float t)
        {
            if (_waypoints.Count == 0) return Waypoint.Invalid;
            if (_waypoints.Count == 1) return new Waypoint(_waypoints[0]);

            t = _speedTable.GetConstPathPercFromTimePerc(t);
            var p1 = this.GetRealPositionAt(t);
            var p2 = this.GetRealPositionAt(t + 0.01f);
            return new Waypoint(p1, (p2 - p1));
        }

        public Vector3[] GetDetailedPositions(float segmentLength)
        {
            int detail = Mathf.FloorToInt(this.GetArcLength() / segmentLength) + 1;
            Vector3[] arr = new Vector3[detail + 1];
            for (int i = 0; i <= detail; i++)
            {
                arr[i] = this.GetPositionAt((float)i / (float)detail);
            }
            return arr;
        }

        #endregion

        #region IIndexedWaypointPath Interface

        public int Count
        {
            get { return _waypoints.Count; }
        }

        public IWaypoint ControlPoint(int index)
        {
            return _waypoints[index];
        }

        public int IndexOf(IWaypoint waypoint)
        {
            return _waypoints.IndexOf(waypoint);
        }

        public Vector3 GetPositionAfter(int index, float t)
        {
            if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();

            float range = 1f / (_waypoints.Count - 1);
            return this.GetPositionAt(index * range + t * range);
        }

        public Waypoint GetWaypointAfter(int index, float t)
        {
            if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();

            float range = 1f / (_waypoints.Count - 1);
            return this.GetWaypointAt(index * range + t * range);
        }

        public RelativePositionData GetRelativePositionData(float t)
        {
            int cnt = _waypoints.Count;
            switch(cnt)
            {
                case 0:
                    return new RelativePositionData(-1, 0f);
                case 1:
                    return new RelativePositionData(0, 0f);
                case 2:
                    return new RelativePositionData(0, t);
                default:
                    {
                        float range = 1f / (_waypoints.Count - 1);
                        int i = Mathf.Clamp(Mathf.FloorToInt(t / range), 0, _waypoints.Count - 1);
                        float dt = (t - i * range) / range;
                        return new RelativePositionData(i, dt);
                    }
            }
        }

        public void Clean()
        {
            _speedTable.SetDirty();
        }

        #endregion

        #region IConfigurableIndexedWaypointPath Interface

        public void AddControlPoint(IWaypoint waypoint)
        {
            _waypoints.Add(waypoint);
            _speedTable.SetDirty();
        }

        public void InsertControlPoint(int index, IWaypoint waypoint)
        {
            _waypoints.Insert(index, waypoint);
            _speedTable.SetDirty();
        }

        public void ReplaceControlPoint(int index, IWaypoint waypoint)
        {
            _waypoints[index] = waypoint;
            _speedTable.SetDirty();
        }

        public void RemoveControlPointAt(int index)
        {
            _waypoints.RemoveAt(index);
            _speedTable.SetDirty();
        }

        public void Clear()
        {
            _waypoints.Clear();
            _speedTable.SetDirty();
        }

        #endregion

        #region IEnumerable Interface

        public IEnumerator<IWaypoint> GetEnumerator()
        {
            return _waypoints.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _waypoints.GetEnumerator();
        }

        #endregion

    }

    /// <summary>
    /// Normalizes t over spline so that its position appears to progress over the spline at a constant rate.
    /// </summary>
    internal class CurveConstantSpeedTable
    {

        #region Fields
    
        private float _totalArcLength = float.NaN;
        private float[] _timesTable;
        private float[] _lengthsTable;

        #endregion

        #region CONSTRUCTOR

        #endregion

        #region Properties

        public float TotalArcLength
        {
            get { return _totalArcLength; }
        }

        public bool IsDirty
        {
            get { return float.IsNaN(_totalArcLength); }
        }

        public int SubdivisionCount
        {
            get { return (_timesTable != null) ? _timesTable.Length : 0; }
        }

        #endregion

        #region Methods

        public void Clean(int subdivisions, System.Func<float, Vector3> getRealPositionAt)
        {
            _totalArcLength = 0f;
            float incr = 1f / subdivisions;
            _timesTable = new float[subdivisions];
            _lengthsTable = new float[subdivisions];

            var prevP = getRealPositionAt(0);
            float perc;
            Vector3 currP;
            for (int i = 1; i <= subdivisions; ++i)
            {
                perc = incr * i;
                currP = getRealPositionAt(perc);
                _totalArcLength += Vector3.Distance(currP, prevP);
                prevP = currP;
                _timesTable[i - 1] = perc;
                _lengthsTable[i - 1] = _totalArcLength;
            }
        }

        public void SetDirty()
        {
            _totalArcLength = float.NaN;
            _timesTable = null;
            _lengthsTable = null;
        }

        public void SetZero()
        {
            _totalArcLength = 0f;
            _timesTable = null;
            _lengthsTable = null;
        }

        public float GetConstPathPercFromTimePerc(float t)
        {
            if (float.IsNaN(_totalArcLength) || _totalArcLength == 0f) return t;

            //Apply constant speed
            if (t > 0f && t < 1f)
            {
                float tLen = _totalArcLength * t;
                float t0 = 0f, l0 = 0f, t1 = 0f, l1 = 0f;
                int alen = _lengthsTable.Length;
                for (int i = 0; i < alen; ++i)
                {
                    if (_lengthsTable[i] > tLen)
                    {
                        t1 = _timesTable[i];
                        l1 = _lengthsTable[i];
                        if (i > 0) l0 = _lengthsTable[i - 1];
                        break;
                    }
                    t0 = _timesTable[i];
                }
                t = t0 + ((tLen - l0) / (l1 - l0)) * (t1 - t0);
            }

            if (t > 1f) t = 1f;
            else if (t < 0f) t = 0f;
            return t;
        }

        public float GetTimeAtSubdivision(int index)
        {
            if (_timesTable == null) return 0f;
            return _timesTable[index];
        }

        #endregion

    }

}

There are plenty of free spline libraries out there as well if you don’t want to write one yourself.

For example here’s a simple bezier spline implementation I found by going to the unity story and searching for keyword ‘spline’ and selecting only the free options:
https://www.assetstore.unity3d.com/en/#!/content/11278

There were several others as well.
https://www.assetstore.unity3d.com/en/#!/search/page=1/sortby=relevance/query=spline&price:0

And if you prefer your way… still you could do it in 6 lists then. 5 for each flightpath, then once one is selected you generate the 1 list for the minutia data for only that chosen path.

2 Likes

Thank you all for the input, much appreciated, I have so much to learn.

@lordofduct I went the way I did because I use the bezier curve generation in runtime to compute flight paths for enemy with jetpacks so if you meet them again they will never fly the same path.

I now need to look more into nested Lists as I didn’t realise they would become that complicated.

I’ve looked at some of the spline assets on the asset store but most are editor only.

Another reason I stored the Vector3’s is so I could do a reverse of the list … flightPointsPositionsOutBound.Reverse();
so a flight craft landing in a bay could just turn around and fly out the way it came in.

Like I said I still have a lot to learn and will be looking deeper into this as it’s the main part of my project at the moment, once again thanks for all the input.

I understand you can get a point anywhere on the curve, so again I need to rethink how I’m going about this.

Thanks.

I suggest looking at the code I shared then. It’s not editor only… actually it’s NO editor, (though I have an editor extension for it, not shared).

It’s all pretty much there, you’ll just need to remove the IConfigurableIndexedWaypointPath declaration, as well as need the IWapoint interface:

    public interface IWaypoint
    {

        Vector3 Position { get; set; }
        Vector3 Heading { get; set; }

    }

Or just use the IWaypointPath interface and its children:

    public interface IWaypointPath
    {

        bool IsClosed { get; set; }

        float GetArcLength();
        Vector3 GetPositionAt(float t);
        Waypoint GetWaypointAt(float t);

        Vector3[] GetDetailedPositions(float segmentLength);
    }

    public interface IIndexedWaypointPath : IWaypointPath, IEnumerable<IWaypoint>
    {

        int Count { get; }
        IWaypoint ControlPoint(int index);
        int IndexOf(IWaypoint waypoint);
        Vector3 GetPositionAfter(int index, float t);
        Waypoint GetWaypointAfter(int index, float t);
        /// <summary>
        /// Returns data pertaining to the relative position between the 2 control points on either side of 't'.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        RelativePositionData GetRelativePositionData(float t);


        void Clean();

    }

    public struct RelativePositionData
    {
        public int Index;
        public float TPrime;

        public RelativePositionData(int index, float t)
        {
            this.Index = index;
            this.TPrime = t;
        }
    }

    public interface IConfigurableIndexedWaypointPath : IIndexedWaypointPath
    {
        void AddControlPoint(IWaypoint waypoint);
        void InsertControlPoint(int index, IWaypoint waypoint);
        void ReplaceControlPoint(int index, IWaypoint waypoint);
        void RemoveControlPointAt(int index);
        void Clear();

    }

With that you can have a bezier curve working.

I also have a catmull-rom, bezier chain (rather than spline), and a generic linear chain.

Actually thinking about it, you probably expect a bezier chain, not a bezier spline. The chain is a sequence of quadratic beziers, where as the spline is more like an N-dimensional bezier. Basically with a chain, the curve goes through the end waypoints of each sub curve, where as the spline they head toward the waypoints but never actually touch.

chain is a much simpler implementation, but the heading of the IWaypoint matters a lot more:

    /// <summary>
    /// Acts like a composite of multiple bezier curves. Where one curve ends, the next starts.
    /// The path will travel through control points.
    /// </summary>
    public class BezierChainPath : IConfigurableIndexedWaypointPath
    {

        #region Fields

        private bool _isClosed;
        private List<IWaypoint> _waypoints = new List<IWaypoint>();

        private Vector3[] _points; //null if dirty
        private float[] _lengths;
        private float _totalArcLength = float.NaN;

        #endregion
    
        #region CONSTRUCTOR

        public BezierChainPath()
        {

        }

        public BezierChainPath(IEnumerable<IWaypoint> waypoints)
        {
            _waypoints.AddRange(waypoints);
            this.Clean_Imp();
        }

        #endregion

        #region Properties

        #endregion

        #region Methods

        private void Clean_Imp()
        {
            if (_waypoints.Count == 0)
            {
                _points = new Vector3[] { };
                _totalArcLength = float.NaN;
            }
            else if (_waypoints.Count == 1)
            {
                _points = new Vector3[] { _waypoints[0].Position };
                _totalArcLength = 0f;
            }
            else
            {
                _totalArcLength = 0f;

                //get points
                int cnt = _waypoints.Count;
                if (_isClosed) cnt++;
                _points = new Vector3[(cnt - 1) * 3 + 1];
                _points[0] = _waypoints[0].Position;
                for (int i = 1; i < cnt; i++)
                {
                    var w1 = _waypoints[i % _waypoints.Count]; //if we're closed, this will result in the first entry
                    var w2 = _waypoints[i - 1];
                    var v = (w1.Position - w2.Position);
                    var s = v.magnitude / 2.0f; //distance of the control point

                    float s1 = (w1 is IWeightedWaypoint) ? (w1 as IWeightedWaypoint).Strength : 0f;
                    float s2 = (w2 is IWeightedWaypoint) ? (w2 as IWeightedWaypoint).Strength : 0f;
                    int j = (i - 1) * 3 + 1;
                    _points[j] = w2.Position + (w2.Heading * s1 * s);
                    _points[j + 1] = w1.Position - (w1.Heading * s2 * s);
                    _points[j + 2] = w1.Position;
                }

                //calculate lengths
                _lengths = new float[cnt];
                for (int i = 0; i < _lengths.Length - 1; i++)
                {
                    int j = i * 3;
                    var p0 = _points[j];
                    var p1 = _points[j + 1];
                    var p2 = _points[j + 2];
                    var p3 = _points[j + 3];

                    //approximation is sum of all 3 legs, and the coord from p0 to p3, all divided by 2.
                    float t = (p1 - p0).magnitude + (p2 - p1).magnitude + (p3 - p2).magnitude * (p3 - p0).magnitude;
                    _lengths[i] = t / 2.0f;
                }

                _totalArcLength = 0f;
                foreach (var l in _lengths) _totalArcLength += l;
            }
        }

        #endregion

        #region IIndexedWaypointPath Interface

        public int Count
        {
            get { return _waypoints.Count; }
        }

        public bool IsClosed
        {
            get { return _isClosed; }
            set
            {
                if (_isClosed == value) return;
                _isClosed = value;
                _points = null;
            }
        }

        public IWaypoint ControlPoint(int index)
        {
            return _waypoints[index];
        }

        public int IndexOf(IWaypoint waypoint)
        {
            return _waypoints.IndexOf(waypoint);
        }

        public float GetArcLength()
        {
            if (_points == null) this.Clean_Imp();
            return _totalArcLength;
        }

        public Vector3 GetPositionAt(float t)
        {
            if (_points == null) this.Clean_Imp();
            if (_waypoints.Count == 0) return VectorUtil.NaNVector3;
            if (_waypoints.Count == 1) return _waypoints[0].Position;
            if (_waypoints.Count == 2) return this.GetPositionAfter(0, t);


            float len = _lengths[0];
            float tot = len;
            int i = 0;
            while (tot / _totalArcLength < t && i < _lengths.Length)
            {
                i++;
                len = _lengths[i];
                tot += len;
            }

            float lt = (tot - len) / _totalArcLength;
            float ht = tot / _totalArcLength;
            float dt = com.spacepuppy.Utils.MathUtil.PercentageMinMax(t, ht, lt);
            return this.GetPositionAfter(i, dt);
        }

        public Waypoint GetWaypointAt(float t)
        {
            if (_points == null) this.Clean_Imp();
            if (_waypoints.Count == 0) return Waypoint.Invalid;
            if (_waypoints.Count == 1) return new Waypoint(_waypoints[0]);
            if (_waypoints.Count == 2) return this.GetWaypointAfter(0, t);


            float len = _lengths[0];
            float tot = len;
            int i = 0;
            while (tot / _totalArcLength < t && i < _lengths.Length)
            {
                i++;
                len = _lengths[i];
                tot += len;
            }

            float lt = (tot - len) / _totalArcLength;
            float ht = tot / _totalArcLength;
            float dt = com.spacepuppy.Utils.MathUtil.PercentageMinMax(t, ht, lt);
            return this.GetWaypointAfter(i, dt);
        }

        public Vector3 GetPositionAfter(int index, float t)
        {
            if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
            if (_points == null) this.Clean_Imp();
            if (_waypoints.Count == 0) return VectorUtil.NaNVector3;
            if (_waypoints.Count == 1) return _waypoints[0].Position;
            if (!_isClosed && index == _waypoints.Count - 1) return _waypoints[index].Position;

            t = Mathf.Clamp01(t);
            var ft = 1 - t;

            var i = index * 3;
            var p0 = _points[i];
            var p1 = _points[i + 1];
            var p2 = _points[i + 2];
            var p3 = _points[i + 3];

            /*
             C(t) = P0*(1-t)^3 + P1*3*t(1-t)^2 + 3*P2*t^2*(1-t) + P3*t^3

             dC(t)/dt = T(t) =
             -3*P0*(1 - t)^2 +
             P1*(3*(1 - t)^2 - 6*(1 - t)*t) +
             P2*(6*(1 - t)*t - 3*t^2) +
             3*P3*t^2
             */
            var p = (ft * ft * ft) * p0 +
                    3 * (ft * ft) * t * p1 +
                    3 * (1 - t) * (t * t) * p2 +
                    (t * t * t) * p3;
            var tan = -3 * p0 * (ft * ft) +
                      p1 * (3 * (ft * ft) - 6 * ft * t) +
                      p2 * (6 * ft * t - 3 * (t * t)) +
                      3 * p3 * (t * t);

            return p;
        }

        public Waypoint GetWaypointAfter(int index, float t)
        {
            if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
            if (_points == null) this.Clean_Imp();
            if (_waypoints.Count == 0) return Waypoint.Invalid;
            if (_waypoints.Count == 1) return new Waypoint(_waypoints[0]);
            if (!_isClosed && index == _waypoints.Count - 1) return new Waypoint(_waypoints[index]);

            t = Mathf.Clamp01(t);
            var ft = 1 - t;

            var i = index * 3;
            var p0 = _points[i];
            var p1 = _points[i + 1];
            var p2 = _points[i + 2];
            var p3 = _points[i + 3];

            /*
             C(t) = P0*(1-t)^3 + P1*3*t(1-t)^2 + 3*P2*t^2*(1-t) + P3*t^3

             dC(t)/dt = T(t) =
             -3*P0*(1 - t)^2 +
             P1*(3*(1 - t)^2 - 6*(1 - t)*t) +
             P2*(6*(1 - t)*t - 3*t^2) +
             3*P3*t^2
             */
            var p = (ft * ft * ft) * p0 +
                    3 * (ft * ft) * t * p1 +
                    3 * (1 - t) * (t * t) * p2 +
                    (t * t * t) * p3;
            var tan = -3 * p0 * (ft * ft) +
                      p1 * (3 * (ft * ft) - 6 * ft * t) +
                      p2 * (6 * ft * t - 3 * (t * t)) +
                      3 * p3 * (t * t);

            return new Waypoint(p, tan);
        }

        public Vector3[] GetDetailedPositions(float segmentLength)
        {
            int detail = Mathf.FloorToInt(this.GetArcLength() / segmentLength) + 1;
            Vector3[] arr = new Vector3[detail + 1];
            for (int i = 0; i <= detail; i++)
            {
                arr[i] = this.GetPositionAt((float)i / (float)detail);
            }
            return arr;
        }

        public RelativePositionData GetRelativePositionData(float t)
        {
            var cnt = _waypoints.Count;
            switch (cnt)
            {
                case 0:
                    return new RelativePositionData(-1, 0f);
                case 1:
                    return new RelativePositionData(0, 0f);
                case 2:
                    return new RelativePositionData(0, t);
                default:
                    {
                        if (_points == null) this.Clean_Imp();

                        float len = _lengths[0];
                        float tot = len;
                        int i = 0;
                        while (tot / _totalArcLength < t && i < _lengths.Length)
                        {
                            i++;
                            len = _lengths[i];
                            tot += len;
                        }

                        float lt = (tot - len) / _totalArcLength;
                        float ht = tot / _totalArcLength;
                        float dt = com.spacepuppy.Utils.MathUtil.PercentageMinMax(t, ht, lt);
                    
                        return new RelativePositionData(i, dt);
                    }
            }
        }

        public void Clean()
        {
            _points = null;
        }

        #endregion

        #region IConfigurableIndexedWaypointPath Interface

        public void AddControlPoint(IWaypoint waypoint)
        {
            _waypoints.Add(waypoint);
            _points = null;
        }

        public void InsertControlPoint(int index, IWaypoint waypoint)
        {
            _waypoints.Insert(index, waypoint);
            _points = null;
        }

        public void ReplaceControlPoint(int index, IWaypoint waypoint)
        {
            _waypoints[index] = waypoint;
            _points = null;
        }

        public void RemoveControlPointAt(int index)
        {
            _waypoints.RemoveAt(index);
            _points = null;
        }

        public void Clear()
        {
            _waypoints.Clear();
            _points = null;
        }

        #endregion

        #region IEnumerable Interface

        public IEnumerator<IWaypoint> GetEnumerator()
        {
            return _waypoints.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _waypoints.GetEnumerator();
        }

        #endregion

    }

It may seem like more code, but it’s technically less work over all as the formula is more straightforward, and doesn’t require the speedTable from the spline.

And for shits and giggles, here’s catmull-rom. Personally I think catmull-rom’s look the best.

    /// <summary>
    /// Represents a catmull-rom cardinal spline.
    /// </summary>
    public class CardinalSplinePath : IConfigurableIndexedWaypointPath
    {

        #region Fields

        private const int SUBDIVISIONS_MULTIPLIER = 16;

        private bool _isClosed;
        private List<IWaypoint> _waypoints = new List<IWaypoint>();
        private bool _useConstantSpeed = true;

        private Vector3[] _points;
        private CurveConstantSpeedTable _speedTable = new CurveConstantSpeedTable();

        #endregion

        #region CONSTRUCTOR

        public CardinalSplinePath()
        {

        }

        public CardinalSplinePath(IEnumerable<IWaypoint> waypoints)
        {
            _waypoints.AddRange(waypoints);
            this.Clean_Imp();
        }

        #endregion

        #region Properties

        public bool UseConstantSpeed
        {
            get { return _useConstantSpeed; }
            set { _useConstantSpeed = value; }
        }

        #endregion

        #region Methods

        private void Clean_Imp()
        {
            if (_waypoints.Count == 0)
            {
                _points = new Vector3[] { };
                _speedTable.SetZero();
                return;
            }
            else if (_waypoints.Count == 1)
            {
                _points = new Vector3[] { _waypoints[0].Position };
                _speedTable.SetZero();
                return;
            }
            else
            {
                //get points
                _points = (_isClosed) ? new Vector3[_waypoints.Count + 3] : new Vector3[_waypoints.Count + 2];
                for (int i = 0; i < _waypoints.Count; i++) _points[i + 1] = _waypoints[i].Position;
                if (_isClosed)
                {
                    _points[0] = _waypoints[_waypoints.Count - 1].Position;
                    _points[_points.Length - 2] = _points[1];
                    _points[_points.Length - 1] = _points[2];
                }
                else
                {
                    _points[0] = _points[1];
                    var lastPnt = _waypoints[_waypoints.Count - 1].Position;
                    var diffV = lastPnt - _waypoints[_waypoints.Count - 2].Position;
                    _points[_points.Length - 1] = lastPnt + diffV;
                }

                _speedTable.Clean(SUBDIVISIONS_MULTIPLIER * _points.Length, this.GetRealPositionAt);
            }
        }

        /// <summary>
        /// Returns the position with out speed correction on the path of points.
        /// This method does NOT validate itself, make sure the curve is clean before calling.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private Vector3 GetRealPositionAt(float t)
        {
            int numSections = _points.Length - 3;
            int tSec = Mathf.FloorToInt(t * numSections);
            int currPt = numSections - 1;
            if (currPt > tSec) currPt = tSec;
            float u = t * numSections - currPt;

            Vector3 a = _points[currPt];
            Vector3 b = _points[currPt + 1];
            Vector3 c = _points[currPt + 2];
            Vector3 d = _points[currPt + 3];
            return 0.5f * (
                    (-a + 3f * b - 3f * c + d) * (u * u * u)
                    + (2f * a - 5f * b + 4f * c - d) * (u * u)
                    + (-a + c) * u
                    + 2f * b
                    );
        }

        #endregion

        #region IWaypointPath

        public bool IsClosed
        {
            get { return _isClosed; }
            set
            {
                if (value == _isClosed) return;
                _isClosed = value;
                _points = null;
            }
        }


        public Vector3 GetPositionAt(float t)
        {
            if (_speedTable.IsDirty) this.Clean_Imp();
            if (_points.Length < 2) return (_points.Length == 0) ? VectorUtil.NaNVector3 : _points[0];

            if (_useConstantSpeed) t = _speedTable.GetConstPathPercFromTimePerc(t);

            return GetRealPositionAt(t);
        }

        public Waypoint GetWaypointAt(float t)
        {
            if (_speedTable.IsDirty) this.Clean_Imp();
            if (_points.Length < 2) return (_points.Length == 0) ? Waypoint.Invalid : new Waypoint(_points[0], Vector3.zero);

            if (_useConstantSpeed) t = _speedTable.GetConstPathPercFromTimePerc(t);

            var p1 = this.GetRealPositionAt(t);
            var p2 = this.GetRealPositionAt(t + 0.01f); //TODO - figure out a more efficient way of calculating the tangent
            return new Waypoint(p1, (p2 - p1).normalized);
        }

        public float GetArcLength()
        {
            if (_speedTable.IsDirty) this.Clean_Imp();
            return _speedTable.TotalArcLength;
        }

        public Vector3[] GetDetailedPositions(float segmentLength)
        {
            int detail = Mathf.FloorToInt(this.GetArcLength() / segmentLength) + 1;
            Vector3[] arr = new Vector3[detail + 1];
            for (int i = 0; i <= detail; i++)
            {
                arr[i] = this.GetPositionAt((float)i / (float)detail);
            }
            return arr;
        }

        #endregion

        #region IIndexedWaypointPath Interface

        public int Count
        {
            get { return _waypoints.Count; }
        }

        public IWaypoint ControlPoint(int index)
        {
            return _waypoints[index];
        }

        public int IndexOf(IWaypoint waypoint)
        {
            return _waypoints.IndexOf(waypoint);
        }

        public Vector3 GetPositionAfter(int index, float t)
        {
            if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
            if (_speedTable.IsDirty) this.Clean_Imp();
            if (_points.Length < 2) return (_points.Length == 0) ? VectorUtil.NaNVector3 : _points[0];

            index++; //index at 0 is an ignored control point, index should be 1-base
            int i = index * SUBDIVISIONS_MULTIPLIER;
            int j = (index + 1) * SUBDIVISIONS_MULTIPLIER;
            //float nt = _timesTable[i] + (_timesTable[j] - _timesTable[i]) * t;
            float nt = _speedTable.GetTimeAtSubdivision(i) + (_speedTable.GetTimeAtSubdivision(j) - _speedTable.GetTimeAtSubdivision(i)) * t;
            return this.GetRealPositionAt(nt);
        }

        public Waypoint GetWaypointAfter(int index, float t)
        {
            if (index < 0 || index >= _waypoints.Count) throw new System.IndexOutOfRangeException();
            if (_speedTable.IsDirty) this.Clean_Imp();
            if (_points.Length < 2) return (_points.Length == 0) ? Waypoint.Invalid : new Waypoint(_points[0], Vector3.zero);

            index++; //index at 0 is an ignored control point, index should be 1-base
            int i = index * SUBDIVISIONS_MULTIPLIER;
            int j = (index + 1) * SUBDIVISIONS_MULTIPLIER;
            //float nt = _timesTable[i] + (_timesTable[j] - _timesTable[i]) * t;
            float nt = _speedTable.GetTimeAtSubdivision(i) + (_speedTable.GetTimeAtSubdivision(j) - _speedTable.GetTimeAtSubdivision(i)) * t;

            var p1 = this.GetRealPositionAt(nt);
            var p2 = this.GetRealPositionAt(nt + 0.01f); //TODO - figure out a more efficient way of calculating the tangent
            return new Waypoint(p1, (p2 - p1).normalized);
        }

        public RelativePositionData GetRelativePositionData(float t)
        {
            int cnt = _waypoints.Count;
            switch (cnt)
            {
                case 0:
                    return new RelativePositionData(-1, 0f);
                case 1:
                    return new RelativePositionData(0, 0f);
                case 2:
                    return new RelativePositionData(0, t);
                default:
                    {
                        if (_speedTable.IsDirty) this.Clean_Imp();

                        if (_useConstantSpeed) t = _speedTable.GetConstPathPercFromTimePerc(t);

                        t = Mathf.Clamp01(t);
                        if (MathUtil.FuzzyEqual(t, 1f)) return new RelativePositionData(_waypoints.Count - 1, 0f);

                        int index;
                        float segmentTime;
                        if(_isClosed)
                        {
                            index = Mathf.FloorToInt(cnt * t);
                            segmentTime = 1f / cnt;
                        }
                        else
                        {
                            index = Mathf.FloorToInt((cnt - 1) * t);
                            segmentTime = 1f / (cnt - 1);
                        }
                        float lt = index * segmentTime;
                        float ht = (index + 1) * segmentTime;
                        float dt = MathUtil.PercentageMinMax(t, ht, lt);
                     
                        return new RelativePositionData(index, dt);
                    }
            }
        }

        public void Clean()
        {
            _points = null;
        }

        #endregion

        #region IConfigurableIndexedWaypointPath Interface

        public void AddControlPoint(IWaypoint waypoint)
        {
            _waypoints.Add(waypoint);
            _points = null;
        }

        public void InsertControlPoint(int index, IWaypoint waypoint)
        {
            _waypoints.Insert(index, waypoint);
            _points = null;
        }

        public void ReplaceControlPoint(int index, IWaypoint waypoint)
        {
            _waypoints[index] = waypoint;
            _points = null;
        }

        public void RemoveControlPointAt(int index)
        {
            _waypoints.RemoveAt(index);
            _points = null;
        }

        public void Clear()
        {
            _waypoints.Clear();
            _points = null;
        }

        public void DrawGizmos(float segmentLength)
        {
            if (_waypoints.Count <= 1) return;

            var length = this.GetArcLength();
            int divisions = Mathf.FloorToInt(length / segmentLength) + 1;

            for (int i = 0; i < divisions; i++)
            {
                float t1 = (float)i / (float)divisions;
                float t2 = (float)(i + 1) / (float)divisions;

                Gizmos.DrawLine(this.GetPositionAt(t1), this.GetPositionAt(t2));
            }
        }

        #endregion

        #region IEnumerable Interface

        public IEnumerator<IWaypoint> GetEnumerator()
        {
            return _waypoints.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _waypoints.GetEnumerator();
        }

        #endregion

    }
1 Like

@lordofduct Once again thank you for taking the time to explain this, and for sharing some code.

As you say a bezier chain might be the better option for me, I’ll look into using some of this over the next few days and implementing it into my project …

Cheers.