Coding the perfect slope
This is very different from the other stuff i’ve posted so far. This is a technical post on how I made 45º slopes in my game.
First of all, this post is intended for those who are coding their own platformer physics. If you’re using an engine, the hard work might have already be done for you.
What the hell is a “perfect slope” anyways?
When I finished implementing square tiles in my game I was ready to make slopes, when I remembered countless games that had wonky slope physics. I didn’t want something like that for my game.
In those games the player would do things like these:
- Slide down the slope when idle
- Not be able jump off a slope
- “Fall” off a slope when descending it
- Stand on a slope with only the corner of their feet
Would you play a game if the player couln’t jump off a slope?
I decided i would have to either drop slopes or adress all those problems. I did the latter and it turned out pretty well (after about 2 weeks).
How my collision works
My collision is done in two passes, horizontal and vertical.
First i update the player’s x position and detect horizontal collisions, then I update the player’s y and detect vertical collisions. Here’s pseudocode:
“px” and “py” are the player’s positions, and “pSpeedX” and “pSpeedY” are the player’s velocities. the parameter passed to “collisionDetect” is the pass. 0 for horizontal, 1 for vertical.
Detecting collisions with slopes (45º only)
With square tiles it’s pretty trivial, since both are box-shaped. This is called an AABB (Axis Aligned Bounding Box) collision detection and it’s done like this:
Detecting collisions with square tiles are done in both passes.
With slopes it’s a little bit different:
First of all you need to know which direction the slope is “facing”. I do that with a boolean called “slopeDirection”.
You start by checking if the player is inside the slope’s bounding box before making further checks. You do that the same way we did with square tiles. Then, we have variables “collisionX” and “yTop” (forgive my bad naming conventions).
collisionX is the horizontal position where we will be checking for collisions. If the slope is facing right (slopeDirection == 1), we know the collision will only happen with the player’s bottom-right corner. When the slope is facing left, it happens with the bottom-left corner. We set this variable based on the slope’s direction.
yTop is the slope’s topmost position where collisionX is. This image explains it better:
Then we do a second check to determine if the player is intersecting an AABB where the topmost position is yTop. Since yTop changes when collisionX changes, this AABB’s height will change too.
This is important: While collisions with square tiles are detected and resolved on both passes, collisions with slopes are detected and resolved only on the second (vertical) pass!
Resolving the collision
OK, we know when the player’s inside the slope, but how do we make him react to it?
We set the player’s topmost vertical position to yTop minus his height. This pushes the player up to the point where his feet is barely touching the slope.
Now the player reacts correctly to slopes, but we have a problem:
The “Heel/Toe Stand”
This one is particulary infuriating. Take a look:
What’s happening here is that since the player’s hitbox is rectangle-shaped, it stands on slopes with only one corner, leaving the player’s feet floating. Hitboxes shown below.
I fixed this by making a special slope-only collision “box” for the player. The word “box” is between quotes because it’s actually a line, not a box:
Now the player collides with square tiles using the green box and collides with slopes using the blue line. This ensures his feet are centered on the slope.
To achieve this we change the slope detection code to this:
Now “collisionX” is set to the center of the player’s collision box regardless of the slope’s direction, and instead of detecting intersections using the player’s box, we check intersections using the player’s line, with “collisionX”.
At this moment i thought i fixed it. Unfortunately i was wrong.
Since the player collides with slopes using the blue line, some part of the green box remains inside the ground, and upon walking up this slope, that green part inside the ground collides with the side of A, preventing the player to walk up the slope.
This is a tile-based environment, so when loading the level, i disable collision with the left side of all the tiles that have a slope to it’s left, and disable collision with the right side of all that have a slope to it’s right.
Other problems arised.
“Falling” off a slope
I thought everything was fixed and working, until i tried descending a slope. This happened:
Instead of smoothly descending the slope, i fell off it.
The fix was rather complicated, but worth it.
The player has a boolean “onGround”, determining if he is “grounded” and able to jump. This is set everytime the player collides with something on the vertical pass and his vertical speed is greater than zero (meaning he’s falling).
The slopes were only detected on the vertical pass, but I changed that and made the slope’s horizontal pass set a boolean called “descendingSlope” if the player is “grounded” and colliding with an enlarged version of it’s bounding box:
Notice I set “descendingSlope” to true if
- It’s the horizontal pass
- The player is “grounded”
- The player is colliding with an enlarged version of it’s bounding box
What does this boolean do then?
On the update loop that runs every frame, if this boolean is set and if the player’s vertical speed is greater or equal to zero (means he’s falling), the player’s vertical speed will be set to same value as the horizontal, meaning the player will move downwards and horizontally with the same speed, making him move in a 45º angle and “stick” to the slope.
This variable needs to be reset to ‘false’ every frame after checking it!
Why do we check if the player is falling before “sticking” him to the slope?
If we don’t perform this check, the player will stick to the slope even when he’s going up, meaning he can’t jump off the slope.
Also, have you noticed that “descendingSlope” is only set to true in the detection code when the player is “grounded”? This is because we don’t want the player sticking to the slope before he even touches it.
Wait, what? More problems?
The “Ultimate Heel/Toe Stand”
You thought we already squished this bug. You were wrong.
The blue line should be touching the slope, but it isn’t, because the player is heel/toe standing on the square tile on the side of the slope.
Fix: Disable collision with all square tiles if the player is inside a slope’s bounding box.
If the player is inside a slope’s bounding box, we set a (yet another) boolean called “onSlope”.
This boolean is reset to 'false’ every frame along with “descendingSlope” and “onGround”.
Now that we have this boolean, we can disable collision with square tiles if it’s set. Slopes and squares must be checked separately though: all slopes must be checked, then all the squares must be checked, but only if onSlope == false.
Now for the final touch:
Resultant speed on slopes
If slope collision resolving only changes the player’s y position and speed, it means the horizontal speed stays intact. And if he’s going up or down at the same time his horizontal speed stays the same, it means his resultant speed is greater when on slopes than when on ground.
There’s an easy fix on the update loop though:
Before “maxSpeed” is changed, it represents the maximum horizontal speed he can achieve on a flat ground. If the player is on a slope and he’s grounded, we change that so that he doesn’t go faster on a slope than on ground.
We basically want the maximum resultant speed when on slopes (diagonal) to be equal to “maxSpeed”. But since “maxSpeed” is only horizontal, we need to find the horizontal component of this diagonal vector.
We need to find 'x’.
x*x + x*x = maxSpeed*maxSpeed
(x*x)2 = maxSpeed*maxSpeed
x*x = (maxSpeed*maxSpeed)/2.0
x = sqrt((maxSpeed*maxSpeed)/2.0)
x = maxSpeed/sqrt(2.0)
x = maxSpeed*(1.0/sqrt(2.0))
Now the player runs on slopes with the same speed as when on ground.
This took about 2 hours to type, so reblogs are appreciated. I hope i helped someone with this huge post.
Also, would you like more technical posts like this, or only gameplay/progress posts from now on?