Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

Basic 2D Platformer Physics, Part 8: Slopes

Scroll to top
Read Time: 44 min
Final product imageFinal product imageFinal product image
What You'll Be Creating

Demo

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.

1
switch (tileCollisionType)
2
{
3
    default://slope       
4
        break;
5
    case TileCollisionType.Empty:
6
        break;
7
    case TileCollisionType.Full:
8
        state.onOneWay = false;
9
        state.pushesBottomTile = true;
10
        state.bottomTile = new Vector2i(x, bottomleftTile.y);
11
        return true;
12
}

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.

1
Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y);
2
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType);

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

1
Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y);
2
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType);
3
sf.freeUp -= 1;
4
sf.collidingBottom -= 1;

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.

1
Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y);
2
SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType);
3
sf.freeUp -= 1;
4
sf.collidingBottom -= 1;
5
6
if (sf.freeUp >= 0)
7
{
8
}

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.

1
public const int cSlopeWallHeight = 4;

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.

1
if (sf.freeUp >= 0 || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight))
2
{
3
}

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.

1
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
2
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f));
3
int collidingBottom = int.MinValue;
4
int slopeX = -1;

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.

1
if ((sf.freeUp >= 0 || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight))
2
    && sf.collidingBottom >= collidingBottom)
3
{
4
    collidingBottom = sf.collidingBottom;
5
    slopeX = x;
6
} 

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

1
if (slopeX != -1)
2
{
3
    state.pushesBottomTile = true;
4
    state.bottomTile = new Vector2i(slopeX, bottomleftTile.y);
5
    position.y += collidingBottom;
6
    topRight.y += collidingBottom;
7
    bottomLeft.y += collidingBottom;
8
    return true;
9
}
10
11
return false;

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.

1
public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f));
4
    Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
5
    int freeDown = int.MaxValue;
6
    int slopeX = -1;
7
8
    for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
9
    {
10
        var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y);
11
12
        if (Slopes.IsOneWay(tileCollisionType))
13
            continue;
14
15
        switch (tileCollisionType)
16
        {
17
            default://slope

18
19
                Vector2 tileCenter = mMap.GetMapTilePosition(x, topRightTile.y);
20
                SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y + 0.5f, tileCollisionType);
21
                sf.freeDown += 1;
22
                sf.collidingTop += 1;
23
24
                if (sf.freeDown < freeDown && sf.freeDown <= 0 && sf.freeDown == sf.collidingTop)
25
                {
26
                    freeDown = sf.freeDown;
27
                    slopeX = x;
28
                }
29
30
                break;
31
            case TileCollisionType.Empty:
32
                break;
33
            case TileCollisionType.Full:
34
                state.pushesTopTile = true;
35
                state.topTile = new Vector2i(x, topRightTile.y);
36
                return true;
37
        }
38
    }
39
40
    if (slopeX != -1)
41
    {
42
        state.pushesTopTile = true;
43
        state.topTile = new Vector2i(slopeX, topRightTile.y);
44
        position.y += freeDown;
45
        topRight.y += freeDown;
46
        bottomLeft.y += freeDown;
47
        return true;
48
    }
49
50
    return false;
51
}

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 slopesDifferent shaped slopesDifferent 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.

1
default://slope
2
3
    Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y);
4
    float leftTileEdge = (tileCenter.x - Map.cTileSize / 2);
5
    float rightTileEdge = (leftTileEdge + Map.cTileSize);
6
    float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2);

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. 

1
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
2
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;

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.

1
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
2
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;
3
4
if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile))
5
{
6
    state.pushesRightTile = true;
7
    state.rightTile = new Vector2i(topRightTile.x, y);
8
    return true;
9
}

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.

1
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f));
2
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
3
float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
4
TileCollisionType slopeCollisionType = TileCollisionType.Empty;

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.

1
var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
2
slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;
3
4
if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile))
5
{
6
    state.pushesRightTile = true;
7
    state.rightTile = new Vector2i(topRightTile.x, y);
8
    return true;
9
}
10
else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset))
11
{
12
    slopeCollisionType = tileCollisionType;
13
    state.rightTile = new Vector2i(topRightTile.x, y);
14
}
15
else
16
    slopeOffset = oldSlopeOffset;

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.

1
if (slopeOffset != 0.0f)
2
{
3
}

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.

1
if (slopeOffset != 0.0f)
2
{
3
    if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
4
    {
5
    }
6
}

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.

1
if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
2
{
3
    Vector2 pos = position, tr = topRight, bl = bottomLeft;
4
    pos.y += slopeOffset - Mathf.Sign(slopeOffset);
5
    tr.y += slopeOffset - Mathf.Sign(slopeOffset);
6
    bl.y += slopeOffset - Mathf.Sign(slopeOffset);
7
    PositionState s = new PositionState();
8
9
    if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
10
    {
11
        state.pushesRightTile = true;
12
        state.pushesRightSlope = true;
13
        return true;
14
    }
15
}

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

1
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
2
{
3
    state.pushesRightTile = true;
4
    state.pushesRightSlope = true;
5
    return true;
6
}
7
else
8
{
9
    position.y += slopeOffset;
10
    bottomLeft.y += slopeOffset;
11
    topRight.y += slopeOffset;
12
    state.pushesBottomTile = true;
13
    state.pushesBottomSlope = true;
14
}

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

1
if (slopeOffset != 0.0f)
2
{
3
    if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
4
    {
5
        Vector2 pos = position, tr = topRight, bl = bottomLeft;
6
        pos.y += slopeOffset - Mathf.Sign(slopeOffset);
7
        tr.y += slopeOffset - Mathf.Sign(slopeOffset);
8
        bl.y += slopeOffset - Mathf.Sign(slopeOffset);
9
        PositionState s = new PositionState();
10
11
        if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
12
        {
13
            state.pushesRightTile = true;
14
            state.pushesRightSlope = true;
15
            return true;
16
        }
17
        else
18
        {
19
            position.y += slopeOffset;
20
            bottomLeft.y += slopeOffset;
21
            topRight.y += slopeOffset;
22
            state.pushesBottomTile = true;
23
            state.pushesBottomSlope = true;
24
        }
25
    }
26
    else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight)
27
    {
28
        Vector2 pos = position, tr = topRight, bl = bottomLeft;
29
        pos.y += slopeOffset - Mathf.Sign(slopeOffset);
30
        tr.y += slopeOffset - Mathf.Sign(slopeOffset);
31
        bl.y += slopeOffset - Mathf.Sign(slopeOffset);
32
        PositionState s = new PositionState();
33
34
        if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
35
        {
36
            state.pushesRightTile = true;
37
            state.pushesRightSlope = true;
38
            return true;
39
        }
40
        else
41
        {
42
            position.y += slopeOffset;
43
            bottomLeft.y += slopeOffset;
44
            topRight.y += slopeOffset;
45
            state.pushesTopTile = true;
46
            state.pushesTopSlope = true;
47
        }
48
    }
49
}

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.

1
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
2
{
3
}

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

1
if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
2
{
3
    state.pushesRightTile = true;
4
    state.pushesRightSlope = true;
5
    return true;
6
}
7
else if (move)
8
{
9
    position.y += slopeOffset;
10
    bottomLeft.y += slopeOffset;
11
    topRight.y += slopeOffset;
12
    state.pushesBottomTile = true;
13
    state.pushesBottomSlope = true;
14
}
15
16
//...

17
18
if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
19
{
20
    state.pushesRightTile = true;
21
    state.pushesRightSlope = true;
22
    return true;
23
}
24
else if (move)
25
{
26
    position.y += slopeOffset;
27
    bottomLeft.y += slopeOffset;
28
    topRight.y += slopeOffset;
29
    state.pushesTopTile = true;
30
    state.pushesTopSlope = true;
31
}
32
33
34

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.

1
if ((sf.freeUp >= 0 && sf.collidingBottom == sf.freeUp)
2
    || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight && sf.freeUp >= sf.collidingBottom))
3
{
4
    state.onOneWay = isOneWay;
5
    state.oneWayY = bottomleftTile.y;
6
    state.pushesBottomTile = true;
7
    state.bottomTile = new Vector2i(x, bottomleftTile.y);
8
    position.y += sf.collidingBottom;
9
    topRight.y += sf.collidingBottom;
10
    bottomLeft.y += sf.collidingBottom;
11
    return true;
12
}

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 itSlope with three squares marked on itSlope 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.

1
if (mSticksToSlope && state.pushedBottomTile && move)
2
{
3
}

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.

1
var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f);
2
var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1;
3
4
var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y));
5
var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY));
6
7
var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y));
8
var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));

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.

1
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type)
2
{
3
    switch (type)
4
    {
5
        case TileCollisionType.Empty:
6
            return 0;
7
        case TileCollisionType.Full:
8
        case TileCollisionType.OneWayPlatform:
9
            return Map.cTileSize;
10
    }
11
}

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.

1
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type)
2
{
3
    switch (type)
4
    {
5
        case TileCollisionType.Empty:
6
            return 0;
7
        case TileCollisionType.Full:
8
        case TileCollisionType.OneWayPlatform:
9
            return Map.cTileSize;
10
    }
11
    
12
    var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]);
13
    return offset.collidingBottom;
14
}

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

1
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type)
2
{
3
    switch (type)
4
    {
5
        case TileCollisionType.Empty:
6
            return 0;
7
        case TileCollisionType.Full:
8
        case TileCollisionType.OneWayPlatform:
9
            return Map.cTileSize;
10
    }
11
    
12
    if (IsFlippedX(type))
13
        x = Map.cTileSize - 1 - x;
14
            
15
    var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]);
16
    return offset.collidingBottom;
17
}

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.

1
var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]);
2
return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom;

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.

1
if (!IsFlipped90(type))
2
{
3
    var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]);
4
    return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom;
5
}
6
else
7
{
8
    var offset = new SlopeOffsetI(slopeOffsets[(int)type][0][x][Map.cTileSize - 1][0]);
9
    return IsFlippedY(type) ? offset.collidingLeft : -offset.collidingRight;
10
}

That's the final function.

1
public static int GetSlopeHeightFromBottom(int x, TileCollisionType type)
2
{
3
    switch (type)
4
    {
5
        case TileCollisionType.Empty:
6
            return 0;
7
        case TileCollisionType.Full:
8
        case TileCollisionType.OneWayPlatform:
9
            return Map.cTileSize;
10
    }
11
12
    if (IsFlippedX(type))
13
        x = Map.cTileSize - 1 - x;
14
15
    if (!IsFlipped90(type))
16
    {
17
        var offset = new SlopeOffsetI(slopeOffsets[(int)type][x][0][0][Map.cTileSize - 1]);
18
        return IsFlippedY(type) ? -offset.collidingTop : offset.collidingBottom;
19
    }
20
    else
21
    {
22
        var offset = new SlopeOffsetI(slopeOffsets[(int)type][0][x][Map.cTileSize - 1][0]);
23
        return IsFlippedY(type) ? offset.collidingLeft : -offset.collidingRight;
24
    }
25
}

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.

1
var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y));
2
var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));
3
4
int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
5
int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);

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

1
int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType);
2
int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType);
3
4
var offset = slopeHeight + Map.cTileSize - nextSlopeHeight;

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.

1
if (offset < Constants.cSlopeWallHeight && offset > 0)
2
{
3
    Vector2 pos = position, tr = topRight, bl = bottomLeft;
4
    pos.y -= offset - Mathf.Sign(offset);
5
    tr.y -= offset - Mathf.Sign(offset);
6
    bl.y -= offset - Mathf.Sign(offset);
7
    bl.x += 1.0f;
8
    tr.x += 1.0f;
9
    PositionState s = new PositionState();
10
11
    if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
12
    {
13
        position.y -= offset;
14
        bottomLeft.y -= offset;
15
        topRight.y -= offset;
16
        state.pushesBottomTile = true;
17
        state.pushesBottomSlope = true;
18
    }
19
}

All in all, the function should look like this.

1
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f));
4
    Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
5
    float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
6
    TileCollisionType slopeCollisionType = TileCollisionType.Empty;
7
8
    for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
9
    {
10
        var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y);
11
12
        switch (tileCollisionType)
13
        {
14
            default://slope

15
16
                Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y);
17
                float leftTileEdge = (tileCenter.x - Map.cTileSize / 2);
18
                float rightTileEdge = (leftTileEdge + Map.cTileSize);
19
                float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2);
20
21
                oldSlopeOffset = slopeOffset;
22
23
                var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
24
                slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;
25
26
                if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile))
27
                {
28
                    state.pushesRightTile = true;
29
                    state.rightTile = new Vector2i(topRightTile.x, y);
30
                    return true;
31
                }
32
                else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset))
33
                {
34
                    slopeCollisionType = tileCollisionType;
35
                    state.rightTile = new Vector2i(topRightTile.x, y);
36
                }
37
                else
38
                    slopeOffset = oldSlopeOffset;
39
40
                break;
41
            case TileCollisionType.Empty:
42
                break;
43
        }
44
    }
45
46
    if (slopeOffset != 0.0f)
47
    {
48
        if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
49
        {
50
            Vector2 pos = position, tr = topRight, bl = bottomLeft;
51
            pos.y += slopeOffset - Mathf.Sign(slopeOffset);
52
            tr.y += slopeOffset - Mathf.Sign(slopeOffset);
53
            bl.y += slopeOffset - Mathf.Sign(slopeOffset);
54
            PositionState s = new PositionState();
55
56
            if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
57
            {
58
                state.pushesRightTile = true;
59
                state.pushesRightSlope = true;
60
                return true;
61
            }
62
            else if (move)
63
            {
64
                position.y += slopeOffset;
65
                bottomLeft.y += slopeOffset;
66
                topRight.y += slopeOffset;
67
                state.pushesBottomTile = true;
68
                state.pushesBottomSlope = true;
69
            }
70
        }
71
        else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight)
72
        {
73
            Vector2 pos = position, tr = topRight, bl = bottomLeft;
74
            pos.y += slopeOffset - Mathf.Sign(slopeOffset);
75
            tr.y += slopeOffset - Mathf.Sign(slopeOffset);
76
            bl.y += slopeOffset - Mathf.Sign(slopeOffset);
77
            PositionState s = new PositionState();
78
79
            if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
80
            {
81
                state.pushesRightTile = true;
82
                state.pushesRightSlope = true;
83
                return true;
84
            }
85
            else if (move)
86
            {
87
                position.y += slopeOffset;
88
                bottomLeft.y += slopeOffset;
89
                topRight.y += slopeOffset;
90
                state.pushesTopTile = true;
91
                state.pushesTopSlope = true;
92
            }
93
        }
94
    }
95
96
    if (mSticksToSlope && state.pushedBottomTile && move)
97
    {
98
        var nextX = mMap.GetMapTileXAtPoint(bottomLeft.x + 1.0f);
99
        var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1;
100
101
        var prevPos = mMap.GetMapTilePosition(new Vector2i(bottomLeftTile.x, bottomLeftTile.y));
102
        var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY));
103
104
        var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y));
105
        var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));
106
107
        int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
108
        int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
109
110
        int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType);
111
        int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType);
112
113
        var offset = slopeHeight + Map.cTileSize - nextSlopeHeight;
114
115
        if (offset < Constants.cSlopeWallHeight && offset > 0)
116
        {
117
            Vector2 pos = position, tr = topRight, bl = bottomLeft;
118
            pos.y -= offset - Mathf.Sign(offset);
119
            tr.y -= offset - Mathf.Sign(offset);
120
            bl.y -= offset - Mathf.Sign(offset);
121
            bl.x += 1.0f;
122
            tr.x += 1.0f;
123
            PositionState s = new PositionState();
124
125
            if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
126
            {
127
                position.y -= offset;
128
                bottomLeft.y -= offset;
129
                topRight.y -= offset;
130
                state.pushesBottomTile = true;
131
                state.pushesBottomSlope = true;
132
            }
133
        }
134
    }
135
136
    return false;
137
}

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

1
public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
4
    Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f));
5
    float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
6
    TileCollisionType slopeCollisionType = TileCollisionType.Empty;
7
8
    for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
9
    {
10
        var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y);
11
12
        switch (tileCollisionType)
13
        {
14
            default://slope

15
16
                Vector2 tileCenter = mMap.GetMapTilePosition(bottomLeftTile.x, y);
17
                float leftTileEdge = (tileCenter.x - Map.cTileSize / 2);
18
                float rightTileEdge = (leftTileEdge + Map.cTileSize);
19
                float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2);
20
21
                oldSlopeOffset = slopeOffset;
22
23
                var offset = Slopes.GetOffsetHeight(tileCenter, bottomLeft.x - 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
24
                slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;
25
26
                if (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile))
27
                {
28
                    state.pushesLeftTile = true;
29
                    state.leftTile = new Vector2i(bottomLeftTile.x, y);
30
                    return true;
31
                }
32
                else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset))
33
                {
34
                    slopeCollisionType = tileCollisionType;
35
                    state.leftTile = new Vector2i(bottomLeftTile.x, y);
36
                }
37
                else
38
                    slopeOffset = oldSlopeOffset;
39
40
41
                break;
42
            case TileCollisionType.Empty:
43
                break;
44
        }
45
    }
46
47
    if (slopeCollisionType != TileCollisionType.Empty && slopeOffset != 0)
48
    {
49
        if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
50
        {
51
            Vector2 pos = position, tr = topRight, bl = bottomLeft;
52
            pos.y += slopeOffset - Mathf.Sign(slopeOffset);
53
            tr.y += slopeOffset - Mathf.Sign(slopeOffset);
54
            bl.y += slopeOffset - Mathf.Sign(slopeOffset);
55
            PositionState s = new PositionState();
56
57
            if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
58
            {
59
                state.pushesLeftTile = true;
60
                state.pushesLeftSlope = true;
61
                return true;
62
            }
63
            else if (move)
64
            {
65
                position.y += slopeOffset;
66
                bottomLeft.y += slopeOffset;
67
                topRight.y += slopeOffset;
68
                state.pushesBottomTile = true;
69
                state.pushesBottomSlope = true;
70
            }
71
        }
72
        else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight)
73
        {
74
            Vector2 pos = position, tr = topRight, bl = bottomLeft;
75
            pos.y += slopeOffset - Mathf.Sign(slopeOffset);
76
            tr.y += slopeOffset - Mathf.Sign(slopeOffset);
77
            bl.y += slopeOffset - Mathf.Sign(slopeOffset);
78
            PositionState s = new PositionState();
79
80
            if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
81
            {
82
                state.pushesLeftTile = true;
83
                state.pushesLeftSlope = true;
84
                return true;
85
            }
86
            else if (move)
87
            {
88
                position.y += slopeOffset;
89
                bottomLeft.y += slopeOffset;
90
                topRight.y += slopeOffset;
91
                state.pushesTopTile = true;
92
                state.pushesTopSlope = true;
93
            }
94
        }
95
    }
96
97
    if (mSticksToSlope && state.pushedBottomTile && move)
98
    {
99
        var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f);
100
        var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1;
101
102
        var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y));
103
        var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY));
104
105
        var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y));
106
        var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));
107
108
        int x1 = (int)Mathf.Clamp((topRight.x - 1.0f - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
109
        int x2 = (int)Mathf.Clamp((topRight.x - 1.5f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
110
111
        int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType);
112
        int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType);
113
114
        var offset = slopeHeight + Map.cTileSize - nextSlopeHeight;
115
116
        if (offset < Constants.cSlopeWallHeight && offset > 0)
117
        {
118
            Vector2 pos = position, tr = topRight, bl = bottomLeft;
119
            pos.y -= offset - Mathf.Sign(offset);
120
            tr.y -= offset - Mathf.Sign(offset);
121
            bl.y -= offset - Mathf.Sign(offset);
122
            bl.x -= 1.0f;
123
            tr.x -= 1.0f;
124
            PositionState s = new PositionState();
125
126
            if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
127
            {
128
                position.y -= offset;
129
                bottomLeft.y -= offset;
130
                topRight.y -= offset;
131
                state.pushesBottomTile = true;
132
                state.pushesBottomSlope = true;
133
            }
134
        }
135
    }
136
137
    return false;
138
}

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

Animation of character moving on slopeAnimation of character moving on slopeAnimation 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:

1
public enum TileCollisionType
2
{
3
    Empty = 0,              //normal tiles
4
    Full,
5
    OneWayPlatform,
6
    
7
    SlopesStart,            //starting point for slopes
8
    
9
    Slope45,                //basic version of the slope
10
    Slope45FX,              //slope flipped on the X axis
11
    Slope45FY,              //slope flipped on the Y axis
12
    Slope45FXY,             //slope flipped on the X and Y axes
13
    Slope45F90,             //slope rotated 90 degrees
14
    Slope45F90X,            //slope rotated and flipped on X axis
15
    Slope45F90Y,            //slope rotated and flipped on Y axis
16
    Slope45F90XY,           //slope rotated and flipped on both axes
17
    
18
    ...
19
}

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.

1
public static bool IsFlippedX(TileCollisionType type)
2
{
3
}

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.

1
public static bool IsFlippedX(TileCollisionType type)
2
{
3
    int typeId = (int)type - (int)TileCollisionType.SlopesStart + 1;
4
}

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

1
public static bool IsFlippedX(TileCollisionType type)
2
{
3
    int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8;
4
}

So now the translations have an assigned number for them.

1
Slope45,                //0
2
Slope45FX,              //1
3
Slope45FY,              //2
4
Slope45FXY,             //3
5
Slope45F90,             //4
6
Slope45F90X,            //5
7
Slope45F90Y,            //6
8
Slope45F90XY,           //7

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.

1
public static bool IsFlippedX(TileCollisionType type)
2
{
3
    int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8;
4
5
    switch (typeId)
6
    {
7
        case 1:
8
        case 3:
9
        case 5:
10
        case 7:
11
            return true;
12
    }
13
14
    return false;
15
}

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

1
public static bool IsFlippedY(TileCollisionType type)
2
{
3
    int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8;
4
5
    switch (typeId)
6
    {
7
        case 2:
8
        case 3:
9
        case 6:
10
        case 7:
11
            return true;
12
    }
13
14
    return false;
15
}

And finally, if the collision type is rotated.

1
public static bool IsFlipped90(TileCollisionType type)
2
{
3
    int typeId = ((int)type - ((int)TileCollisionType.SlopesStart + 1)) % 8;
4
5
    return (typeId > 3);
6
}

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.

1
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType)
2
{
3
    int posX, posY, sizeX, sizeY;
4
5
    float leftTileEdge = tileCenter.x - Map.cTileSize / 2;
6
    float rightTileEdge = leftTileEdge + Map.cTileSize;
7
    float bottomTileEdge = tileCenter.y - Map.cTileSize / 2;
8
    float topTileEdge = bottomTileEdge + Map.cTileSize;
9
    SlopeOffsetI offset;
10
11
    posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1);
12
    sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1);
13
14
    posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1);
15
    sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1);
16
17
    offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);
18
19
    if (topTileEdge < topY)
20
    {
21
        if (offset.freeDown < 0)
22
            offset.freeDown -= (int)(topY - topTileEdge);
23
        offset.collidingTop = offset.freeDown;
24
    }
25
    if (bottomTileEdge > bottomY)
26
    {
27
        if (offset.freeUp > 0)
28
            offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY);
29
        offset.collidingBottom = offset.freeUp;
30
    }
31
32
    return offset;
33
}

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.

1
if (IsFlippedX(tileCollisionType))
2
{
3
    posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1);
4
    sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1);
5
}
6
else
7
{
8
    posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1);
9
    sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1);
10
}

Similarly for the flip on the Y axis.

1
if (IsFlippedY(tileCollisionType))
2
{
3
    posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1);
4
    sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1);
5
}
6
else
7
{
8
    posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1);
9
    sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1);
10
}

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!

1
if (IsFlippedY(tileCollisionType))
2
{
3
    int tmp = offset.freeDown;
4
    offset.freeDown = -offset.freeUp;
5
    offset.freeUp = -tmp;
6
    tmp = offset.collidingTop;
7
    offset.collidingTop = -offset.collidingBottom;
8
    offset.collidingBottom = -tmp;
9
}

Now let's handle the 90-degree rotation.

1
if (!IsFlipped90(tileCollisionType))
2
{
3
    if (IsFlippedX(tileCollisionType))
4
    {
5
        posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1);
6
        sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1);
7
    }
8
    else
9
    {
10
        posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1);
11
        sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1);
12
    }
13
14
    if (IsFlippedY(tileCollisionType))
15
    {
16
        posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1);
17
        sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1);
18
    }
19
    else
20
    {
21
        posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1);
22
        sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1);
23
    }
24
25
    offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);
26
27
    if (IsFlippedY(tileCollisionType))
28
    {
29
        int tmp = offset.freeDown;
30
        offset.freeDown = -offset.freeUp;
31
        offset.freeUp = -tmp;
32
        tmp = offset.collidingTop;
33
        offset.collidingTop = -offset.collidingBottom;
34
        offset.collidingBottom = -tmp;
35
    }
36
}
37
else
38
{
39
}

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.

1
if (IsFlippedY(tileCollisionType))
2
{
3
    posX = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1);
4
    sizeX = (int)Mathf.Clamp(topY - (bottomTileEdge + posX), 0.0f, Map.cTileSize - 1);
5
}
6
else
7
{
8
    posX = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1);
9
    sizeX = (int)Mathf.Clamp((topTileEdge - posX) - bottomY, 0.0f, Map.cTileSize - 1);
10
}
11
12
if (IsFlippedX(tileCollisionType))
13
{
14
    posY = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1);
15
    sizeY = (int)Mathf.Clamp((rightTileEdge - posY) - leftX, 0.0f, Map.cTileSize - 1);
16
}
17
else
18
{
19
    posY = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1);
20
    sizeY = (int)Mathf.Clamp(rightX - (leftTileEdge + posY), 0.0f, Map.cTileSize - 1);
21
}

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.

1
if (IsFlippedY(tileCollisionType))
2
{
3
    offset.collidingBottom = offset.collidingLeft;
4
    offset.freeDown = offset.freeLeft;
5
    offset.collidingTop = offset.collidingRight;
6
    offset.freeUp = offset.freeRight;
7
}
8
else
9
{
10
    offset.collidingBottom = -offset.collidingRight;
11
    offset.freeDown = -offset.freeRight;
12
    offset.collidingTop = -offset.collidingLeft;
13
    offset.freeUp = -offset.freeLeft;
14
}

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.

1
public static SlopeOffsetI GetOffsetHeight(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType)
2
{
3
    int posX, posY, sizeX, sizeY;
4
5
    float leftTileEdge = tileCenter.x - Map.cTileSize / 2;
6
    float rightTileEdge = leftTileEdge + Map.cTileSize;
7
    float bottomTileEdge = tileCenter.y - Map.cTileSize / 2;
8
    float topTileEdge = bottomTileEdge + Map.cTileSize;
9
    SlopeOffsetI offset;
10
11
    if (!IsFlipped90(tileCollisionType))
12
    {
13
        if (IsFlippedX(tileCollisionType))
14
        {
15
            posX = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1);
16
            sizeX = (int)Mathf.Clamp((rightTileEdge - posX) - leftX, 0.0f, Map.cTileSize - 1);
17
        }
18
        else
19
        {
20
            posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1);
21
            sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1);
22
        }
23
24
        if (IsFlippedY(tileCollisionType))
25
        {
26
            posY = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1);
27
            sizeY = (int)Mathf.Clamp((topTileEdge - posY) - bottomY, 0.0f, Map.cTileSize - 1);
28
        }
29
        else
30
        {
31
            posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1);
32
            sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1);
33
        }
34
35
        offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);
36
37
        if (IsFlippedY(tileCollisionType))
38
        {
39
            int tmp = offset.freeDown;
40
            offset.freeDown = -offset.freeUp;
41
            offset.freeUp = -tmp;
42
            tmp = offset.collidingTop;
43
            offset.collidingTop = -offset.collidingBottom;
44
            offset.collidingBottom = -tmp;
45
        }
46
    }
47
    else
48
    {
49
        if (IsFlippedY(tileCollisionType))
50
        {
51
            posX = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1);
52
            sizeX = (int)Mathf.Clamp(topY - (bottomTileEdge + posX), 0.0f, Map.cTileSize - 1);
53
        }
54
        else
55
        {
56
            posX = (int)Mathf.Clamp(topTileEdge - topY, 0.0f, Map.cTileSize - 1);
57
            sizeX = (int)Mathf.Clamp((topTileEdge - posX) - bottomY, 0.0f, Map.cTileSize - 1);
58
        }
59
60
        if (IsFlippedX(tileCollisionType))
61
        {
62
            posY = (int)Mathf.Clamp(rightTileEdge - rightX, 0.0f, Map.cTileSize - 1);
63
            sizeY = (int)Mathf.Clamp((rightTileEdge - posY) - leftX, 0.0f, Map.cTileSize - 1);
64
        }
65
        else
66
        {
67
            posY = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1);
68
            sizeY = (int)Mathf.Clamp(rightX - (leftTileEdge + posY), 0.0f, Map.cTileSize - 1);
69
        }
70
71
        offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);
72
73
        if (IsFlippedY(tileCollisionType))
74
        {
75
            offset.collidingBottom = offset.collidingLeft;
76
            offset.freeDown = offset.freeLeft;
77
            offset.collidingTop = offset.collidingRight;
78
            offset.freeUp = offset.freeRight;
79
        }
80
        else
81
        {
82
            offset.collidingBottom = -offset.collidingRight;
83
            offset.freeDown = -offset.freeRight;
84
            offset.collidingTop = -offset.collidingLeft;
85
            offset.freeUp = -offset.freeLeft;
86
        }
87
    }
88
89
    if (topTileEdge < topY)
90
    {
91
        if (offset.freeDown < 0)
92
            offset.freeDown -= (int)(topY - topTileEdge);
93
        offset.collidingTop = offset.freeDown;
94
    }
95
    if (bottomTileEdge > bottomY)
96
    {
97
        if (offset.freeUp > 0)
98
            offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY);
99
        offset.collidingBottom = offset.freeUp;
100
    }
101
102
    return offset;
103
}

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

Animation of character moving on slopeAnimation of character moving on slopeAnimation 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.

1
public enum TileCollisionType
2
{
3
    Empty = 0,
4
    Full,
5
6
    SlopesStart,
7
8
    ...
9
10
    Slope45,
11
    Slope45FX,
12
    Slope45FY,
13
    Slope45FXY,
14
    Slope45F90,
15
    Slope45F90X,
16
    Slope45F90Y,
17
    Slope45F90XY,
18
19
    //...

20
    
21
    OneWayStart,
22
    
23
    OneWaySlope45,
24
    OneWaySlope45FX,
25
    OneWaySlope45FY,
26
    OneWaySlope45FXY,
27
    OneWaySlope45F90,
28
    OneWaySlope45F90X,
29
    OneWaySlope45F90Y,
30
    OneWaySlope45F90XY,
31
32
    //...

33
34
    SlopeEnd = OneWaySlopeMid4RevF90XY,
35
36
    OneWayFull,
37
38
    OneWayEnd,
39
40
    Count,
41
}

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

1
public static bool IsOneWay(TileCollisionType type)
2
{
3
    return ((int)type > (int)TileCollisionType.OneWayStart && (int)type < (int)TileCollisionType.OneWayEnd);
4
}

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.

1
case TileCollisionType.Slope45:
2
    slopesHeights[i] = slope45;
3
    slopesExtended[i] = Extend(slopesHeights[i]);
4
    posByHeightCaches[i] = CachePosByHeight(slopesHeights[i]);
5
    slopeHeightByPosAndSizeCaches[i] = CacheSlopeHeightByPosAndLength(slopesHeights[i]);
6
    slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]);
7
    break;
8
case TileCollisionType.Slope45FX:
9
case TileCollisionType.Slope45FY:
10
case TileCollisionType.Slope45FXY:
11
case TileCollisionType.Slope45F90:
12
case TileCollisionType.Slope45F90X:
13
case TileCollisionType.Slope45F90XY:
14
case TileCollisionType.Slope45F90Y:
15
case TileCollisionType.OneWaySlope45:
16
case TileCollisionType.OneWaySlope45FX:
17
case TileCollisionType.OneWaySlope45FY:
18
case TileCollisionType.OneWaySlope45FXY:
19
case TileCollisionType.OneWaySlope45F90:
20
case TileCollisionType.OneWaySlope45F90X:
21
case TileCollisionType.OneWaySlope45F90XY:
22
case TileCollisionType.OneWaySlope45F90Y:
23
    slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45];
24
    slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45];
25
    posByHeightCaches[i] = posByHeightCaches[(int)TileCollisionType.Slope45];
26
    slopeHeightByPosAndSizeCaches[i] = slopeHeightByPosAndSizeCaches[(int)TileCollisionType.Slope45];
27
    slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45];
28
    break;

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.

1
public bool mIgnoresOneWay = false;
2
public bool mOnOneWayPlatform = false;
3
public bool mSticksToSlope = true;
4
public bool mIsKinematic = false;

The second one is inside the PositionState structure.

1
public bool onOneWay;
2
public bool tmpIgnoresOneWay;

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

1
public bool onOneWay;
2
public bool tmpIgnoresOneWay;
3
public int oneWayY;

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.

1
public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
4
    Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f));
5
    bool isOneWay;
6
7
    for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
8
    {
9
        var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y);
10
11
        isOneWay = Slopes.IsOneWay(tileCollisionType);
12
13
        if ((mIgnoresOneWay || state.tmpIgnoresOneWay) && isOneWay)
14
            continue;

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.

1
if (((sf.freeUp >= 0 && sf.collidingBottom == sf.freeUp)
2
        || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight && sf.freeUp >= sf.collidingBottom))
3
    && !(isOneWay && Mathf.Abs(sf.collidingBottom) >= Constants.cSlopeWallHeight))
4
{
5
    state.onOneWay = isOneWay;
6
    state.oneWayY = bottomleftTile.y;
7
    state.pushesBottomTile = true;
8
    state.bottomTile = new Vector2i(x, bottomleftTile.y);
9
    position.y += sf.collidingBottom;
10
    topRight.y += sf.collidingBottom;
11
    bottomLeft.y += sf.collidingBottom;
12
    return true;
13
}

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

1
 public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f));
4
    Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
5
6
    for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
7
    {
8
        var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y);
9
10
        if (Slopes.IsOneWay(tileCollisionType))
11
            continue;

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.

1
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f));
4
    Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
5
    float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
6
    bool wasOneWay = false, isOneWay;
7
    TileCollisionType slopeCollisionType = TileCollisionType.Empty;

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.

1
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
2
{
3
    Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f));
4
    Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
5
    float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
6
    bool wasOneWay = false, isOneWay;
7
    TileCollisionType slopeCollisionType = TileCollisionType.Empty;
8
    
9
    for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
10
    {
11
        var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y);
12
        isOneWay = Slopes.IsOneWay(tileCollisionType);
13
14
        if (isOneWay && (!move || mIgnoresOneWay || state.tmpIgnoresOneWay || y != bottomLeftTile.y))
15
            continue;

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

1
if (!isOneWay && (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)))
2
{
3
    state.pushesRightTile = true;
4
    state.rightTile = new Vector2i(topRightTile.x, y);
5
    return true;
6
}

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.

1
if (!isOneWay && (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)))
2
{
3
    state.pushesRightTile = true;
4
    state.rightTile = new Vector2i(topRightTile.x, y);
5
    return true;
6
}
7
else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset))
8
{
9
    wasOneWay = isOneWay;
10
    slopeCollisionType = tileCollisionType;
11
    state.rightTile = new Vector2i(topRightTile.x, y);
12
}

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.

1
state.onOneWay = wasOneWay;

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.

1
else if (move.y != 0.0f && move.x == 0.0f)
2
{
3
    MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state);
4
5
    if (step.y > 0.0f)
6
        state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);
7
    else
8
        state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);
9
10
    if (!mIgnoresOneWay && state.tmpIgnoresOneWay && mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY)
11
        state.tmpIgnoresOneWay = false;
12
}

And also at the end of the third case.

1
else
2
{
3
    float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x);
4
    float vertAccum = 0.0f;
5
6
    while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f))
7
    {
8
        vertAccum += Mathf.Sign(step.y) * speedRatio;
9
10
        MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state);
11
        move.x -= step.x;
12
13
        while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f))
14
        {
15
            move.y -= step.y;
16
            vertAccum -= step.y;
17
18
            MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state);
19
        }
20
    }
21
22
    if (step.x > 0.0f)
23
        state.pushesLeftTile = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state);
24
    else
25
        state.pushesRightTile = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state);
26
27
    if (step.y > 0.0f)
28
        state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);
29
    else
30
        state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);
31
32
    if (!mIgnoresOneWay && state.tmpIgnoresOneWay && mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY)
33
        state.tmpIgnoresOneWay = false;
34
}

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.

1
if (KeyState(KeyInput.GoDown))
2
    mPS.tmpIgnoresOneWay = true;

Let's see how this looks in action.

New animation of character moving on slopeNew animation of character moving on slopeNew animation of character moving on slope

Summary

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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.