Finally, we go beyond recreating (cut-down version of) Phaser's arcade physics engine, and tackle the problem of partial tiles. The algorithm did end up dealing adequately with the Donkey Kong ramp stage (covered in the following) post.
There are known limitations, given the simplicity of its implementation. There are 2 ways to overcome them, and I comment on these through the post as well.
build more comprehensive collision checks
design the platform so that you do not encounter such limitations
As this is just my little project to experiment with specific platform mechanics, I have left further development to future posts.
Creating the collision detection routine for partial tiles
Preparing the experiment material
As our goal for this project is to recreate the "ramp stage" original arcade donkey kong (at least the climbing bit) game, we will use 8x8 tiles. The donkey kong tile set were "open" only on either the top side or the bottom side, but we will experiment with tiles that are open on both top and bottom. They are however, always "filled" across 8 pixels horizontally (although there is no reason why the logic I am about to describe cannot be adapted for any AABB type of partially filled tiles).
The tile in the top left hand corner is a "full" 8x8 tile. The tile to the right is 7 pixels tall, with 1 "open" row at the top. The tile on the second row, on the left hand side is also 7 pixels tall but with the bottom row, "open". I have created a variety of tiles.
Plan of attack
Collision detection
The plan is to carry out the collision detection in 2 stages:
stage 1: get all the tiles overlapping with the colliding edge, as in the previous examples
stage 2: cycle through all the tiles obtained in stage 1, and check whether the colliding edge is colliding with the part of the tile that is "filled"
Collision resolution
Up till now, collision resolution was based on the assumption that a tile is "whole". In otherwords, in order to calculate the degree of overlap (ie the amount needed to move the physics body to separate from the tile), the body's colliding edge was compared against the "boundary" of the tile, which was easily obtained from the tile properties, tile.pixelX, tile.pixelY, tile.right, tile.bottom. With partial tiles, what we need is the "actual" boundaries of the filled in part of the tile. We can build this into the custom properties of the tiles in TILED. So that we can utilise the code developed so far, I wanted to keep referring to the same named properties like tile.pixelX, tile.pixelY and so on, but for those properties to contain the "edges" of the actual filled in part of the partial tiles. For this, I created an object called CozyTile which houses just the tile properties that's needed for partial tile collision detection.
CozyTile object
In order to create the CozyTile object, we need to embed some information which tells us which part of the tile is filled in. I did this by including custom properties which tells how big the "gap" is at the top and bottom of each tile. For example, looking at the highlighted tile of the tileset below, the "filled in" part of the tile is 2 pixels tall, with a 1 row gap at the top and 5 rows gap at the bottom. Hence I create custom properties top and bottom respectively set to 1 and 5. The Phaser 3 tile object for this tile will give us pixelX, pixelY, right and bottom, and from these, the equivalent "pixelY" and "bottom" of the filled in part can be calculated easily (pixelX and right would be the same as the Phaser 3 tile object's properties since they the tiles below are all filled in across the entire 8 pixels horizontally).
Implementing the above logic is easy. The CozyTile object is as follows. It is just an object with the properties of the tile needed for collision detection, and there is a method called copy, that copies just those properties from the original tile, except the pixelY and bottom properties which are adjusted by the custom properties described above.
class CozyTile {
constructor() {
this.index;
this.canCollide = false;
this.collideDown = false;
this.collideUp = false;
this.collideRight = false;
this.collideLeft = false;
this.pixelX;
this.pixelY;
this.right;
this.bottom;
}
copy(tile) {
this.index = tile.index;
this.pixelX = tile.pixelX;
this.pixelY = tile.pixelY + tile.properties.top;
this.right = tile.right;
this.bottom = tile.bottom - tile.properties.bottom;
this.canCollide = tile.canCollide;
this.collideDown = tile.collideDown;
this.collideUp = tile.collideUp;
this.collideRight = tile.collideRight;
this.collideLeft = tile.collideLeft;
return this;
}
}
Returning just those tiles that are actually colliding
This is the most complicated part.
Using getTilesOnLine method we can get the tiles that overlap. However, in the below situation, if the player is moving to the left or downward, the sprite would be overlapping with 2 "non-blank" tiles on the body's bottom edge or the left edge, but it is in fact not colliding.
Once we obtain the overlapping "non-blank" tiles, we need to filter only those tiles which are truly overlapping.
The getTilesOnLine method converts the ends of the line to tile coordinates and calls getTilesWithin method. What I did was to create an amended version of getTilesWithin method that checks for "actual intersection" and returns only those intersecting tiles to the getTilesonLine method. Since the "filled in" part of the partial tiles are rectangles, I figured I could create a Phaser.Geom.Rectangle object of the filled in part, and call the Phaser.Geom.Intersects method against the rectangle vs the colliding edge (line). In the end, I got it working, but it took a lot of experimentation, to get it "pixel perfect".
getTilesWithin(xStart, yStart, xEnd, yEnd, edge) {
var layer = this.colliderLayer.layer;
var results = [];
for (var ty = yStart; ty <= yEnd; ty++)
{
for (var tx = xStart; tx <= xEnd; tx++)
{
var tile = this.colliderLayer.layer.data[ty][tx];
if (tile !== null && tile.index !== -1) {
if (this.intersectLine(tile, edge)) {
var cozyTile = new CozyTile().copy(tile);
results.push(cozyTile);
}
}
}
}
return results;
}
Limitations
If you've followed along this explanation to here, you would probably have noticed some limitations or situations that would cause problems with this implementation, biggest of which is tunnelling, in case of the player object (particularly small ones) moving at high speed and flying right through "thin" tiles.
Combined with the above, the implementation will get confused if you have "gaps" between two partial tiles stacked on top of each other.
You could build more checks to overcome such situations.
Or simply, you could design your platform to avoid such situations.
Example Code
And here is the resulting code, accessible in CodePen.
Comments