Matter Ghost Collisions

class Example extends Phaser.Scene
{
    map;
    cam;
    text;

    preload ()
    {
        this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/matter-ghost-vertices.json');
        this.load.image('kenney_redux_64x64', 'assets/tilemaps/tiles/kenney_redux_64x64.png');
        this.load.image('ball', 'assets/sprites/mushroom-32x32.png');
    }

    create ()
    {
        this.map = this.make.tilemap({ key: 'map' });
        const tileset = this.map.addTilesetImage('kenney_redux_64x64');
        const layer = this.map.createLayer(0, tileset, 0, 0);

        // "Ghost collisions" can happen in physics engines when two colliding bodies are next to one
        // another, e.g. a player trying to walk over two neighboring ground tiles. The order in which
        // the collisions are resolved by the engine can cause "unrealistic" effects, e.g. the player
        // being stopping dead in their tracks on flat ground). See
        // http://www.iforce2d.net/b2dtut/ghost-vertices for more info. When working with tilemaps and
        // Matter, there are a couple ways to mitigate this issue:
        //  - Add chamfer to bodies, i.e. round the edges, or use circular bodies to reduce the impact
        //    of the ghost collisions.
        //  - Map out your level's hitboxes as as a few convex hulls instead of giving each tile a
        //    separate body. You can still use Tiled for this. Create an object layer, and fill it
        //    with shapes, convert those shapes to Matter bodies in Phaser (see below).
        //  - Use a library like hull.js (https://github.com/AndriiHeonia/hull) to automatically figure
        //    out convex hulls from your tiles.

        // Set up the grass tiles to have individual matter bodies.
        layer.setCollisionByProperty({ type: 'grass' });
        this.matter.world.convertTilemapLayer(layer);

        // The stone platform has been mapped as a single, long rectangle in Tiled. See
        // Phaser.Physics.Matter.TileBody#setFromTileCollision for how to parse other Tiled shapes.
        const rect = this.map.findObject('Collision', obj => obj.name === 'Stone Platform');
        this.matter.add.rectangle(
            rect.x + (rect.width / 2), rect.y + (rect.height / 2),
            rect.width, rect.height,
            { isStatic: true }
        );

        this.cam = this.cameras.main;
        this.cam.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
        this.cam.setScroll(0, 700);

        this.time.addEvent({
            delay: 500,
            callback: function ()
            {
                const shroom1 = this.matter.add.image(10, 1200, 'ball');
                shroom1.setRectangle();
                shroom1.setFriction(0);
                shroom1.body.force.x = 0.05;
                this.time.addEvent({ delay: 2000, callback: this.destroyShroom.bind(this, shroom1) });

                const shroom2 = this.matter.add.image(10, 880, 'ball');
                shroom2.setRectangle();
                shroom2.setFriction(0);
                shroom2.body.force.x = 0.05;
                this.time.addEvent({ delay: 2000, callback: this.destroyShroom.bind(this, shroom2) });
            },
            callbackScope: this,
            loop: true
        });

        this.matter.world.setBounds(this.map.widthInPixels, this.map.heightInPixels);

        this.text = this.add.text(16, 16, 'Ghost Collisions Demo\nGrass: Individual Tile Bodies\nStone: A Single Convex Body', {
            fontSize: '20px',
            padding: { x: 20, y: 10 },
            backgroundColor: '#ffffff',
            fill: '#000000'
        });
        this.text.setScrollFactor(0);
    }

    destroyShroom (shroom)
    {
        this.matter.world.remove(shroom);
        shroom.destroy();
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    pixelArt: true,
    physics: {
        default: 'matter',
        matter: {
            gravity: { y: 1 },
            debug: true
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);