-
Notifications
You must be signed in to change notification settings - Fork 8
Functions Explained In Depth
This section explains, in detail, how each function is used and how they work. I’m going to go a bit out of order when explaining how the functions work. I’m first going to explain “Step_Player_Up()”, then “InterpolateCamera()”, “Touch_CheckAndExecute()”, and “Slope_AffectSpeed()”.
Here is a quick reference of the functions and what they do, in case you need it.
-
InterpolateCamera(Prev_Pos_Local_Y, Time_Current, Time_Delta)
- InterpolateCamera( float, float, float )
- Description: Interpolates the camera position when moving up a step or staircase for smooth camera movement going up them.
- Arguments:
-
Prev_Pos_Local_Y
- Type: float
- Description: The previous local position of the camera before the player was moved up the step.
-
Time_Current
- Type: float
- Description: The current time of the camera interpolation.
-
Time_Delta
- Type: float
- Description: The delta, in seconds, from the last frame.
-
Prev_Pos_Local_Y
-
Step_Player_Up()
- Step_Player_Up( void )
- Description: Checks to see if there is a step to be stepped upon, and moves the character up if there is.
-
Touch_CheckAndExecute()
- Touch_CheckAndExecute( void )
- Description: Checks for any “touch” functions inside the currently colliding objects, and activates them if there are.
-
Slope_AffectSpeed()
- Slope_AffectSpeed( void )
- Description: Checks to see if the player is on a slope of some kind and affects speed accordingly.
This function simply steps the player up when encountering a step. This function should generally be used after a “move_and_slide()” is done so that the wall, floor, and ceiling states can be updated.
if(SlideCount > 1):
The first thing it does is see if there are more than one slide collisions. This way the function only executes when encountering more than one slide, like if the player is running against a wall that would constitute 2 to 3 slides.
if(State_OnFloor):
It then checks to see if the player is even on the floor. How can you step up something without being on the ground?
if(State_OnWalls):
Then it checks to see if the player is on a wall. This is way if there are multiple slides, like going up a ramp, it won’t try to step up the ramp. There can also be multiple slides when simply walking on a level floor. Usually there are 2 slides when doing so. I’m not totally sure why this is.
for Slide in range(SlideCount):
If the player is on a wall, floor, and has more than 1 slide we start a “for” loop that goes through each slide.
if(get_slide_collision(Slide).normal.y <= MaxFloorAngleNor_Y):
It first checks to see if the slide collision in question is a wall by checking it against the max floor angle normal (“MaxFloorAngleNor_Y”).
Step_CollPos = get_slide_collision(Slide).position
If it is, it gets the position of the collision in global space.
Player_GlobalFeetPos_Y = Player_Position.y - (Player_Height / 2) - get("collision/safe_margin")
Then it gets the global position of the player’s “feet”, the very bottom of the player character’s collision shape. By default this is 0.9 meters below the player character’s origin, since the default character height is 1.8 meters. But this “0.9” does not include the safety margin of the kinematic body, so we include that too.
if(Player_GlobalFeetPos_Y < Step_CollPos.y):
It then checks to see if the global position of the collision slide is even above the player’s “feet” in the first place.
Step_CollPos_RelToPlayer = to_local(Step_CollPos)
If it is it gets the local position of the collision position by using “to_local(Step_CollPos)”. Since this is called inside “Player.gd" that is attached to “Player.tscn”, it returns the collision position in the player’s local space.
Step_PlayerVel_Global_Norm = Vector3( Slope_PlayerVelVec2D.x ,
0.0,
Slope_PlayerVelVec2D.y )
It now gets the player’s global horizontal velocity and puts it into a variable. This is so we can compare it between the step’s angle and the player’s velocity later.
What we want is the horizontal velocity of the player in a normalized vector.
The elements of this vector are from “Slope_PlayerVelVec2D”, which is set inside “Slope_AffectSpeed()” whenever the player is walking on the ground, so there’s no need to worry about errors due to “Slope_PlayerVelVec2D” not being set, as it will always be whenever the player moves on the ground.
Also, “Slope_PlayerVelVec2D” has already been normalized inside of “Slope_AffectSpeed()”, so there is no need to do it here.
Note that it is a 3D vector, but the middle element (the Y axis) is “0.0”. This is because we are going to be comparing the this against another 3D vector. More on that when we get there.
Step_CollPos_Global_RelToPlayer = Vector3(Step_CollPos.x, Player_Position.y, Step_CollPos.z) - Player_Position
What we do here is get the relative position of the collision compared to the player’s position in global space. We do this by simply subtracting the player’s global position from the collision’s global position.
The Y axis, or the second element, of this vector will always equal “0”. This is because the second element of the first vector in this code is the player’s y position. That’s so we can just compare the horizontal axis positions without having to worry about the vertical position, which isn’t important in this case.
I could have converted the X and Z axis of the collision and player positions into a 2D vector, but that would take extra work on the computer’s part and so we are just going to compare two vectors in the upcoming code in which both vector’s Y axis are “0”.
Step_CollPos_Global_RelToPlayer = Step_CollPos_Global_RelToPlayer.normalized()
Now this just normalizes the collision position relative to the player, so we can use it in a dot product in the next line of code. Check the “Normals Explained: Normalization” section of this manual for information on vector normalization.
Let’s recap what has happened so far, first by looking at this picture:
In this picture, we have the player moving north and a little bit to the east. Let’s say that it’s about “(0.24 , 0.0, 0.96)” (remember that it has been normalized). And then the collision position is exactly to the east of the player. So let’s say that it is “(1.0, 0.0, 0.0)” (this is also normalized).
So now what we want to do is find out the angle between these two vectors. We’ll find out how why we need it later.
Step_CollPos_AngleToPlayer = abs( asin(Step_CollPos_Global_RelToPlayer.dot(Step_PlayerVel_Global_Norm)) )
In this line of code, we have a lot going on.
The first thing we need is the dot product of the “Step_CollPos_Global_RelToPlayer” vector and the “Step_PlayerVel_Global_Norm” vector, so in this line of code we do:
Step_CollPos_Global_RelToPlayer.dot(Step_PlayerVel_Global_Norm)
To save time, I’m going to tell you that the dot product is “0.246385”. Now, what we want to do is find the arcsine (“asin()” in Godot) of that number to get the angle in radians between the direction that the player is walking and the direction of the collision position.
This is trigonometry, so let’s take this:
And look at it like this:
You see why we need trigonometry? In the end, vectors and relations between them are all just triangles, even in 3D. But we will just stick to 2D for now, as it is simpler and all we care about right now is the horizontal plane, anyway.
Like I said, we need the angle between the collision position and the player’s velocity. We do this by using “asin()” on the dot product we just got. So the code looks like this:
asin( Step_CollPos_Global_RelToPlayer.dot(Step_PlayerVel_Global_Norm) )
And if we were to use real numbers:
asin( 0.246385 ) = 0.2489484 radians = 14.263692637 degrees
So now we see that the angle between the player’s velocity vector and the collision position is “1.015985294” radians, which is about “14.26” degrees.
Okay, that all great, but what’s the point? Let’s look at the next line:
if(Step_CollPos_AngleToPlayer > 0.200712864):
It checks to see if the angle between then player’s velocity vector and the step collision vector is less than about “0.2007” radians, or “11.5” degrees.
The reason is that when the player is running along a step within ~11.5 degrees, the script attempts to step the player up, but since the player is still moving almost parallel along the step, the player slides along the side of the step and isn’t able to get completely on the step, and therefore falls back down.
And example picture:
Notice that X velocity of the player isn’t very much? In fact, it’s so little that it isn’t able to move the player completely onto the step, and instead slides along the side of the step. And when the player is moved up in order for the player to get on the step, the player has already been slid on the side of the step, and is therefore not on the step itself when moving.
If the player is moving at more than a 11.5 degree angle, there is enough velocity of the player towards the step that he is able to move onto it.
It can be difficult to visualize what’s going on in your code, but it’s essential to practice doing so as it is a needed step in problem solving. If you have a hard time doing so I suggest putting your head down, eyes closed, in a quiet room and give yourself time to think about it.
The next lines of code are:
Step_CollPos.x = to_global(Step_CollPos_RelToPlayer * Step_RaycastDistMultiplier).x
Step_CollPos.y = Player_GlobalFeetPos_Y + Step_MaxHeight
Step_CollPos.z = to_global(Step_CollPos_RelToPlayer * Step_RaycastDistMultiplier).z
These lines of code push the collision position vector out a little bit so that it is over the step geometry instead of being on the edge, as shown here:
So if our relative collision position was something like “( 0.5 , 0.0 , 0.0 )” after multiplying it by “Step_RaycastDistMultiplier” (which is set in the “SETTINGS” section as “1.1”) we get “( 0.55 , 0.0, 0.0 )”. We convert it the X and Z axis of these lines to global space, which is needed for the ray cast, as it takes Vectors in global space.
Notice that we set the Y axis position of the collision position to be the vertical position of the player’s “feet” plus the maximum height that a step can be, which by default is “0.5”.
The reason we are doing all this is because we are going to use “Step_CollPos” as the starting point for several ray casts.
The first being the next few lines of code:
Ray_SpaceState = get_world().get_direct_space_state()
Ray_From = Vector3(Player_Position.x , Step_CollPos.y , Player_Position.z)
Ray_To = Step_CollPos
Ray_Result = Ray_SpaceState.intersect_ray(Ray_From,Ray_To,[self])
“Ray_SpaceState” get the state of the physics “world”, that is it get the state of everything in the scene that has anything to do with physics, like collision shapes, locations, etc. We call the ray tracing function from this space state.
“Ray_From” sets the position that the ray cast is going to start from. In this case, it is going to be at the player’s horizontal position, and the step collision’s vertical position.
“Ray_To” sets the position that the ray is going to be cast to. It’s going to be what we set it as, before.
“Ray_Result” cast the ray by using “Ray_SpaceState.intersect_ray()” and keeps the result of that ray cast, which we can look at in the next line of code.
So let’s get a visualization of the ray cast to see what we are doing:
So to start off with, “Ray_To” is the collision position’s X and Z position. And it’s Y position is the position of the player’s “feet” plus “0.5”, which is the maximum height that a step can be, according to the default “Step_MaxHeight” setting at the top of the script.
This ray cast is needed because imagine that there are two separate steps, with their own independent collision bodies, on top of each other. If the player were to walk into these steps, one of the slide collisions will be on top of the bottom step. I don’t know why this is, that’s just how the physics in Godot work. And the ray cast done above makes sure there isn’t anything in the way of the player being able to step up onto the step being collided with. Here’s a visualization:
The red dot represents a collision slide that would be used for stepping up the player. The ray cast we are currently talking about is represented here, and shows why it is necessary.
if(Ray_Result.empty()):
It first checks to see if the ray cast just done is empty. If it isn’t, then nothing happens as the player isn’t able to step up with something in the way, like it would be in the above picture. And if it isn’t the last slide collision, it goes to the next slide.
But, if it is empty, we move on with the code.
Ray_From = Step_CollPos
Ray_To = Vector3( Step_CollPos.x , Player_Position.y + (Player_Height * 0.5) + ( to_global(Step_CollPos_RelToPlayer).y - Player_GlobalFeetPos_Y) , Step_CollPos.z )
Ray_Result = Ray_SpaceState.intersect_ray(Ray_From,Ray_To,[self])
Now we are going to setup and execute another ray cast. This time, “Ray_From” is simply the modified step collision position. “Ray_To” is the collision position, also. But, the vertical position is the player’s origin position, plus half of the player’s height, plus the height of the step. It then casts a ray. Here’s a visualization:
The reason we need this ray cast is to check and see if there is a floating platform above the collision position that could block the player from being able to get onto it. Here’s a second visualization:
So what we are doing is casting a ray up to see if there is anything above the step that could obstruct the player from moving onto the step. In this picture, there is a floating platform in the way.
The reason that the ray cast goes so high is that it shoots all the way up to where the top of player’s collision shape would be if he were on top of the step.
if(Ray_Result.empty()):
Then it checks to see if the ray cast hit anything. If it did, nothing happens, and if it isn’t the last slide collision, it goes to the next slide. If it didn’t then we move on.
Ray_From = Step_CollPos
Ray_To = Vector3(Step_CollPos.x , Player_GlobalFeetPos_Y , Step_CollPos.z)
Ray_Result = Ray_SpaceState.intersect_ray(Ray_From,Ray_To,[self])
We setup and execute one last ray cast. This ray cast shoots down onto the step.
“Ray_From” is the same as it was last time, being the modified step collision position. “Ray_To” is the step collision position, but with the Y position being equal with the player’s “feet”. Here’s a visualization:
And from the side:
And then we have the next line of code:
if(not Ray_Result.empty()):
And yet again we check to see if the ray cast has a result. But this time we are checking to see if it has something in it instead of checking to see if it’s empty. If it has a result then we continue on with our code. If it doesn’t, it goes to the next slide, or in case that was the last slide it exits out of the “for” loop.
if(SteppingUp_SteppingDistance == 0):
Then we check to see if the stepping distance has been set from a previous loop iteration, to avoid having it set twice.
Step_Cam_PosBefore_Global = to_global(Node_Camera3D.translation)
Here we get the current global location of the camera (before it’s moved up with the player), and put it into a variable.
If the camera is being interpolated from a previous step and hasn’t finished doing so, this variable becomes the new starting point for the interpolation.
SteppingUp_SteppingDistance = (Ray_Result.position.y - Player_GlobalFeetPos_Y + Step_SafetyMargin)
Then we find out the distance to move the player up by getting the vertical position of the ray cast result and subtracting the player’s “feet” vertical position from it, so we can get can get the height of the step.
As an example, say that the player hits a step. The player’s origin on the Y axis is “2.5”. To get the vertical position of the player’s feet we subtract half of the player’s height from “2.5”. The player’s height is “1.8”, so half that is “0.9”. So we subtract “0.9” from “2.5” to get “1.6”, the Y axis position of the bottom of the player.
Then we get the Y axis position of the step. Let’s say it’s “2.0”. So let’s take that and subtract “1.6” from it. We get “0.4”, which is the height of the step.
After we get the height of the step we add the step safety margin to it all to make sure the character fully rises above the step.
CamInterpo_StartingPos_Local_Y = to_local(Step_Cam_PosBefore_Global).y - SteppingUp_SteppingDistance
Now we need to get the starting position of the camera in the player’s local space. We do this by getting “Step_Cam_PosBefore_Global” (which was set two lines above), converting it to local space with “to_local()”, and then getting the Y axis component of it.
After we get the local Y axis component of the the camera position, we subtract from it the distance that we are going to move the character up, so that the camera will have the same vertical position before and after the player is moved up a step. To illustrate:
In this picture, it shows how after the player is moved up, the global vertical position of the camera is still the same. We do this by subtracting the local position of the camera by the height of the step, because remember that whenever we move the player we move the camera. So any changes to the camera will need to be done in local space.
if(CamInterpo_StartingPos_Local_Y < 0.0):
CamInterpo_StartingPos_Local_Y = 0.0
The next couple of lines of code. First we check to see if the starting position of the camera (in local space) is below “0.0”. This is because a local vertical position below “0.0” means that the camera is more than half way down the player! That’s too low. I would rather have the camera “jump” in such circumstances than to have the player all the way down to the player feet, if he happens to be moving fast enough.
So if the camera’s starting position happens to be below “0.0”, we just set it to “0.0” to keep it from going too far down.
global_translate(Vector3(0.0, SteppingUp_SteppingDistance, 0.0))
Okay, here we actually DO something, instead of just calculations and crap. This line actually moves the player up over the step. Note that we are not using “move_and_slide()”. This is because I don’t need any slides to take place. I just need to move the player up. Also be aware that “global_translate()” uses the physics engine, which means that you can’t clip through objects with it. At least not with the Bullet physics engine. I haven’t tested with Godot’s built-in physics engine.
So if we accidentally move the player up into something that is hanging over the step, the player won’t get stuck inside of it, so that’s cool.
Node_Camera3D.translation.y = CamInterpo_StartingPos_Local_Y
This line also actually does something. After moving the whole player up, the camera now needs to come back down to what it was before, so that the camera will be at its starting position for the vertical camera interpolation.
We have already calculated where to move the camera and placed the result in “CamInterpo_StartingPos_Local_Y”. So all we do have to do is make the camera’s vertical position whatever is in “CamInterpo_StartingPos_Local_Y”.
Note that this line of code isn’t actually “moving” the camera like it would if we used “global_translate()”. Instead what it does is simply change the camera’s position, regardless of what it was before.
And, of course, this changes the camera’s local position. (I hope I’m not being too confusing).
CamInterpo_DoInterpolation = true
This line sets the flag that tells the script to vertically interpolate the camera. Check the next section, “InterpolateCamera()”, for more information.
CamInterpo_CurrentTime_Secs = 0
This resets the camera interpolation timer back to 0 since we have a new step.
Okay, so after this we only have one more line of code:
SteppingUp_SteppingDistance = 0
This line is executed after the “for” loop. It set’s the stepping distance back down to “0.0” so that the line that checks the stepping distance (“if(SteppingUp_SteppingDistance == 0.0):”) will allow the code following it to be executed.
So this function interpolates the camera’s position whenever a player takes a step, to allow smoothing of the camera’s vertical position, so that movement up stairs and steps look smooth from the player’s perspective.
There are most likely better ways of doing this, but since this is just a beginner project I decided that this way will work fine.
Here are some pictures that show the basic idea of how this works:
In this this picture, the player hits a step. It goes through “Step_Player_Up()”, finds out how high the step is, records the current vertical position of the camera, and then steps the player up.
After that, the camera is moved back down to what it was before the player moved up the step. Then (on the right side of the picture) the camera is moved back up to its default position over time, which is defined by “CamInterpo_DefaultPosition_Local_Y”. The time it takes for the camera to get to its default position is defined by the “CamInterpo_Length_Secs” setting at the top of the script.
“InterpolateCamera()” has 3 arguments: “Prev_Pos_Local_Y”, “Time_Current”, and “Time_Delta”.
“Prev_Pos_Local_Y” is the position that the camera was at before the player was moved up the step. This is set inside “Step_Player_Up()” whenever the player is moved up the step. If the player steps up another step while the camera is still being interpolated the current local position of the camera will be the new “Prev_Pos_Local_Y”.
“Time_Current” is the current time of the interpolation, in seconds. So if the length of the interpolation was “0.25” seconds, “Time_Current” will be anything between “0.0” to “0.25”.
“Time_Delta” is the delta of the frame time, in seconds. The argument specified will increment “Time_Current”. Simply put “delta” here when calling from “_process()” or “_physics_process()”. So if the delta of the frame was “0.016” and “Time_Current” was “0.05”, “Time_Current” would then be “0.066”.
Let’s say that we are going to call this function “_physics_process()”. It would look like this:
Current_Interpo_Time = InterpolateCamera(Cam_Start_Pos, Current_Interpo_Time , delta)
Note that “InterpolateCamera” returns a value, and we are putting it into a variable called “Current_Interpo_Time”. This is because the function adds “delta” to the current interpolation timer inside of the function and returns it back to the timer variable so that it can be used in the next frame.
We also have “Current_Interpo_Time” being used as an argument. This is because we need it to be able to tell what position the camera will be and to increment the timer.
The first line of code is:
if(Time_Current < CamInterpo_Length_Secs):
It checks to see if the current time of the interpolation is less than the length of the interpolation process as set in the “SETTINGS” section. Remember that “Time_Current” isn’t a global variable, but is one of the arguments specified when calling this function.
If the camera interpolation isn’t over yet...
Node_Camera3D.translation.y = lerp(Prev_Pos_Local_Y, CamInterpo_DefaultPosition_Local_Y, Time_Current / CamInterpo_Length_Secs)
We start off by changing the camera’s position by using a “lerp()” function. Check the “lerp()” chapter for an explanation.
After that:
Time_Current += Time_Delta
We simply increment the timer according to the delta specified.
return Time_Current
And we return the current time after being incremented so we can use it in the next frame loop.
Now, if the interpolation is over and the camera has reached its final position:
# Otherwise, if time is up and the camera is in it's final position...
else:
# Make sure to manually set the final Y position of the camera, just in case.
Node_Camera3D.translation.y = CamInterpo_DefaultPosition_Local_Y
I included the comments from the code for clarity.
As you can see, in the first line of code in the “else” statement we simply move the vertical position of the camera to its default position. This is done to make sure that the camera is exactly where it’s supposed to be when the interpolation is over, as sometimes the timer can be off a bit when it goes over the time limit.
An example: let’s say that the current timer is “0.225” and that the interpolation length is “0.25”. So the camera position is interpolated in the “if” statement above as being 90% of the way though interpolation. And then let’s say the delta is “0.03”, so the timer is incremented to “0.255”.
Since “0.255” is over the time limit, which is “0.25”, the “if” statement above is not executed, so that means it will go to the “else” statement. That being so, we will have to set the final position of the camera ourselves by simply stating that the position of the camera will now be the default one, “CamInterpo_DefaultPosition_Local_Y”, which is set whenever the script is loaded.
After that:
CamInterpo_DoInterpolation = false
We set the state that says to stop the interpolation.
return 0.0
And then we return 0, which then resets the counter to 0. And that’s it.
This function finds out whether the player is walking up, down, or sideways on a inclined surface and adjusts the speed accordingly.
For instance, if the player is walking up a slight incline, his speed will be slightly slower. But if the player is walking up a steep incline, his speed will be relatively much slower.
Or let’s say the player is walking down the slope. In video games one of the basic problems with player character’s is that when they walk down a slope, they tend to “jump” down the slope. Here’s an example picture:
In this case, what I did was make gravity pull the player down more when walking down a slope. This resulted in two things: the player walking down the slope faster, and the player staying on the slope.
Both these effects are realistic. Imagine you can run very fast and you are standing on the side of a steep slope. What would happen if you just started running at top speed down the slope? At the onset of your running you would jump off the ground a little bit because you are moving horizontally faster than gravity can pull you down. But then gravity catches up and starts pulling you down, and as you keep running you stay on the slope.
There are better ways of doing this, but this is a basic but effective way and is good for teaching as it’s not too complicated.
In another situation let’s say that this slope is very wide, and the player is walking along it, not going up or down. Since he is moving along the slope his velocity isn’t slowed down or sped up, as he isn’t fighting against or going with gravity.
This is how this function is set up. So now that we have an overview let’s get to the code:
if(SlideCount > 0):
First we check if there even any slides to be bothered with. If there are we move on and if there aren’t we break out of the whole function.
for Slide in range(SlideCount):
Next we go through all the slides in a loop.
if(get_slide_collision(Slide).normal.y > MaxFloorAngleNor_Y):
Now we check to see if the slide normal is a “floor” or not. If it is, we move on. If not, we go to the next slide, or if that was the last slide we break out of the function.
Slope_FloorNor2D = Vector2(get_slide_collision(Slide).normal.x, get_slide_collision(Slide).normal.z).normalized()
We get the horizontal floor normals; the X and Z axis. We get these instead of the Y axis because we want to find out what direction the slope is facing. We put the X and Z axis normals of the floor slope and put them into a 2D vector.
And, of course, we normalize the 2D vector so we can have accurate dot products.
Slope_PlayerVelVec2D =
Vector2(
(TempMoveVel_FWAndBW.x + TempMoveVel_LeftAndRight.x) ,
(TempMoveVel_FWAndBW.z + TempMoveVel_LeftAndRight.z)
).normalized()
Sorry if this is hard to read, but it’s kind of long and complicated so I formatted it to be a little easier to read.
What we have here is the horizontal velocity of the player. This is used to dot product against the slope’s normal so we can find out which direction the player is walking on the slope.
Slope_DotProduct = Slope_FloorNor2D.normalized().dot(Slope_PlayerVelVec2D)
Check the “Dot Product” section for more information on how they work.
Here we get the dot product of the normalized floor vector and the player’s velocity vector.
Check the file “slope_dot_pro.mp4” in the “documents/manual_reference_files” folder for a visualization of what we have looked at so far. (I can’t put the video file here, for some reason).
if(Slope_DotProduct > 0):
Slope_DotProduct *= Slope_EffectMultiplier_ClimbingDown
else:
Slope_DotProduct *= -Slope_EffectMultiplier_ClimbingUp
We then check to see if the player is going up or down the slope. If the dot product is more than 0 (that means he’s moving along with the normal of the slope), the dot product is multiplied by the effect multiplier for climbing down. And if the dot product is less than 0, the opposite happens, but instead of multiplying by a positive number, we are multiplying by a negative number. This is to avoid the player’s velocity from being positive, as the dot product when moving up a slope is negative and we multiply the falling speed by it, causing it to be negative. Then later when we calculate the final vertical velocity of the player we ultimately multiply the falling speed variable by a a negatived “Falling_Speed”, as shown here:
Falling_Speed = Falling_Gravity * Falling_Speed_Multiplier
FinalMoveVel.y = -Falling_Speed
So if the slope dot product variable is negative the final velocity will be inverted and the player will jump up the slope.
Moving on, “Slope_EffectMultiplier_ClimbingDown” by default is “1.5”. This means that when going down a ramp the character is pulled down harder by gravity than when going up it or walking on a flat surface. The reasons for this are outlined at the beginning of this section. The “Slope_AffectSpeed()” section, that is.
Falling_Speed_Multiplier = lerp(
Falling_Speed_Multiplier_Default ,
1.0 ,
pow(
acos( get_slide_collision(Slide).normal.y ) / acos( MaxFloorAngleNor_Y ) *
Slope_DotProduct ,
2.0
)
)
So we have a “lerp()” function. For more information on it check the last part of the “InterpolateCamera()” section above.
The first argument is the default speed multiplier. By default this is “0.25” and what this does is make the falling speed of the character 25% of gravity. Since gravity defined in this script is defined as “9.8”, that means that the default falling speed would be “2.45”.
In this script, when the player is standing still, his vertical velocity is, by default, “-2.45”. This is because whenever the gravity is more than that and the player is standing on an incline, he will slide down instead of standing still. So that’s why we have “Falling_Speed_Multiplier_Default”.
So in this function, “Falling_Speed_Multiplier_Default” is our starting point for how much gravity affects the player when walking around or moving.
As the second argument we have “1.0”. Let’s say the player hit a very steep incline that is close to the limit of what the player can walk on (according to “MaxFloorAngleNor_Y”). And let’s say that the after all the math “Falling_Speed_Multiplier” ends up bing about “0.9”. That means that whenever we get to the point of calculating the final falling speed of the character in the “_physics_process() > VERTICAL MOVEMENT > FALLING” section, the character's vertical velocity would be “-8.82”. And this only happens when the player is moving.
And the final argument is the weight of the linear interpolation. Let’s take a look and then break it all down:
pow(
acos( get_slide_collision(Slide).normal.y ) / acos( MaxFloorAngleNor_Y ) *
Slope_DotProduct ,
2.0
)
And the first part we are going to look at is this:
acos( get_slide_collision(Slide).normal.y ) / acos( MaxFloorAngleNor_Y )
So the first thing this “lerp()” line does is get the angle, in radians, of the floor normal and divide it by the angle, in radians, of the maximum floor angle the player can walk on. Here an example:
acos( get_slide_collision(Slide).normal.y ) = 0.436317612 (which is about 25 degrees)
acos( MaxFloorAngleNor_Y ) = 0.700000291 (which is about 40 degrees)
0.436317612 / 0.700000291 = 0.623310615
So what this does is find out, in percentage, how steep the floor is in relation to the maximum floor angle the player can walk on. It gives us about “63%”, which is basically saying that the floor is 63% of the way to the floor angle limit.
Continuing from our example, the next part is this:
Slope_DotProduct = 0.25
0.623310615 * 0.25 = 0.155827654
“Slope_DotProduct” was already calculated in the last few lines of code, so now we multiply it against our ratio. This will result in the amount that our character will be affected by gravity.
But before we let this be the final amount to affect the player’s gravity, let’s make the gravity falloff smoother, as right now it’s linear. So what we do is use “pow()” on our result like this:
pow( 0.155827654 , 2.0 ) = 0.024282258
Check the “Math Terms and Functions Explained > pow()” section for how it works and what it can be used for.
We now have the final weight we want for our “lerp()” function, and now we can get our final result:
Falling_Speed_Multiplier_Default = 0.25
Falling_Speed_Multiplier = lerp ( 0.25 , 1.0 , 0.024282258 ) = ~0.268212
So our final falling speed multiplier equals “0.268212”. That means that when the final movement velocity is calculated, the vertical velocity will be “-2.6284776”.
The reason this is so low is because of two reasons:
- In our example the dot product was multiplied by “Slope_EffectMultiplier_ClimbingUp”, which is “0.25” by default. This is because we don’t want the character to walk too slowly up the slope. If you want to change how slowly the character walks up the slope, then modify “Slope_EffectMultiplier_ClimbingUp”.
- The “lerp()” weight argument was squared, so that we could have a smoother falloff.
So after everything is done “Falling_Speed” will be used as the final vertical velocity, but will be negated at the end of the “_physics_process() > VERTICAL MOVEMENT > FALLING” section, shown here:
# Apply final falling velocity.
FinalMoveVel.y = -Falling_Speed
And then “FinalMoveVel” is used when calling “move_and_slide()”.
That’s it.
This function allows the player to activate an object with a “Touched_Function()" method in the script attached to it when touched.
It creates a list of objects that the player has touched, and when the player is no longer touching the object, gets rid of whatever objects are no longer being touched from the list and allows the object’s “Touched_Function()” to once again be used.
The first line:
if(State_OnFloor or State_OnWalls or State_OnCeiling):
It checks to see if the player is touching anything at all. If the player is in the air then there’s no point in wasting computational time with this function.
If the player is on a wall, floor, or ceiling…
if(SlideCount > 0):
This is here just as a safeguard. If the player is on a ceiling, wall, or floor, then of course there’s a slide collision. But just in case there isn’t, we have this here.
for Slide in range(SlideCount):
We start going through the slides.
if(get_slide_collision(Slide).collider.has_method("Touched_Function")):
The first thing we do is check if the collider of the slide collision has a “Touched_Function()” method in a script attached to it. If it does:
if(not Touch_ObjectsTouched.has(get_slide_collision(Slide).collider)):
We then check to see if the collider is already inside the array that holds the list of colliders with said function that have already been touched. If is has already been touched, nothing happens. If it hasn’t:
Touch_ObjectsTouched[Slide] = get_slide_collision(Slide).collider
We add the collider to the list of objects that have been touched.
Touch_ObjectsTouched[Slide].Touched_Function()
And then we activate the function in question.
After that for loop is finished, we go on to these lines:
SlideCollisions.clear()
SlideCollisions.resize(MaxSlides)
This clears the list that holds the current slide collisions of the player, and then resizes it to the number of maximum slides the player can have at any one time. It’s resized because “clear()” sets the size back down to zero.
for Index in range(SlideCollisions.size()):
Now we have a new for loop, that goes through the “SlideCollision” array, adding all the current slide collisions of the player to it, so we can compare them to the “Touch_ObjectsTouched” list to see if we are still touching any of those objects.
if(Index < SlideCount):
So this checks to see if the current iteration of the for loop is less than the amount of current slide collisions, so that we don’t try to access a slide collision that doesn’t exist, causing a segmentation fault.
If it is less then the slide count:
SlideCollisions[Index] = get_slide_collision(Index).collider
This then adds the current slide to the slide collision array.
On the other hand, if the current loop iteration is more than the number of slides:
else:
# Set the current element to null.
SlideCollisions[Index] = null
We simply set the current element of the slide collision array to “null”. Simple.
After that we start a new loop:
for Index in range(Touch_ObjectsTouched.size()):
if(Touch_ObjectsTouched[Index] != null):
I’ve concatenated these two lines in this example for expediency.
The first line starts a new for loop which goes through each element of the “Touch_ObjectsTouched” array, checking to see if any currently touched objects are still being touched.
The second line checks to see if the current element of the array is empty, so as to not try to accessing something that isn’t there.
if(not SlideCollisions.has(Touch_ObjectsTouched[Index])):
Touch_ObjectsTouched[Index] = null
Then it checks to see if the “SlideCollisions” array has the current element in the “Touch_ObjectsTouched” array.
If it doesn’t, then it nullifies the current element in the “Touch_ObjectsTouched” array, to basically say that object isn’t being touched anymore and therefore can be activated against on the next collision with it.
Now that we’ve taken a look at what happens when there’s a collision slide, now we are going to look to see what happens when there isn’t a slide collision:
# Otherwise, if there aren't slide collisions...
else:
Ray_SpaceState = get_world().get_direct_space_state()
Ray_From = self.get_global_transform().origin
Ray_To = Ray_From
Ray_To.y -= ((Player_Height * 0.5) * 1.05)
Ray_Result = Ray_SpaceState.intersect_ray(Ray_From,Ray_To,[self])
As you can see, what happens if there isn’t a collision slide is that we cast a ray from the center of the player, straight down to a tiny bit below his feet, and see if there is anything there.
This is because if you jump on an object, even though the collision is registered between the player and the object, no actual slide occurs. That’s because the player isn’t actually sliding along the surface and is, instead, falling straight down onto it, and doesn’t move left or right. The slide collisions you get with “get_slide_collision(Slide)” and exactly that: the slide, not the collision. So if you were to walk 100% straight into a wall, the wall would not be registered as a slide collision, and “is_on_wall()” would return false. This is a design issue with Godot and will probably be fixed someday (written as of March 31, 2018, with Godot 3.0.2 being the current stable release).
Since we can’t get a slide collision from the player body, we need to shoot a ray cast down from the center of the player. This may miss an object that the player is touching but isn’t directly under him, but if that is the case the chances are that it will be detected within a slide collision, as the player would slide off the side because the player’s collision shape is a radius.
After we shoot the ray cast down from the center of the player:
if(Ray_Result):
if(Ray_Result.collider.has_method("Touched_Function")):
if(not Touch_ObjectsTouched.has(Ray_Result.collider)):
We first check if there is even a result from the ray cast.
Then we check if the ray cast hit an object with a “Touched_Function()” method.
If it has that function, then we check if the “Touch_ObjectsTouched” array already has that object. If it doesn’t, then we move on to:
Touch_ObjectsTouched[0] = Ray_Result.collider
Touch_ObjectsTouched[0].Touched_Function()
We add the collider to the touched object list as the first element. As no slide collisions are happening and there is only one ray cast, we can do this.
Then we activate the function of that object.
So that shows what happens if we have a result of the ray cast, but what happens if we don’t? It’s exactly the same thing that happens if the slide count is greater than 0. But I’ll explain it once more, for completion:
else:
SlideCollisions.clear()
SlideCollisions.resize(MaxSlides)
First we clear the slide collision array. Then we resize it back to what it was.
for Index in range(SlideCollisions.size()):
if(Index < SlideCount):
SlideCollisions[Index] = get_slide_collision(Index).collider
Then we setup a for loop that goes through the slide collision array. It checks to see if the current loop iteration is less than the slide count. If it is, it goes on to adding the current slide collision to the slide collision index.
else:
SlideCollisions[Index] = null
However, if the for loop iteration is more than the slide count, then what we do is simply make the slide collision array element “null”.
for Index in range(Touch_ObjectsTouched.size()):
if(Touch_ObjectsTouched[Index] != null):
if(not SlideCollisions.has(Touch_ObjectsTouched[Index])):
Touch_ObjectsTouched[Index] = null
Then we have one last for loop that goes through the “touched objects” array.
It first checks if the current element that we are looking at is null. If it is, we do nothing. If it isn’t, we go on to check if current object that we are looking at, that has been previously touched, is still being touched. If it is still being touched, we leave the object inside the “Touch_ObjectsTouched” array. But if that object is no longer being touched, we get rid of it by nullifying the element of the array that it occupies.
And there you have it, all the functions explained, in detail. Now let’s go on to the rest of the script.