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.
Answer by PvtPuddles · Feb 27, 2020 at 09:14 PM
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!
Lets say we have a 'y' junction with two 'arms' and a 'base'. You want to add the switch controller as a component to the rail you are using as the base. Options 1 and 2 are the two arms that the switch will swap between.
This is awesome, I was also trying to work out a solution to the move to another path at end problem. I've tried to take your solution from the answer here but run into a small issue where an index out of bounds exception occurs in VertexPath.CalculatePercentOnPathData at the point where there's a while loop trying to find the closest vertices. This happens at the end of my path. Is there something missing from your answer, perhaps a new case statement for SwitchPath which needs adding to this method?
thanks in advance
Follow up - I've lazily worked around the issue by constraining the index "i" with a Math.Min call so I have: int i = System.Math.Min((NumPoints - 1), Mathf.RoundToInt (t * (NumPoints - 1))); // starting guess
I don’t recall running into this problem myself, but that sounds like a viable solution
Answer by streeetwalker · Feb 25, 2020 at 06:48 PM
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...
...yes. So you are right that finding the endpoints isn't too hard, but what I am looking for is something more along the lines of the object that follows the path figuring out whether or not it is at the end of the path it is on. This is tricky because as far as the object is aware, it gives its current position to the path object, then the path does a bunch of magic-wizardry, then returns a new position for the object to move to. During that magic wizardry, the path handles cases where the object has finished following the path. It is not clear to me if the path can then communicate back to the follower object and tell it that it has reached the end (or the beginning, if it is going backwards).
$$anonymous$$aybe you're trying to make it too hard? If you know the start and end points say in world coordinates (and any points in between that you've passed to that function), you can compare world position of the object as it moves it to those points outside of anything his script is doing.
So you write a script that compares those in an update loop. Then when the object is close enough to one of those points, I'm sure you can figure out how to kill the current animation and swap it out to a new path... Not sure how easy that is but it is doable. Seems like you just need to remove the object from it's parent path and make it a child of the new path.
You might contact him, because I am sure he is approachable and up for suggestions. $$anonymous$$aybe he'd consider adding features that would allow you to say, jump an object to a new path, or take a longer path and split it up if you don't want to figure it out yourself. At the very least I think he'd give you some pointers.
I think I am making it a little too hard, but thats mostly because this is going to be the base of my game, so it needs to work exactly how I would like it to. Your solution sounds plausible, so I may resort to that as a last-ditch solution. The main problem I see with it is when speed is high, but the segment length is low. In this scenario, it is possible to move past the entire segment in one frame, and so the path never realizes you passed the end. This is a problem I had grappled with in previous attempts, but using the t time values, Lague's completely solves it. Once you know you need to swap out the path, it's nice and easy. You just assign a different path node to the follower, so that shouldn't be a problem. Lague's script even takes care of making sure that the object is on the path you swap to.
I'll see if I can shoot him an email, but this project of his is over a year old, so it's a hit or miss.
Your answer
Follow this Question
Related Questions
Using triggers to then follow a specified path 0 Answers
Making the player follow a path 0 Answers
Math Library for bezier curves. 0 Answers
How can I setup a AI Bot to Pathfind around my small map? 0 Answers