Edit: Current list of collision system posts:
Slopes!(aka Collision System Part 2C)I was recently inspired to add something new to the game's core systems: slopes! This was something I'd avoided throughout the project just to avoid all the issues that often come up when implementing slopes. However, I had an idea for a method of doing slopes that would avoid a lot of those issues.
To prevent this exploding into a mess of bugs and edge cases I came up with some sensible limitations:
- Only one gradient of slope
- No ceiling slopes, just floor
- Slopes must be capped off with flat tiles (not unlike the slopes in Mario Maker 2)
- No jump-through slopes, only solid
The basic slope case is this:

In this example all of the solid tiles, with the exception of the one on the bottom right, are mandatory. So there's no need to handle entities colliding with the slope tiles from below, or from the side.
This devlog post works as part of my series of posts about collision. I recommend looking at
Part 2A first.
It's worth noting that this system is essentially being hacked into the game, in a way that causes as little disruption as possible. It may not be the best slope solution for other games or if more complex slopes are needed.
General principleIncorrect approachFirstly let's examine the common-sense approach to adding slope collision to the game.
As mentioned in previous posts, when entities move, X movement and Y movement are handled separately. When moving, three collision checks are done, and the closest collision is used to prevent the entity from clipping into the solid collision.
When handling downwards Y movement, what if we simply allow each collision check to hit the slope?

We get this rather awkward case where one corner of the entity collides with the slope, and the entity kind of hovers above it.
This also doesn't play well with other collision checks that the game uses. For example, when enemies are walking, they perform small collision checks below their front edge, to check if they're about to walk off the edge of a platform. If they don't detect any collision, they turn around.
This slope approach would cause enemies to turn around rather than walking down the slope, because their front edge has no collision beneath it:
Correct approachThe method I actually used has a simple change: when calculating collision against the slope, rather than using the X position of each collision 'ray', always use the X position of the centre of the entity instead, shown here by a pink line:

This means that the left, central and right collision checks will all collide against the slope at the same Y position. As the entity moves left or right, the Y position at which the collision checks collide with the slope will move up or down with the slope.
This essentially turns the slope into a flat surface that moves up and down as the entity moves left/right. The entity no longer hovers awkwardly above the slope, instead the centre of the entity is grounded on the slope in a way that looks much better.
This fixes the problem with enemies turning around on slopes. When they do their collision check, they find solid ground in front of them - because as far as they're concerned, they're on a flat surface.
HorizontalHow should slopes be handled when doing the X-axis movement for entities?

We just ignore the slope altogether. This allows the entity to move left or right without being impeded by the slope. If the entity moves to the right, into the slope, then the Y-axis collision pass will push them upwards back onto the surface of the slope.
Hooking slopes into the collision check systemSo that's the general principle of how collision against slopes should work. Now, how is it integrated into the collision engine?
When doing each one of the three collision checks, we query the world to get the collision at the end point of the check. What we want is a collision rectangle to collide against. The query checks the main tile grid of the level to see what type of collision is there. If it's a solid tile, it'll return the rectangle for that tile. If it's a solid entity, it returns the rectangle for the whole entity. Nice and simple.
Here's an example showing the collision rectangles that are being used for collision checks:

So the flow is:
- Collision check queries the world
- Collision rectangle is returned from entities or tile system
- Entity collides against this rectangle
How do slopes fit into this? My first thought was the following:
- Collision check queries the world
- Collision rectangle or slope is returned from entities or tile system
- Entity collides against this rectangle or slope
However this would be a bit fiddly. If entities are essentially treating the slopes as flat surfaces, then it doesn't seem necessary for entities to even know about slopes. So the easier solution is:
- Collision check queries the world
- Collision rectangle is returned from entities or tile system (including slope tiles)
- Entity collides against this rectangle
When the collision check queries the tile grid for its collision rects, the slope tiles will also just return collision rects! The centre position of the entity is passed into the query, so the slope tile can return a collision rect based on the X position of the centre of the entity.
A quick look at this in action:

An important point is that even if two or more entities query collision for the same slope tile at the same time, it can return different collision rects for each entity, because the entities are at different X positions.
Tile collision typesThe final thing to solve is how to specify the slope collision within the tile data.
To keep it simple, only two types of slope tile exist within the tileset. Lower half of slope, and upper half of slope. Any other surrounding tiles are just the standard solid tile.

It's worth noting that, visually, there may be more tiles. For example the slopes for my test environment are made up of four tiles to make them look nice. However the bottom two of these tiles are just standard solid tiles as far as the collision system is concerned.
Basic slope definition for the main tilesThe two types of slope tile have the slope data defined as you would expect - see the solid green line in these images:


However it's also important that the slope extends out beyond the edges of the tile along the dotted green line. It's a bit tricky to get your head around, but consider the following example:

The entity is moving down to the slope. The rightmost of the three collision checks collides with the slope tile outlined in solid green. However, the centre X position of the entity is actually to the left of that tile's edge. So when calculating the height of the slope it needs to handle input X positions that extend beyond the bounds of the tile. This ensures that all collision returned by a continuous slope is at the same height (represented by the blue line) regardless of which of the slope tiles the collision check hits.
Edge casesNow the real fun begins. The following edges cases have various solutions but these are all done as an automatic process by checking where slope tiles and solid tiles neighbour each other, and marking the collision as needing special treatment. So this doesn't make the level editing any more difficult - the level editor and tileset only needs to be aware of the two main types of slope tile.
Top of slope -> flatWhen the top of a slope meets a solid tile, we run into the following problem:

The solid tile at the top is treated as a wall. Because Leilani is still on the slope, she's lower down than the solid tile, so the X-axis collision pass collides with the side of the solid tile as if it was a wall.
The solution is to mark solid tiles at the top of slopes as being another kind of slope tile. When collision checks are done against these tiles, they use the slope data as shown in this diagram:

This means the slope continues smoothly all the way up. But wait - now the slope extends too far and creates this janky situation at the top of the slope:

While it's important for the slope collision to extend beyond the bounds of the tile for the reasons mentioned previously, in the case where the slope ends and meets a flat tile, the slope needs to be capped off.


Notice the dotted green lines no longer extend above the top of the slope. If a collision check hits the slope tile, but the entity is to the right of the tile (past the top of the slope), then the collision rectangle that's returned won't go higher than the top of the slope.

Now it's all smooth!
Flat -> Bottom of slopeThe next problem is that when moving from a flat tile onto the bottom of the slope, Leilani is too low to actually collide with the slope. Her collision checks just land on the solid tile below the slope.

The solution is again to mark up the solid tile below the slope with some slope data. This way, when the collision check hits the tile below the slope, the collision rectangle that it returns correctly represents the height of the slope at that point, and Leilani is pushed upwards onto the slope.

And even more edge cases...There are a few more edge cases and they grow increasingly niche. A quick example is that enemies were turning around at the bottom of slopes:

This is again solved by adding slope data to a neighbouring tile:

I don't think it's worth going into all of these little edge cases, hopefully you get the idea that fixing these issues is just a case of preventing any discontinuities in where the floor appears to be. This all comes together to allow entities to move smoothly along slopes without really caring that they're there.
Having to solve all these edge cases by applying slope collision data to neighbouring tiles is messy, although since it's all an automated process done in code, rather than having to be set up when editing the level, I don't mind too much. It does prevent the possibility of adding slopes of different steepness, because it would just be too awkward to handle all the possible cases of flat surfaces meeting different slope gradients or different slope gradients meeting each other. If the game was programmed to handle slopes properly from the beginning these would be easier problems to solve.
One last hackJust one thing I'd like to mention quickly. If an entity is moving fast and going downhill on a slope, the inevitable problem occurs that the entity loses contact with the floor. I hacked this by detecting the entity is on a slope and simply forcing them to move downwards each frame. For example if the entity is in contact with the floor and is going downhill, and they move 4 pixels across this frame, then also move them 4 pixels downwards. This simply ensures that they will still be in contact with the floor after all collisions are done.
Dev timeI've been using toggl.com to keep track of my work hours, and it's interesting to see how much work actually goes into certain things.
For slopes, the actual dev time came out at 7h30m. I'm happy with this - it's a justifiable amount of time to spend on some feature creep that can add a lot to the game. The limitations I placed on slopes to begin with definitely helped to reduce the amount of time it took to implement.
Also this devlog post has taken 3 hours, which is hefty but I think is worthwhile.
ResultsI think even this limited implementation of slopes can add a lot to the game!
Here's a montage I made for screenshot saturday on twitter. It shows the slope collision working nicely in various cases - including as a moving platform.

And my first attempt at sneaking a couple of slopes into an existing level:

I like that it can make the non-mechanical parts of the environment feel a little more natural.
I'll be working on a new late-game level next and will make heavy use of slopes. I'm looking forward to seeing how it turns out.
Thanks for reading!