Monday, November 9, 2015

Single-Direction Collision for your Phaser Tiles

I was working on my platformer (full source code on GitHub) using Phaser, and I wanted to add some platforms to the map. You know, like you do in platformers. Specifically, I wanted to have some box-like things in the environment that the frog could jump on top of, but also walk in front of. Like those box things with the screws in the corners from Super Mario Bros. 3. Mario walked in front of them, but if he jumped up he would land on top and walk across them. You remember:

I wanted boxes like the one upon which Mario stands.

The tileset I was using had some things like that in it, so I used them and made some nice platforms in my sandbox level. It was then I realized that I had a problem. If I set the tiles that made up the boxes to collide with the frog, he could jump on them but not walk in front of them. Maybe if I just set the top tiles as having collisions, or put the lower halves in the background layer, I could walk in front of them. Yes, but I still couldn't jump straight up and land on top because the frog would bump into the top tiles from below.

I want to jump on these.

Phaser supports one-way collisions for physics bodies with the Arcade Physics module that I am using, but since I was dealing with a tilemap there didn't seem to be a way to set it up. When you want to set up collision with some of your tiles, you use one of the setCollision functions (setCollision, setCollisionBetween, setCollisionByExclusion). They don't take any parameters to fine-tune the collisions. Internally, those functions call setCollisionByIndex. Looking at the source for that function, I could see that every tile was indeed set to collide on all sides. It took me a little while, but I found a way to override that behavior and get the effect I wanted.

Here is the function I came up with:

function setTileCollision(mapLayer, idxOrArray, dirs) {

    var mFunc; // tile index matching function
    if (idxOrArray.length) {
        // if idxOrArray is an array, use a function with a loop
        mFunc = function(inp) {
            for (var i = 0; i < idxOrArray.length; i++) {
                if (idxOrArray[i] === inp) {
                    return true;
                }
            }
            return false;
        };
    } else {
        // if idxOrArray is a single number, use a simple function
        mFunc = function(inp) {
            return inp === idxOrArray;
        };
    }

    // get the 2-dimensional tiles array for this layer
    var d = mapLayer.map.layers[mapLayer.index].data;
    
    for (var i = 0; i < d.length; i++) {
        for (var j = 0; j < d[i].length; j++) {
            var t = d[i][j];
            if (mFunc(t.index)) {
                
                t.collideUp = dirs.top;
                t.collideDown = dirs.bottom;
                t.collideLeft = dirs.left;
                t.collideRight = dirs.right;
                
                t.faceTop = dirs.top;
                t.faceBottom = dirs.bottom;
                t.faceLeft = dirs.left;
                t.faceRight = dirs.right;
                
            }
        }
    }

}

Despite the weird-looking top half of the function that is just setting up a way of matching against either a single tile id value or an array of values, overall the function is very simple. It gets the 2-dimensional array of all the tiles in the given layer and loops over them. If the tile index matches one of the specified index values, the collision settings for that tile are altered in accordance with the dirs object. The dirs object should have 4 boolean fields, top, bottom, left, and right. A value of true for any of them indicates that you want Phaser to check for collisions with other objects coming from that direction. In my case, only top was set to true. I only want to cause a collision when the frog hits the tile from above.

Here is how I use it from my create function:

function create() {

    game.physics.startSystem(Phaser.Physics.ARCADE);
    game.physics.arcade.gravity.y = 1500;

    var map = game.add.tilemap("stage1");
    map.addTilesetImage("ground", "tiles");

    layer = map.createLayer("layer1");
    layer.resizeWorld();

    map.setLayer(layer);
    
    // set collisions on ground tiles
    map.setCollisionBetween(1, 275);

    // set collisions on tiles that are the top parts of the boxes
    map.setCollisionBetween(3354, 3356);
    map.setCollisionBetween(2900, 2903);
    map.setCollisionBetween(3714, 3716);

    // set those box top tiles to only collide from above
    setTileCollision(layer, [3354, 3355, 3356, 2900, 2901, 2902, 2903, 3714, 3715, 3716], {
        top: true,
        bottom: false,
        left: false,
        right: false
    });

    // ... other stuff ...    

}

The end result is just what I wanted - the frog can walk in front of the boxes and jump up to land on top of them. The following video shows the box-jumping in action.


And speaking of jumping on boxes - if it's that time of year at your workplace in which you check boxes on reviews of your coworkers, you might enjoy today's comic. In it, we find that the frogs have a more basic approach to the classic forced ranking system.

Amphibian.com comic for 9 November 2015