The first thing you’ll want to do is create the path the player can move along. There are numerous ways to do this, but the easiest to explain would be using a custom spline.
To do this, create a new actor class and add a spline to it.
For simplicity’s sake, I’ve made it the default root as well, but that isn’t strictly necessary. I’ve named the actor PathSpline.

Leave the spline here alone- you’ll modify it in the level rather than blueprint.
As well as that, we want to create an array of integers.
This array will dictate which points we move through but don’t stop at. This allows for more complex movement if you want it.

Simply place the actor in your level, click on one of the dots, and move it around. Each of these dots is a path point.
To add another point, simply hold Alt when moving one- it’ll create a new point and move that instead.
The rotation of these points is the direction that the player will face when at that point, so make sure that’s how you want it.
Now that we have the path, let’s create everything we’ll need in the player.

Distance Per Second is the only one you need to change right now.
This is how fast you move along the path. You’ll have to trial and error to find what you want, but you’ll probably want something ~1,000
We then want to get a reference to our created path. We’ll get the actor and use it to set our just created variable.
Before properly implementing the AttemptMoveForward event, we want to create the timeline we’ll use.
Double click to open the timeline once you’ve placed it. This is what it’ll look like
Change the length to 1 and add a float track. I named the float track progress, but you can name it anything you’d like.

Next, add two keys to the graph. You can do this by right clicking:
Set one of these points to 1,1 and the other to 0,0
Right click on both of them (one at a time) and set their key interpolation mode to auto. This’ll smooth things out.
Result:
Next, let’s create our empty AttemptMoveForward event.

We then want to go into our UI and call the attempt move forward event whenever our move forward button is pressed.
Now that it’s getting called properly, we’ll actually create it. First things first, the basics:
If we’re not moving, we record our current index as the last one, and increment it.
Next, we increment the position index until it’s valid. We do this by checking if it’s not an ignored index:
After we get our new index, we want to play our timeline (from the start).
But before we can do that, we have to adjust the play rate. If you remember, our timeline lerps from 0 to 1 over the course of 1 second. Leaving as is, we could travel an infinite distance over the course of one second.
To combat this, we want to adjust our play speed such that we move at exactly our desired distance per second (speed).
With that, the AttemptMoveForward event is complete.
Finally, we implement the timeline. The meat of the operation.
We simply lerp between our last position’s time and our target position’s time. We don’t want to directly lerp between the transforms at those times since doing that would cause us to move in a straight line.
Once we’re done, we say we’re not moving anymore by setting the appropriate boolean to false.
We have to use this indirect way since there is no GetTimeAtSplinePoint node, but that’s effectively what we’re doing.
Make absolute sure that the coordinate space is in world space: