1. Game Development
  2. Platformer

Basic 2D Platformer Physics, Part 8: Slopes

Final product image
What You'll Be Creating


The demo shows the end result of the slope implementation. Use WASD to move the character. Right mouse button creates a tile. You can use the scroll wheel or the arrow keys to select a tile you want to place. The sliders change the size of the player's character.

The demo has been published under Unity 5.5.2f1, and the source code is also compatible with this version of Unity.

Before We Start...

As was true for the previous parts in the series, we'll be continuing our work where we left off in the last part. Last time we calculated and cached data needed to move the objects out of the slopes collision and changed how the collisions are checked against the tilemap. In this part we'll need the same setup from the end of last part.

You can download the project files from the previous part and write the code along with this tutorial.

In this part we'll be implementing the collision with slopes or other custom tiles, adding one-way slopes, and making it possible for the game object to travel along the slopes smoothly.

Slopes Implementation

Vertical Slope Check

We can finally get to slopes! First off, we'll try to handle when the bottom edge of the object is within a slope tile.

Let's go and take a look at our CollidesWithTileBottom function, particularly the part where we are handling the tiles.

To be able to see whether our object collides with the slope, we first need to get the offsets from the function we created earlier, which does most of our work.

Since we're checking one pixel below our character, we need to adjust the offset.

The condition for the collision is that the freeUp offset is greater or equal to 0, which means that either we move the character up or the character is standing on the slope.

We shouldn't forget about the case when we want the character to stick to the slope, though. This means that even though the character walks off the slope, we want it to behave as if it were on the slope anyway. For this, we need to add a new constant which will contain the value of how steep a slope needs to be in order to be considered a vertical wall instead of a slope.

If the offset is below this constant, it should be possible for the object to smoothly travel along the slope's curve. If it's equal or greater, it should be treated as a wall, and jumping would be needed to climb up.

Now we need to add another condition to our statement. This condition will check whether the character is supposed to be sticking to slopes, whether it was on a slope's last frame, and whether it needs to be pushed down or up by fewer pixels than our cSlopeWallHeight constant.

If the condition is true, we need to save this tile as a potential collidee with the object. We'll still need to iterate through all the other tiles along the X axis. First off, create the variables which will hold the X coordinate and the offset value for the colliding tile.

Now save the values, if the defined condition holds true. If we already found a colliding tile, we need to compare the offsets, and the final colliding tile will be the one for which the character needs to be offset the most.

Finally, after we've iterated through all the tiles and found a tile the object is colliding with, we need to offset the object.

That's pretty much it for the bottom check, so now let's do the top one. This one will be a bit simpler, as we don't even need to handle sticking.

That's it.

Horizontal Slope Check

The horizontal check will be a bit more complicated, as it is here where we'll be handling the most troublesome cases.

Let's start with handling the slopes on the right. There are a couple things that we'll need to be aware of, mostly concerning moving up the slopes. Let's consider the following situations.

Different shaped slopes

We'll need to handle those cases with special care because at some point when we move along the slope we're going to hit the ceiling. To prevent that, we'll need to do some more checks in case the character is moving horizontally.

For the vertical checks, we did move the object up from the tile, but in general we won't be using that functionality there. Since we're always checking a pixel that's just outside the object bounds, we'll never really overlap an obstacle. For the horizontal checks, it's a bit different, because this is the place where we'll be handling moving along the slope, so naturally the height adjustment will mainly take place here.

To make the proper collision response for the cases illustrated above, it'll be easier to check whether we can enter into a space horizontally, and if that's possible then check whether the object doesn't overlap with any solid pixels if it had to be moved vertically due to moving along a slope. If we fail to find the space, we know that it's impossible to move towards the checked direction, and we can set the horizontal wall flag.

Let's move to the CollidesWithTileRight function, to the part where we handle the slopes.

We get the offset in a similar way we get it for the vertical checks, but the offset we care about is the one that's bigger. 

Now, let's see if our character should treat the checked tile as a wall. We do this if either the slope offset is greater or equal to our cSlopeWallHeight constant or to get out of collision we'd need to offset the character up or down while we are already colliding with a tile in the same direction, which means that our object is squeezed between the top and bottom tiles.

If that's not the case and the offset is greater than 0, then we hit a slope. One problem here is that we do not know whether we hit a wall on other tiles that we have yet to check, so for now we'll just save the slope offset and tile collision type in case we need to use them later.

Now, instead of seeing if the slope offset is greater than zero, let's compare it to another tile's slope offset, in case we already found a colliding slope in previous iterations.

Handle the Squeezing Between Tiles

After we finish looping through all the tiles of interest, let's see if we need to move the object. Let's handle the case where the slope offset ended up being non-zero.

We'll have to handle two cases here, and we need to do slightly different things depending whether we need to offset our object up or down.

First off, we need to check whether we can fit into the space after offsetting the object. If that's the case, then we're handling one of the cases illustrated above. Where the character is trying to move right, the offset is positive, but if we offset the object then it will be pushed into the top wall, so instead we'll just mark that it's colliding with the wall on the right side to block the movement in that direction.

If we fit into the space, we'll mark that we collide with the bottom tile and offset the object's position appropriately.

We handle the case in which the object needs to be offset down in a similar manner.

Moving Object in Collision Check

Now this function will offset the object up or down as is necessary if we want to step on the tile to the right, but what if we want to use this function just as a check, and we don't really want to move the character by calling it? To solve this issue, let's add an additional variable named 'move' to mark whether the function can move the object or not.

And move the object only if this new flag is set to true.

Handle Slope Sticking

Now let's handle sticking to slopes. It's pretty straightforward, but we'll need to handle all the corner cases properly, so that the character will stick to the slope without any hiccups along the way.

Before we handle the corner cases, though, we can very easily handle slope sticking within a single tile in the vertical collision check. It will be enough if we add the following condition in the CollidesWithTileBottom function.

This condition makes it so that if the distance between the object's position and the nearest ground is between 0 and the cSlopeWallHeight, then the character will get pushed down too, in addition to the original condition. This unfortunately works only within a single tile; the following illustration pinpoints the problem which we need to solve.

Slope with three squares marked on it

The corner case we are talking about is just this: the character moves down and to the left from tile number one to tile number two. Tile number two is empty, so we need to check the tile below it and see if the offset from the character to tile number 3 is proper to keep walking along the slope there.

Handle the Corner Cases

It's going to be easier to handle these corner cases in the horizontal collision checks, so let's head back to the CollidesWithTileRight function. Let's go to the end of the function and handle the troublesome cases here. 

First off, to handle the slope sticking, the mSticksToSlope flag needs to be set, the object must have been on the ground the previous frame, and the move flag needs to be on.

Now we need to find the tile to which we should stick. Since this function checks the collision on the right edge of the object, we'll be handling the slope sticking for the character's bottom left corner.

Now we need to find a way to compare the height the object currently is on to the one it wants to step onto. If the next height is lower than the current one, but still higher than our cSlopeWallHeight constant, we'll push our object down onto the ground.

Get Slope Height

Let's go back to our Slope class to make a function which will return the height of a slope at a particular position.

The parameters for the function are the x value on the slope and the slope type. If the slope is empty we can immediately return 0, and if it's full then we return the tile size.

We can easily get the height of a slope by using our cached offsets. If the tile is not transformed in any way, we just get an offset for an object that is one pixel wide at the position x, and its height is equal to the tile size.

Let's handle this for different transforms. If a slope is flipped on the X axis, we just need to mirror the x argument.

If the slope is flipped on the Y axis, we need to return the collidingTop instead of collidingBottom offset. Since collidingTop in this case will be negative, we'll also need to flip the sign for it.

Finally, if the tile is rotated by 90 degrees, we'll need to be returning collidingLeft or collidingRight offsets. Aside from that, to get a proper cached offset, we'll need to swap the x and y positions and size.

That's the final function.

Back to Corner Cases

Let's move back to the CollidesWithTileRight function, right where we finished determining the slope types for the tiles the character moves between.

To use the function we just created, we need to determine the position at which we want to get the height of a tile.

Now let's calculate the height between those two points.

If the offset is between 0 and the cSlopeWallHeight constant, then we're going to push the object down, but first we need to check whether we actually can push the object down. This is exactly the same routine we did earlier.

All in all, the function should look like this.

Now we need to do everything analogically for the CollidesWithTileLeft function. The final version of it should take the following form.

That's it. The code should be able to handle all manners of untranslated slopes.

Animation of character moving on slope

Handle Translation Types

Before we start handling translated tiles, let's make a few functions that will return whether a particular TileCollisionType is translated in a particular way. Our collision type enum is structured in this way:

We can use these patterns to tell just by the value of the enum how is a particular collision type translated. Let's start by identifying flip on the X axis.

First, let's get the slope id. We'll do that by calculating the offset from the first defined slope tile to the one we want to identify.

We have eight kinds of translations, so now all we need is get the remainder of dividing the typeId by 8.

So now the translations have an assigned number for them.

The flip on the X axis is present in the types equal to 1, 3, 5, and 7, so if it's equal to one of those then the function should return true, otherwise return false.

In the same way, let's create a function which tells whether a type is flipped on the Y axis.

And finally, if the collision type is rotated.

That's all that we need.

Transform the Offset

Let's go back to the Slopes class and make our GetOffset function support the translated tiles.

As usual, since we don't have cached data for translated slopes, we'll be translating the object's position and size so the result is identical as if the tile has been translated. Let's start with the flip on the X axis. All we need to do here is flip the object along the center of the tile.

Similarly for the flip on the Y axis.

Now in case we flipped the tile on the y axis, the offsets we received are actually swapped. Let's translate them so they actually work the same way as the offsets of the untranslated tile, which means up is up and down is down!

Now let's handle the 90-degree rotation.

Here everything should be rotated by 90 degrees, so instead of basing our posX and sizeX on the left and right edges of the object, we'll be basing them on the top and bottom.

Now we need to do a similar thing to what we did previously if the tile was flipped on the Y axis, but this time we need to do it for both the 90-degree rotation and the Y flip.

This is it. Since our final up and down offsets are adjusted to make sense in the world space, our out of tile bounds adjustments are still working properly.

That's it—now we can use translated slopes as well. 

Animation of character moving on slope

On the animation above, there are 45, 22, 15 and 11-degree slopes. Thanks to the 90-degree rotations, we also can get 79, 75 and 68-degree slopes without defining additional slope tiles. You can also see that the 79-degree slope is too steep to move on smoothly with our value of cSlopeWallHeight.

Handle One-Way Platforms

In all this hassle, we've broken our support for one-way platforms. We need to fix that, and extend the functionality to slopes as well. One-way platforms are as important or often even more important than the solid tiles, so we can't afford to miss them.

Add the One-Way Types

The first thing we need to do is to add new collision types for one-way platforms. We'll add them past the non-one-way collision types and also mark where they start, so later on we have an easy time telling whether a particular collision type is one-way or not.

Now all one-way platforms are between the OneWayStart and OneWayEnd enums, so we can easily create a function which will return this information.

The one-way variants of slopes should point to the same data that the non-one-way platforms do, so no worries of extending memory requirements further here.

Cover the Additional Data

Now let's add variables which will allow us to make an object ignore one-way platforms. One will be an object flag, which will basically be for setting permanent ignoring of one-way platforms—this will be useful for flying monsters and other objects which do not have any need for using the platforms, and another flag to temporarily disable collision with one-way platforms, just for the sake of falling through them.

The first variable will be inside the MovingObject class.

The second one is inside the PositionState structure.

We'll also add another variable here which will hold the Y coordinate of the platform we want to skip.

To make one-way platforms work, we'll simply be ignoring a single horizontal layer of platforms. As we enter another layer, that is our character's Y position has changed in the map coordinates, then we set the character to collide with the one-way platforms again.

Modify the Collision Checks

Let's go to our CollidesWithTileBottom function. First of all, as we iterate through tiles, let's check if it's a one-way platform, and if so, whether we should even consider colliding with this tile or not.

We should collide with one-way platforms only if the distance to the top of the platform is less than the cSlopeWallHeightConstant, so we can actually come on top of it. Let's add this to the condition already laid out, and we also need to assign proper values to state.onOneWay and state.oneWayY.

For the CollidesWithTileTop function, we simply ignore one-way platforms.

For the horizontal collision check, there will be a bit more work. First off, let's create two additional booleans at the beginning, which will serve as information about whether the currently processed tile is one-way, and whether the tile from the previous iteration has been a one-way platform.

Now we're interested in iterating through a one-way platform if we're moving along it. We can't really collide with one-way platforms from right or left, but if the character moves along a slope that's also a one-way platform, then it needs to be handled in the same way that a normal slope would.

Now make sure we can't collide with a slope as if it was a wall.

And if that's not the case and the offset is small enough to climb it, then remember that we're moving along a one-way platform now.

Now what's left here is to make sure that every time we change the position state we also need to update the onOneWay variable.

Jumping Down

We need to stop ignoring the one-way platforms once we change the Y position in the map coordinates. We're going to set up our condition after the movement on the Y axis in the Move function. We need to add it at the end of the second case.

And also at the end of the third case.

That should do it. Now the only thing we need to do for a character to drop from a one-way platform is to set its tmpIgnoresOneWay to true.

Let's see how this looks in action.

New animation of character moving on slope


Whew, that was a lot of work, but it was worth it. The result is very flexible and robust. We can define any kind of slope thanks to our handling of collision bitmaps, translate the tiles, and turn them into one-way platforms. 

This implementation still isn't optimized, and I'm sure I've missed a lot of opportunities for that handed by our new one-pixel integration method. I'm also pretty sure that a lot of additional collision checks could be skipped, so if you improve this implementation then let me know in the comments section! 

Thanks for sticking with me this far, and I hope this tutorial is of use to you!

Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.