• The Goob Zone
  • Posts
  • Creating pre-made trajectory with bezier curves!

Creating pre-made trajectory with bezier curves!

Painting the picture

So lets say you want to add a projectile on a predetermined trajectory. Your first thought would be to use a rigid body and physics.

But you also want to have the velocity and range of the projectile to configurable separately as well as show the player where that projectile will land.

There are 2 problems with this, the first being that if you increase the velocity of a projectile, that will then affect how far that projectile travels. Which means you cannot configure the range and velocity separately.

The second problem is that if you want to calculate the trajectory or landing position of your projectile using physics, you have to know what this means:

y = x tan θ − gx2/2v2 cos2 θ

How do we avoid using that black magic shown above?

This is where the easier solution called bezier curves come into play! What is a bezier curve you may be asking?

Now before we continue, I encourage you to create a sample project in godot and code along with me here so you can mess around with the values and get a feel for how it works!

The simplest version of a bezier curve is the quadratic bezier curve with 3 points in space. 

Picture of what this would look like.

So if you are coding along, create 3 marker 2D nodes in a similar pattern shown to this graphic. Make sure to name them accordingly.

Also to make it easier to tell where the

points are, add a sprite 2d as a child to all of your points. On the sprite you can use a scaled down version of the godot logo as a visual representation of where your points are!

Example of how your sample project would look like. I scaled down the godot logo to .3 so it doesnt take up much of the screen.

Now I’m going to plant the seed of what a finished bezier curve looks like in action before I start explaining. This way you can have an easier time connecting dots as you’re reading!

“Goob what the hell, this looks still looks complicated you said it would be easy!” Calm down calm down, after I’m done explaining, this will make complete sense!

The first thing we have to do is lerp from P0 to P1 while also lerping from P1 to P2 at the same time. Now you may be asking “what does lerp mean?”.

Explaining what lerp means

When you lerp between 2 points, think of it like you’re moving from point A to point B by covering a specified percentage of the total distance between the 2 points.

Luckily for us, Godot has a lerp function that does this math for us so we don’t have to! The lerp function in Godot looks like this:

var new_pos = position1.lerp(position2, percent)

Now the lerp function returns a new position. These positions are based on the specified percentage of the total distance to cover between the 2 points.

For example if the percent value was .5, the lerp function would find and return the position 50% of the distance to the next point, or in other words, the middle.

If you are coding along, go ahead and add a Node2D to your main scene and name it projectile. Also add a sprite to that node so that you can see it.

Note that I made the projectile sprite bigger, put it lower in the Z index than the other points, and also made it yellow. This is optional as I wanted to tell the difference between all of these nodes

 

The next thing I want you to do with this projectile node is attach a script to it and copy paste the code shown below.

extends Node2D

#References to the positions we created. Be sure to set these in the inspector tab
@export var pos0: Marker2D
@export var pos1: Marker2D
@export var pos2: Marker2D

#Keeps track of percentage for how far along the curve the projectile is
var percent: float = 0

#Variables used later on
var distance: float 
var projectile_speed: float

#Example to experiment with the lerp function
func _ready():
	self.global_position = pos0.global_position
	var new_pos = self.global_position.lerp(pos1.global_position, .5)
	self.global_position = new_pos

After you copy paste the code, make sure you set the p0, p1, p2 positions!

Setting the references to the points

Now what I want you to do is start the scene by playing it. What you will notice is that the projectile will now be in the middle of point 0 and point 1!

You can mess around with the .5 value in the lerp function to test where the projectile will end up!

Now that I’ve explained what lerping is and set up the foundation for the bezier curve, lets bring this bad boy to life.

Bringing the bezier curve to life

So how are we gonna do this? Well let’s first think back to that graphic. What is it doing? Lets go ahead and break it down piece by piece!

Well if you look close enough you would notice 2 green dots moving from p0 to p1 and p1 to p2 respectively. How is this achieved?

By lerping of course! So let’s go over how that’s done!

The below code snippet is a function that will serve as the basis for our bezier curve! Go ahead and copy paste it into your script.

You can also go ahead and delete the _ready function because we wont be using that anymore! So go ahead and replace it with this function here!

func bezier(p0: Vector2, p1: Vector2, p2: Vector2, percent: float):
	var q0 = p0.lerp(p1, percent)
	var q1 = p1.lerp(p2, percent)  

This function is responsible for lerping between the points along our bezier curve. Notice that I am storing the positions returned from the lerps into variables q0 and q1.

The q1 and q0 positions represent the green dots in the graphic. They are necessary to create the curve.

The next thing we will be adding to this function is lerping from q0 to q1. This is represented by the green line between the positions q0 and q1 on the animated graphic.

Below is the code for achieving this:

var projectile_pos = q0.lerp(q1, percent)

If you couldn’t tell by the name of the variable, this new position will be the actual position of our projectile in 2d space. This position represents the tip of the curve shown in the animated graphic!

Now here is the complete function for the bezier curve. Go ahead and put this inside of your projectile script!

func bezier(p0: Vector2, p1: Vector2, p1: Vector2, percent: float) -> Vector2:
	var q0 = p0.lerp(p1, percent)
	var q1 = p1.lerp(p2, percent)
	
	var projectile_pos = q0.lerp(q1, percent)
	
    return projectile_pos

Every part of this function should be familiar and understandable based off of everything I’ve told you so far!

Although we still aren’t done yet because we have to call this function and increase the percentage of distance covered over a period of time!

Doing this is quite simple. First we have to call our bezier function inside of the physics_process function built into Godot. This function is called every physics frame.

Below is the code necessary to achieve this. Go ahead and add the functions and this variable to your script!

#This variable will be used to influence how fast the percent increases as seen in the process function below
@export var projectile_speed: float = 500


func _ready():
    distance = pos2.global_position.distance_to(pos0.global_position)


func _physics_process(delta):

   #Will stop moving the projectile once it covers 100% of the curve
	if(percent >= 1):
		return
	
	#Calling the bezier function and storing the returned position
	var new_pos = bezier(pos0.global_position, pos1.global_position, pos2.global_position, percent)
	
	self.global_position = new_pos
	
	#Calculating how much to increase the percent of distance traveled  
	percent += delta / (distance / projectile_speed)

Now you have probably noticed that there is a calculation going on with this variable called delta which is being divided by distance over arrow speed.

That formula essentially calculates the velocity of the projectile in delta time instead of seconds. But why would I use some weird variable and not seconds?

Explaining delta time and the code above

Well here is a rundown of what that calculation means, basically since we are trying to simulate physics, we have to use some basic math.

The distance variable is obtained by using a function that calculates the distance between 2 points (in this case p2 and p0)

The arrow speed variable is simply a variable that will be used to configure the projectile speed so it can be whatever you want it to be.

Now the funky part is that the formula distance / speed finds the time it takes to reach that distance, the funky part is that you really should be using delta time.

Delta time is the time in seconds between frames. This is important because lets say for example one person is running the game at 30fps and another is running it at 60fps.

Well since every process function in Godot is called every frame, the 30fps user will have their function called 30 times per second, and the 60fps person will have their function called 60 times per second.

This is very bad because that means that the game runs completely different depending on the users framerate, and framerate is never constant or stable. This will lead to inconsistent messy gameplay.

That is why delta time is so important, delta time helps keep numbers independent of the users framerate. So incorporating that value into any calculation called in a process function is vital.

So in my formula, I simply divided delta by the result of distance / projectile_speed. I then added that result to the percentage of distance covered.

This will make all positions shift the next time the bezier function is called because the percentage of distance covered is increasing every time the process function is called

This finally leads to the result of the graphic I showed you guys earlier.

This is the result of all of our hard work.

It should all start coming together now! I encourage you to play around with the code now that I’ve explained everything!

Start changing values around, changing the positions of the markers, or just breaking the code to get a good feel and understanding for how it actually works!

Alright Goob, bezier curves are cool and all but does this solve the problem stated at the beginning of the newsletter with the configurable speed, range, and landing position?

Well let’s break that down!

Projectile motion

The curve of the bezier curve that we created basically simulates a projectile being launched in a forward/upward direction. Think a tank or a catapult type of projectile!

However also keep in mind that the peak of the curve (in our case p1) must be in the middle and above p0 and p2 to get the trajectory to look the way it does!

The positions of your points really matter for the type of trajectory you want to create. For example here is the same curve we have been working with, however p1 and p2 have shifted. Point O represents the projectile.

Remember to place your points accordingly depending on the type of curve you want. Also don’t mind it moving back and forth.

Projectile speed and range

Alright so how does a bezier curve help us make the projectile speed and range independent of one another?

Remember that projectile speed variable I made you put on the projectile script. As well as that formula delta / (distance / projectile_speed)?

Simply by changing the speed variable we manipulate how much the percent of distance covered increases over delta time! So the projectile can reach the end of the trajectory slower or faster depending on how we like it!

What about the range of the projectile? Well that is simply determined by how far away p2 is from p0 because the projectile always will always make it’s way towards the final point.

So in a nutshell to create a projectile with independent configurable speed and range, using a bezier curve is the easier choice and simpler choice.

Showing where the projectile will land and stopping it

What about that last part about showing where the projectile will land?

Well numb-nuts, all you have to do is add a sprite to that last point (p2) and hide the rest. This works because p2 should be the end of the projectiles trajectory! But wait, the projectile doesn’t stop there it keeps going!

To make sure that the projectile stops at that point, all you have to do is add an if statement at the beginning of the physics process function to check if the projectile has travelled 100% of the curve! Here is that code:

if(percent >= 1):
     return

All that is doing is exiting the process function before it can run any of the other code, therefore it will not increase the percent any further and the projectile will stop at 100% of the trajectory path which is where p2 is.

Conclusion

So if you have followed along so far, the final scene should look like this:

As for the projectile script, it should look like this:

Again I encourage you to mess around with the values and project as a whole to get a good feel for how this works! Besides that, you now know everything you need to know to create any kind of bezier curve and how to apply it to create a pre-made trajectory path for a projectile!

Next week I’ll be covering the tilemap system in Godot! It will cover all features all the way up to Godot 4.2!

See ya next week you goobers!