How can you make a path with a switch in it?

I have been thinking this project over for a couple years, but I cannot figure out how to make an object follow a bezier curve, but able to swap to a different path midway (say with a railroad switch).

Recently I have been working with Sebastian Lague’s Path Creator tool (which is outstanding for applications outside of this, by the way), but there is no easy way to split a path into two. The route I was planning on taking was switching the path object attached to the follower when it reached the end of the path to one of the two paths that start at that endpoint. Unfortunately, Lague’s end behavior is math based (ie. clamp the %-time value to 1) as part of calculating it’s position, and thus there is no easy (to my untrained eye) way to access the endpoint behavior outside of this system.

Does anyone have any other suggestions for creating a pathing system that can be split, merged, and otherwise futzed around with? The only other option I’m seeing would be to rebuild Lague’s with easier to access end behavior, but that would be an enormous time sink for a tiny change.

With enough hours swearing and crying, I though it would be best to share my solution, so that all of y’all can use this if you need it. This solution is based off of Sebastian Lague’s Path Creator, which videos of can be found on YouTube (as well as download links).


Step 1: Disable Lague’s default Endpoint Behavior

Lague did an excellent job making loop, reverse, and stop behavior using clamp01, pingpong, and the like, but we don’t need it. Instead, we will add a fourth option to EndOfPathInstruction, SwitchPath.

public enum EndOfPathInstruction {SwitchPath, Loop, Reverse, Stop};

This will allow the block to reach the end of the path.

Step 4: Add some helper functions

In vertexPath.cs, navigate to the Public methods and Actors field, where we will add the following two methods:

public float GetTAtDist(float dst, EndOfPathInstruction endOfPathInstruction = EndOfPathInstruction.SwitchPath)
        {
            float t = dst / length;
            return t;
        }
        
        // Gets a distance value based on t value
        public float GetDistAtT(float t, EndOfPathInstruction endOfPathInstruction = EndOfPathInstruction.SwitchPath)
        {
            float dist = t * length;
            return dist;
        }

While this functionality is already built into Lague’s system, it relies on the time value t to always be between 0 and 1, but we need to be able to tell when t is outside of those bounds.

Step 3: Add our own Endpoint Behavior

First, we will add a RailController.cs script to the current rail (or path) to store what the next and previous rails are to this one.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace PathCreation.Examples
{
    public class RailController : MonoBehaviour
    {
        public GameObject previousRail;
        public GameObject nextRail;


        public GameObject NextRail()
        {
            if(nextRail != null)
            {
                return nextRail;
            }
            return null;
        }
        public GameObject PreviousRail()
        {
            if (previousRail != null)
            {
                return previousRail;
            }
            return null;
        }
    }
}

The game objects attached to this script should be the parent objects that Lague’s PathCreator script is attached to for the next and previous rail, respectively.

Next, in PathFollower.cs, we need to add:

public GameObject rail;
public RailController railController;
float railDist;

Note: railDist is being used to replace distanceTraveled, so that we can continue tracking the total distance the object has traveled. If this is not important to you, you can continue using distanceTraveled wherever you see railDist.

if (rail != null)
       {
           pathCreator = rail.GetComponent<PathCreator>();
           railController = rail.GetComponent<RailController>();
       }

Remember to initialize the path creator and rail controller in start().

In update(), we are going to want something that looks like this:

if (rail != null)
                {
                    if (pathCreator != null)
                    {
                        //Total distance traveled
                        distanceTravelled += speed * Time.deltaTime;
                        //Distance on this particular rail
                        railDist += speed * Time.deltaTime;
    
                        transform.position = pathCreator.path.GetPointAtDistance(railDist, endOfPathInstruction);
                        transform.rotation = pathCreator.path.GetRotationAtDistance(railDist, endOfPathInstruction);
                    }
    
                    if (endOfPathInstruction == EndOfPathInstruction.SwitchPath)
                    {
                        float t = pathCreator.path.GetTAtDist(railDist, endOfPathInstruction);
                        // Goes to previous rail if you have backed off
                        if (t <= 0 && railController.PreviousRail())
                        {
                            // Updates the rail that the follower is on, as well as the pathcreator object
                            rail = railController.PreviousRail();
                            pathCreator = rail.GetComponent<PathCreator>();
                            railController = rail.GetComponent<RailController>();
                            // Puts the follower at the end of the next rail, rather than the beginning
                            railDist = pathCreator.path.GetDistAtT(1, endOfPathInstruction);
                        }
                        // Goes to next rail
                        else if (t >= 1 && railController.NextRail())
                        {
                            // Updates the rail that the follower is on, as well as the pathcreator object
                            rail = railController.NextRail();
                            pathCreator = rail.GetComponent<PathCreator>();
                            railController = rail.GetComponent<RailController>();
                            // Puts the follower at the beginning of the next rail, rather than the end
                            railDist = 0;
                        }
                    }
                }

At this point, we should have the ability to move from one path to another (which don’t necessarily even need to touch), which at the moment is a little redundant, given that Lague has support for compound curves. However, the real treat is in…

Step 5

Now we have the infrastructure to create a short and simple SwitchController.cs. This gets attached to the base of the switch, or the segment before the split.

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

namespace PathCreation.Examples
{
    public class SwitchController : MonoBehaviour
    {
        public bool nextRail = true;
        public int target = 1;
        public GameObject option1;
        public GameObject option2;
        private RailController rail;

        void Update()
        {
            rail = gameObject.GetComponent<RailController>();
        if (nextRail)
        {
            if (target == 1)
            {
                rail.nextRail = option1;
            }
            else
            {
                rail.nextRail = option2;
            }
        }
        else
        {
            if (target == 1)
            {
                rail.previousRail = option1;
            }
            else
            {
                rail.previousRail = option2;
            }
        }
        }
    }
}

Option 1 and 2 are the game objects of the two rails you want the switch to switch between, and target is a simple int to change the rail selected, and is scale-able up to as many rails as you want per junction. Disable nextRail if the switch is a merger, so that going backwards it still acts like a switch.

With this, you should be done. If you find any errors with this (besides nullpointer errors, because I haven’t even started to weed those out), please let me know. Also, this code is horrendous. Any suggestions for improved readability or just simplification would be greatly appreciated!

Interesting problem. I’m sure you can dig into his code and find where the actual control points are.

He does provide a method, if you read his documentation to, to create a Bezier path using a list of Vector2 or 3’s. If you did that, then you’d know where the endpoints are…