top of page
Search
cedarcantab

Pixel Perfect Tile Collision in Phaser 3, Part 0A


In my exploration of the various mechanics involved tile map based games, I have, among others, used the original Donkey Kong game (which I believe was created on tiles) as textbook material to experiment with. I have so far deliberately avoided tackling the very first stage, namely the "Ramp Stage". This is because the tile maps used are "partial tiles". In this (series of) posts, I look at how I tackle such partial tiles to achieve "Pixel Perfect Tile Collisions" in Phaser 3. I say series of posts, since my venture into partial tiles collision detection was considerably longer and more challenging than I expected at the outset.


Partial Tiles?


Donkey Kong "Ramp Stage" sloped tiles are not sloped tiles

If you look closely at how the "Ramp" stage of the original arcade Donkey Kong game is constructed, you will note that the "sloping" scaffoldings are created by combing 2 tiles on top of each other (except one tile which is a "full" piece of scaffolding - the very left one of the next sprite sheet).


Specifically, the following are 8 different tiles (individually 8x8 pixels in size) for the "top half" of the scaffolding.



which are combined with the below 8 "bottom half" tiles below.



to create the below "sloping" scaffolding.



Looking at the individual tiles, the scaffolding "fill" the tile in full horizontally but have "gaps" either at the top or bottom; in otherwords, they are all AABB of fixed 8 pixel width.


I thought it would be easy to "trick" Phaser 3's arcade physics in to detecting these partial tiles using customSeparate properties but I simply could not get it work, and had to go back to the very beginning....and to do that, I had to get an understanding how tile collisions work.


What is tile collisions all about anyway?


In very general terms, collision detection in physics engines follow the following steps:

  1. update the physics body's position

  2. detect if physic body has (or is about to) collide with a tile

  3. resolve the collision (ie "separate" the physics body from the tile so that it is flush against the tile)

  4. respond to the collision (ie manipulate the physics body's velocity to make it stop, bounce etc)

In principle, very straight forward.


In particular step (2) assuming the tiles are "filled" tiles - which are static and axis aligned rectangles - AABB) - and the physics body is also AABB, is quiet straight forward. Step (2) is also reasonably straight forward. Step (4) is also, in principle, straightforward, but as we will see later, particularly tricky in practice.


How does Phaser 3 handle Tile collisions anyway?


Phaser's arcade physics engine handles collision detection based on AABB for a lot of different type of game objects and the code is very long. I have looked at the code specific to sprite vs tile collisions to get a better understanding how physics engines are structured (this is not meant to be a proper "disassembly" of arcade physics engine - rather it is peeking at the relevant bits for me to gain a better understanding of how Phaser handles sprite to tile collisions).


1. Updating the physics body

The relevant method is update of the arcade physics body class ( think), which begins like below:


 update: function (delta)
    {
        this.prev.x = this.position.x;
        this.prev.y = this.position.y;

        if (this.moves)
        {
            this.world.updateMotion(this, delta);

            var vx = this.velocity.x;
            var vy = this.velocity.y;

            this.newVelocity.set(vx * delta, vy * delta);

            this.position.add(this.newVelocity);

this.world.updateMotion calls method computeVelocity(body, delta), which starts off with code to add gravity to velocity.



    if (body.allowGravity)
        {
            velocityX += (this.gravity.x + body.gravity.x) * delta;
            velocityY += (this.gravity.y + body.gravity.y) * delta;
        }

There's then a bunch of code to handle acceleration and drag, after which below code clamps the velocity within allowed limits, and calculates the speed and updates the relevant property.


velocityX = Clamp(velocityX, -maxX, maxX);
velocityY = Clamp(velocityY, -maxY, maxY);

        body.velocity.set(velocityX, velocityY);

        if (maxSpeed > -1 && speed > maxSpeed)
        {
            body.velocity.normalize().scale(maxSpeed);
            speed = maxSpeed;
        }

        body.speed = speed;

Returning to the update method of the physics body class, the updated velocity is multipled by delta and this.newVelocity is set to the result, and this is added to the position to update the body's position in the world.


The update method continues, and calls checkWorldBounds(). The objective of this method is to separate the body from world bounds, if they have collided. As far as I can tell, this.worldBounce, which (I think) is the same as the bounce property of the physics body, is multiplied with the velocity to "bounce".


At the end of it all, it returns true if the body is colliding with the boundary (wasSet).


     checkWorldBounds: function ()
    {
        var pos = this.position;
        var bounds = this.customBoundsRectangle;
        var check = this.world.checkCollision;

        var bx = (this.worldBounce) ? -this.worldBounce.x : -this.bounce.x;
        var by = (this.worldBounce) ? -this.worldBounce.y : -this.bounce.y;

        var wasSet = false;

        if (pos.x < bounds.x && check.left)
        {
            pos.x = bounds.x;
            this.velocity.x *= bx;
            this.blocked.left = true;
            wasSet = true;
        }
        else if (this.right > bounds.right && check.right)
        {
            pos.x = bounds.right - this.width;
            this.velocity.x *= bx;
            this.blocked.right = true;
            wasSet = true;
        }

        if (pos.y < bounds.y && check.up)
        {
            pos.y = bounds.y;
            this.velocity.y *= by;
            this.blocked.up = true;
            wasSet = true;
        }
        else if (this.bottom > bounds.bottom && check.down)
        {
            pos.y = bounds.bottom - this.height;
            this.velocity.y *= by;
            this.blocked.down = true;
            wasSet = true;
        }

        if (wasSet)
        {
            this.blocked.none = false;
            this.updateCenter();
        }

        return wasSet;
    },

Collision detection with tile


Moving away from the body class, let's look at the relevant code of the main arcade physics world class, particularly the bits to do with sprite vs tile collision - I think the relevant method is collideSpriteVsTilemapLayer.


collideSpriteVsTilemapLayer(sprite, tilemapLayer, [collideCallback], [processCallback], [callbackContext], [overlapOnly])

This is Phaser arcade physics engine's internal handler for Sprite vs. Tilemap collisions, which is a method that returns true if there is a collision.


It is long and complex, but extract what I believe to be the key bits, the tiles overlapping with the physics body is stored in mapData, and passed to this.collideSpriteVsTilesHander:


var x = body.position.x;
var y = body.position.y;
var w = body.width;
var h = body.height;

var layerData = tilemapLayer.layer;

/// REDUCTED ///
var mapData = GetTilesWithinWorldXY(x, y, w, h, null, tilemapLayer.scene.cameras.main, tilemapLayer.layer);

if (mapData.length === 0)
        {
            return false;
        }
        else
        {
            return this.collideSpriteVsTilesHandler(sprite, mapData, collideCallback, processCallback, callbackContext, overlapOnly, true);
        }

The deducted part is the code below. layerData is the bit of a particular tilemap layer that contains data about the map, that all Tilemap and TilemapLayer objects have reference to. This is the bit that is looked up, in performing the various operations on individual tiles.


Then there is some complicated code to adjust the x and y coordinates (that is used to determine that area in which to search tiles overlapping with the physics body). This is (I think - frankly I don't fully understand it) to take into account situations where the base tile size (ie the size of the tile image) is different from the size of the tiles used at layer level (which can be different). I have never myself used tiles that are of different size to the base tile size, but I think this can be used, if you have say 32x64 tilesets, but you want them overlapping in a map. Anyway, if the base and the actual are the same, this is not necessary.


var layerData = tilemapLayer.layer;
        
if (layerData.tileWidth > layerData.baseTileWidth)
        {
            // The x origin of a tile is the left side, so x and width need to be adjusted.
            var xDiff = (layerData.tileWidth - layerData.baseTileWidth) * tilemapLayer.scaleX;
            x -= xDiff;
            w += xDiff;
        }

if (layerData.tileHeight > layerData.baseTileHeight)
        {
// The y origin of a tile is the bottom side, so just the height needs to be adjusted.
            var yDiff = (layerData.tileHeight - layerData.baseTileHeight) * tilemapLayer.scaleY;
            h += yDiff;
        }


collideSpriteVsTilesHandler(sprite, tiles, collideCallback, processCallback, callbackContext, overlapOnly, isLayer)

This is very complicated code but in essence it loops through all the tiles overlapping with the physics body (passed to it from collideSpriteVsTilemapLayer method).


The red code gets the 4 vertices of the tile in terms of worldXY coordinates.


var body = sprite.body;

var tile;
var tileWorldRect = { left: 0, right: 0, top: 0, bottom: 0 };
var tilemapLayer;
var collision = false;

for (var i = 0; i < tiles.length; i++)
{
tile = tiles[i];

tilemapLayer = tile.tilemapLayer;

var point = tilemapLayer.tileToWorldXY(tile.x, tile.y);

tileWorldRect.left = point.x;
tileWorldRect.top = point.y;

//  If the maps base tile size differs from the layer tile size, only the top of the rect
//  needs to be adjusted since its origin is (0, 1).
if (tile.baseHeight !== tile.height)
{
tileWorldRect.top -= (tile.height - tile.baseHeight) * tilemapLayer.scaleY;
            }

tileWorldRect.right = tileWorldRect.left + tile.width * tilemapLayer.scaleX;
tileWorldRect.bottom = tileWorldRect.top + tile.height * tilemapLayer.scaleY;

if (TileIntersectsBody(tileWorldRect, body)
                && (!processCallback || processCallback.call(callbackContext, sprite, tile))
                && ProcessTileCallbacks(tile, sprite)
                && (overlapOnly || SeparateTile(i, body, tile, tileWorldRect, tilemapLayer, this.TILE_BIAS, isLayer)))
{
                this._total++;

                collision = true;
/// REDUCTED ///

The really important bits are hidden away in the if statement; calling the TileIntersectsBody and SeparateTile methods. This if statement (I think) is calling the methods to carry out collision detection and collision resolution (separation).


(However, I do not really understand why TileIntersetBody is called since all the tiles passed to this method should only be those that overlap with the physics body anyway)


TileIntersectsBody(tileWorldRect, body)

This is a function that carries out AABB intersection test and returns a boolean,

  
    return !(
        body.right <= tileWorldRect.left ||
        body.bottom <= tileWorldRect.top ||
        body.position.x >= tileWorldRect.right ||
        body.position.y >= tileWorldRect.bottom
    );

SeparateTile(i, body, tile, tileWorldRect, tilemapLayer, tileBias, isLayer)


The collideSpriteVsTilesHandler method calls thie method with a number of parameters including:

  • i: the position of the tile in the original mapData array (returned from getTilesWithinWorldXY function called from within collideSpriteVsTilemapLayer. I don't know why this is passed to this method as it does not appear to be used anywhere.

  • body: the physics body

  • tile: the tile object

  • tileworldRect: the 4 vertices of the tile object in worldXY coordinates


First, minX and minY is set on the size of the x & y components of the displacement of the physics body (I think) in the last frame. It basically determines in which order to carry out collision detection in the x or y direction.


Then, the important bits. Looking at the blue code (same as red code, except the order in which horizontal vs vertical checks are performed);

  • for the horizontal check, TileCheckX is called with body, tile, tileLeft, tileRight passed to the method

  • for the vertical check, TileCheckY is called with body, tile, tileTop, tileBottom passed to the method



var tileLeft = tileWorldRect.left;
var tileTop = tileWorldRect.top;
var tileRight = tileWorldRect.right;
var tileBottom = tileWorldRect.bottom;
var faceHorizontal = tile.faceLeft || tile.faceRight;
var faceVertical = tile.faceTop || tile.faceBottom;

var ox = 0;
var oy = 0;
var minX = 0;
var minY = 1;

if (body.deltaAbsX() > body.deltaAbsY())
    {
        //  Moving faster horizontally, check X axis first
        minX = -1;
    }
    else if (body.deltaAbsX() < body.deltaAbsY())
    {
        //  Moving faster vertically, check Y axis first
        minY = -1;
    }


if (minX < minY)
    {
        if (faceHorizontal)
        {
            ox = TileCheckX(body, tile, tileLeft, tileRight, tileBias, isLayer);

            //  That's horizontal done, check if we still intersects? If not then we can return now
            if (ox !== 0 && !TileIntersectsBody(tileWorldRect, body))
            {
                return true;
            }
        }

        if (faceVertical)
        {
            oy = TileCheckY(body, tile, tileTop, tileBottom, tileBias, isLayer);
        }
    }
    else
    {
        if (faceVertical)
        {
            oy = TileCheckY(body, tile, tileTop, tileBottom, tileBias, isLayer);

            //  That's vertical done, check if we still intersects? If not then we can return now
            if (oy !== 0 && !TileIntersectsBody(tileWorldRect, body))
            {
                return true;
            }
        }

        if (faceHorizontal)
        {
            ox = TileCheckX(body, tile, tileLeft, tileRight, tileBias, isLayer);
        }
    }

    return (ox !== 0 || oy !== 0);


The really really important part is the TileCheckX and TileCheckY. TileCheckY is explored in detail below.


TileCheckY(body, tile, tileTop, tileBottom, tileBias, isLayer)


This method calculates the overlap of the physics body against the colliding tile, and then calls another method to actually separate the body.


The calculation for calculating the degree of overlap is different depending on whether the physics body is moving up or down.


For example, when the body is moving up (ie body.deltaY() is less than zero), separation is required if the body.y (ie top of the body) is less than the bottom of the tile (tileBottom). The amount that the sprite needs to move (to separate from tile) is the difference between body.y and tileBottom. ov = body.y - tileBottom, would give a negative number, which if deducted from body.y, would force the body to move down, hence separating the body from the tile.


The red highlighted code is somewhat cryptic. There is a check against something called the tileBias. By default, this is a constant set to 16, and described in the official documentation as:


The maximum absolute value of a Body's overlap with a tile that will result in separation on each axis. Larger values favor separation. Smaller values favor no separation. The optimum value may be similar to the tile size.


Hence looking the code in bold, it looks like for any overlaps greater than the tile bias, the overlap is reset to zero (ie the overlap is ignored). So the above description makes sense. However, I do not know why such a check is required.


var oy = 0;

var faceTop = tile.faceTop;
var faceBottom = tile.faceBottom;
var collideUp = tile.collideUp;
var collideDown = tile.collideDown;

/// REDUCTED ///

if (body.deltaY() < 0 && collideDown && body.checkCollision.up)
    {
        //  Body is moving UP
        if (faceBottom && body.y < tileBottom)
        {
            oy = body.y - tileBottom;

            if (oy < -tileBias)
            {
                oy = 0;
            }
        }
    }
    else if (body.deltaY() > 0 && collideUp && body.checkCollision.down)
    {
        //  Body is moving DOWN
        if (faceTop && body.bottom > tileTop)
        {
            oy = body.bottom - tileTop;

            if (oy > tileBias)
            {
                oy = 0;
            }
        }
    }


Once the above is executed, ProcessTileSeparationY is called, before the overlap is returned.


  if (oy !== 0)
    {
        if (body.customSeparateY)
        {
            body.overlapY = oy;
        }
        else
        {
            ProcessTileSeparationY(body, oy);
        }
    }

    return oy;

ProcessTileSeparationX(body, y) and ProcessTileSeparationY(body, y)



This method is quite easy to understand. First, the blocked properties are set.


Then the position is actually updated (to separate the physics body from the tile), and then the velocity is flipped by the bounce factor.


   if (y < 0)
    {
        body.blocked.none = false;
        body.blocked.up = true;
    }
    else if (y > 0)
    {
        body.blocked.none = false;
        body.blocked.down = true;
    }

    body.position.y -= y;

    if (body.bounce.y === 0)
    {
        body.velocity.y = 0;
    }
    else
    {
        body.velocity.y = -body.velocity.y * body.bounce.y;
    }
};

So that's a brief look at the internal workings of Phaser's sprite to tile collision detection, resolution and response.


In the next post I delve a bit more in the collision detection part, which I skipped over in the post.


What I will aim for first

Below is a simple tilemap and player character using Phaser 3 methods, that I will aim to recreate, as the interim step towards creating my own partial tile collision engine. In particular, the ability to handle gravity and bounce.







61 views0 comments

Comments


記事: Blog2_Post
bottom of page