One-Way Platforms

One-Way Platforms
Since we've just finished working on the ground collision check, we
might as well add one-way platforms while we're at it. They are going
to concern only the ground collision check anyway. One-way
platforms differ from solid blocks in that they will stop an object only if
it's falling down. Additionally, we'll also allow a character to drop down
from such a platform.
First of all, when we want to drop off a one-way platform, we basically
want to ignore the collision with the ground. An easy way out here is
to set up an offset, after passing which the character or object will no
longer collide with a platform.
For example, if the character is already two pixels below the top of the
platform, it shouldn't detect a collision anymore. In that case, when we
want to drop off the platform, all we have to do is move the character
two pixels down. Let's create this offset constant.
1
public const float cOneWayPlatformThreshold = 2.0f;
Now let's add a variable which will let us know if an object is currently
on a one-way platform.
1
public bool mOnOneWayPlatform = false;
Let's modify the definition of the
HasGround
function to also take a
reference to a boolean which will be set if the object has landed on a
one-way platform.
1
public bool HasGround(Vector2 oldPosition, Vector2 position, Vector2 speed, out f
onOneWayPlatform)
Now, after we check if the tile we are currently at is an obstacle, and it
isn't, we should check if it's a one-way platform.
1
2
3
4
if (mMap.IsObstacle(tileIndexX, tileIndexY))
return true;
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY))
onOneWayPlatform = true;
As explained before, we also need to make sure that this collision is
ignored if we have fallen beyond the cOneWayPlatformThreshold below the
platform.
Of course, we cannot simply compare the difference between the top
of the tile and the sensor, because it's easy to imagine that even if
we're falling, we might go well below two pixels from the platform's top.
For the one-way platforms to stop an object, we want the sensor
distance between the top of the tile and the sensor to be less than or
equal to the
cOneWayPlatformThreshold
plus the offset from this frame's
position to the previous one.
1
2
3
4
5
if (mMap.IsObstacle(tileIndexX, tileIndexY))
return true;
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY)
&& Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatfor
position.y)
onOneWayPlatform = true;
Finally, there's one more thing to consider. When we find a one-way
platform, we cannot really exit the loop, because there are situations
when the character is partially on a platform and partially on a solid
block.
We shouldn't really consider such a position as "on a one-way
platform", because we can't really drop down from there—the solid
block is stopping us. That's why we first need to continue looking for a
solid block, and if one is found before we return the result, we also
need to set
1
2
3
4
5
6
7
8
onOneWayPlatform
to false.
if (mMap.IsObstacle(tileIndexX, tileIndexY))
{
onOneWayPlatform = false;
return true;
}
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY)
&& Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatfor
position.y)
onOneWayPlatform = true;
Now, if we went through all the tiles we needed to check horizontally
and we found a one-way platform but no solid blocks, then we can be
sure that we are on a one-way platform from which we can drop
down.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
if (mMap.IsObstacle(tileIndexX, tileIndexY))
{
onOneWayPlatform = false;
return true;
}
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY)
&& Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatfo
position.y)
onOneWayPlatform = true;
if (checkedTile.x >= bottomRight.x)
{
if (onOneWayPlatform)
return true;
break;
}
That's it, so now let's add to the character class an option to drop
down the platform. In both the stand and run states, we need to add
the following code.
1
2
3
4
5
if (KeyState(KeyInput.GoDown))
{
if (mOnOneWayPlatform)
mPosition.y -= Constants.cOneWayPlatformThreshold;
}
Let's see how it works.
Everything is working correctly.
Handle Collisions for the Ceiling
We need to create an analogous function to the HasGround for each
side of the AABB, so let's start with the ceiling. The differences are as
follows:

The sensor line is above the AABB instead of being below it

We check for the ceiling tile from bottom to top, as we're moving
up

No need to handle one-way platforms
Here's the modified function.
01
02
public bool HasCeiling(Vector2 oldPosition, Vector2 position, out float ceilingY
{
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var center = position + mAABBOffset;
var oldCenter = oldPosition + mAABBOffset;
ceilingY = 0.0f;
var oldTopRight = oldCenter + mAABB.halfSize + Vector2.up - Vector2.righ
var newTopRight = center + mAABB.halfSize + Vector2.up - Vector2.right;
var newTopLeft = new Vector2(newTopRight.x - mAABB.halfSize.x * 2.0f + 2
int endY = mMap.GetMapTileYAtPoint(newTopRight.y);
int begY = Mathf.Min(mMap.GetMapTileYAtPoint(oldTopRight.y) + 1, endY);
int dist = Mathf.Max(Mathf.Abs(endY - begY), 1);
int tileIndexX;
for (int tileIndexY = begY; tileIndexY <= endY; ++tileIndexY)
{
var topRight = Vector2.Lerp(newTopRight, oldTopRight, (float)Mat
dist);
var topLeft = new Vector2(topRight.x - mAABB.halfSize.x * 2.0f +
for (var checkedTile = topLeft; ; checkedTile.x += Map.cTileSize
{
checkedTile.x = Mathf.Min(checkedTile.x, topRight.x);
tileIndexX = mMap.GetMapTileXAtPoint(checkedTile.x);
if (mMap.IsObstacle(tileIndexX, tileIndexY))
{
ceilingY = (float)tileIndexY * Map.cTileSize - M
mMap.mPosition.y;
return true;
}
if (checkedTile.x >= topRight.x)
break;
}
}
return false;
}
Handle Collisions for the Left Wall
Similarly to how we handled the collision check for the ceiling and
ground, we also need to check if the object is colliding with the wall on
the left or the wall on the right. Let's start from the left wall. The idea
here is pretty much the same, but there are a few differences:

The sensor line is on the left side of the AABB.

The inner
for
loop needs to iterate through the tiles vertically,
because the sensor is now a vertical line.

The outer loop needs to iterate through tiles horizontally to see
if we haven't skipped a wall when moving with a big horizontal
speed.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public bool CollidesWithLeftWall(Vector2 oldPosition, Vector2 position, out floa
{
var center = position + mAABBOffset;
var oldCenter = oldPosition + mAABBOffset;
wallX = 0.0f;
var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.right;
var newBottomLeft = center - mAABB.halfSize - Vector2.right;
var newTopLeft = newBottomLeft + new Vector2(0.0f, mAABB.halfSize.y * 2.
int tileIndexY;
var endX = mMap.GetMapTileXAtPoint(newBottomLeft.x);
var begX = Mathf.Max(mMap.GetMapTileXAtPoint(oldBottomLeft.x) - 1, endX)
int dist = Mathf.Max(Mathf.Abs(endX - begX), 1);
for (int tileIndexX = begX; tileIndexX >= endX; --tileIndexX)
{
var bottomLeft = Vector2.Lerp(newBottomLeft, oldBottomLeft, (float
/ dist);
var topLeft = bottomLeft + new Vector2(0.0f, mAABB.halfSize.y *
for (var checkedTile = bottomLeft; ; checkedTile.y += Map.cTileS
{
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
checkedTile.y = Mathf.Min(checkedTile.y, topLeft.y);
tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y);
if (mMap.IsObstacle(tileIndexX, tileIndexY))
{
wallX = (float)tileIndexX * Map.cTileSize + Map.
mMap.mPosition.x;
return true;
}
if (checkedTile.y >= topLeft.y)
break;
}
}
return false;
}
Handle Collisions for the Right Wall
Finally, let's create the
CollidesWithRightWall
imagine, will do a very similar thing as
function, which as you can
CollidesWithLeftWall
, but instead
of using a sensor on the left, we'll be using a sensor on the right side
of the character.
The other difference here is that instead of checking the tiles from
right to left, we'll be checking them from left to right, since that's the
assumed moving direction.
01
02
03
04
05
06
07
08
09
public bool CollidesWithRightWall(Vector2 oldPosition, Vector2 position, out flo
{
var center = position + mAABBOffset;
var oldCenter = oldPosition + mAABBOffset;
wallX = 0.0f;
var oldBottomRight = oldCenter + new Vector2(mAABB.halfSize.x, -mAABB.ha
var newBottomRight = center + new Vector2(mAABB.halfSize.x, -mAABB.halfS
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var newTopRight = newBottomRight + new Vector2(0.0f, mAABB.halfSize.y *
var endX = mMap.GetMapTileXAtPoint(newBottomRight.x);
var begX = Mathf.Min(mMap.GetMapTileXAtPoint(oldBottomRight.x) + 1, endX
int dist = Mathf.Max(Mathf.Abs(endX - begX), 1);
int tileIndexY;
for (int tileIndexX = begX; tileIndexX <= endX; ++tileIndexX)
{
var bottomRight = Vector2.Lerp(newBottomRight, oldBottomRight, (fl
/ dist);
var topRight = bottomRight + new Vector2(0.0f, mAABB.halfSize.y
for (var checkedTile = bottomRight; ; checkedTile.y += Map.cTile
{
checkedTile.y = Mathf.Min(checkedTile.y, topRight.y);
tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y);
if (mMap.IsObstacle(tileIndexX, tileIndexY))
{
wallX = (float)tileIndexX * Map.cTileSize - Map.cTi
return true;
}
if (checkedTile.y >= topRight.y)
break;
}
}
return false;
}
Move the Object Out of the Collision
All of our collision detection functions are done, so let's use them to
complete the collision response against the tilemap. Before we do that,
though, we need to figure out the order in which we'll be checking the
collisions. Let's consider the following situations.
In both of these situations, we can see the character ended up
overlapping with a tile, but we need to figure out how should we
resolve the overlap.
The situation on the left is pretty simple—we can see that we're falling
straight down, and because of that we definitely should land on top of
the block.
The situation on the right is a bit more tricky, since in truth we could
land on the very corner of the tile, and pushing the character to the top
is as reasonable as pushing it to the right. Let's choose to prioritize
the horizontal movement. It doesn't really matter much which
alignment we wish to do first; both choices look correct in action.
Let's go to our
UpdatePhysics
function and add the variables which will
hold the results of our collision queries.
1
2
float groundY = 0.0f, ceilingY = 0.0f;
float rightWallX = 0.0f, leftWallX = 0.0f;
Now let's start by looking if we should move the object to the right.
The conditions here are that:

the horizontal speed is less or equal to zero

we collide with the left wall

in the previous frame we didn't overlap with the tile on the
horizontal axis—a situation akin to the one on the right in the
above picture
The last one is a necessary condition, because if it wasn't fulfilled then
we would be dealing with a situation similar to the one on the left in
the above picture, in which we surely shouldn't move the character to
the right.
1
2
3
4
5
if (mSpeed.x <= 0.0f
&& CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX)
&& mOldPosition.x - mAABB.halfSize.x + mAABBOffset.x >= leftWallX)
{
}
If the conditions are true, we need to align the left side of our AABB to
the right side of the tile, make sure that we stop moving to the left, and
mark that we are next to the wall on the left.
1
2
3
4
5
6
7
8
if (mSpeed.x <= 0.0f
&& CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX)
&& mOldPosition.x - mAABB.halfSize.x + mAABBOffset.x >= leftWallX)
{
mPosition.x = leftWallX + mAABB.halfSize.x - mAABBOffset.x;
mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
mPushesLeftWall = true;
}
If any of the conditions besides the last one is false, we need to
set
mPushesLeftWall
to false. That's because the last condition being
false does not necessarily tell us that the character is not pushing the
wall, but conversely, it tells us that it was colliding with it already in the
previous frame. Because of this, it's best to change
mPushesLeftWall
to
false only if any of the first two conditions is false as well.
01
02
03
if (mSpeed.x <= 0.0f
&& CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX))
{
04
05
06
07
08
09
10
11
12
if (mOldPosition.x - mAABB.HalfSizeX + AABBOffsetX >= leftWallX)
{
mPosition.x = leftWallX + mAABB.HalfSizeX - AABBOffsetX;
mPushesLeftWall = true;
}
mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
}
else
mPushesLeftWall = false;
Now let's check for the collision with the right wall.
01
02
03
04
05
06
07
08
09
10
11
12
13
if (mSpeed.x >= 0.0f
&& CollidesWithRightWall(mOldPosition, mPosition, out rightWallX))
{
if (mOldPosition.x + mAABB.HalfSizeX + AABBOffsetX <= rightWallX)
{
mPosition.x = rightWallX - mAABB.HalfSizeX - AABBOffsetX;
mPushesRightWall = true;
}
mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);
}
else
mPushesRightWall = false;
As you can see, it's the same formula we used for checking the
collision with the left wall, but mirrored.
We already have the code for checking the collision with the ground,
so after that one we need to check the collision with the ceiling.
Nothing new here as well, plus we don't need to do any additional
checks except that the vertical speed needs to be greater or equal to
zero and we actually collide with a tile that's on top of us.
1
2
3
4
5
6
7
if (mSpeed.y >= 0.0f
&& HasCeiling(mOldPosition, mPosition, out ceilingY))
{
mPosition.y = ceilingY - mAABB.halfSize.y - mAABBOffset.y - 1.0f;
mSpeed.y = 0.0f;
mAtCeiling = true;
}
8
9
else
mAtCeiling = false;
Round Up the Corners
Before we test if the collision responses work, there's one more
important thing to do, which is to round the values of the corners we
calculate for the collision checks. We need to do that, so that our
checks are not destroyed by floating point errors, which might come
about from weird map position, character scale or just a weird AABB
size.
First, for our ease, let's create a function that transforms a vector of
floats into a vector of rounded floats.
1
2
3
4
Vector2 RoundVector(Vector2 v)
{
return new Vector2(Mathf.Round(v.x), Mathf.Round(v.y));
}
Now let's use this function in every collision check. First, let's fix
the
1
2
3
4
HasCeiling
var oldTopRight = RoundVector(oldCenter + mAABB.HalfSize + Vector2.up - Vector2.r
var newTopRight = RoundVector(center + mAABB.HalfSize + Vector2.up - Vector2.righ
var newTopLeft = RoundVector(new Vector2(newTopRight.x - mAABB.HalfSizeX * 2.0f +
Next is
1
2
3
4
OnGround
.
var oldBottomLeft = RoundVector(oldCenter - mAABB.HalfSize - Vector2.up + Vector2
var newBottomLeft = RoundVector(center - mAABB.HalfSize - Vector2.up + Vector2.ri
var newBottomRight = RoundVector(new Vector2(newBottomLeft.x + mAABB.HalfSizeX *
newBottomLeft.y));
PushesRightWall
1
function.
.
var oldBottomRight = RoundVector(oldCenter + new Vector2(mAABB.HalfSizeX, -mAABB.
2
3
4
Vector2.right);
var newBottomRight = RoundVector(center + new Vector2(mAABB.HalfSizeX, -mAABB.HalfSi
var newTopRight = RoundVector(newBottomRight + new Vector2(0.0f, mAABB.HalfSizeY
And finally,
1
2
3
4
PushesLeftWall
.
var oldBottomLeft = RoundVector(oldCenter - mAABB.HalfSize - Vector2.right);
var newBottomLeft = RoundVector(center - mAABB.HalfSize - Vector2.right);
var newTopLeft = RoundVector(newBottomLeft + new Vector2(0.0f, mAABB.HalfSizeY *
That should solve our issues!
Check the Results
That's going to be it. Let's test how our collisions are working now.
Summary
That's it for this part! We've got a fully working set of tilemap collisions,
which should be very reliable. We know in what position state
the object currently is: whether it's on the ground, touching a tile on
the left or on the right, or bumping a ceiling. We've also implemented
the one-way platforms, which are a very important tool in every
platformer game.
In the next part, we'll add ledge-grabbing mechanics, which will
increase the possible movement of the character even further, so stay
tuned!