coding

Creating falling spikes with Phaser JS - Game Devlog #20

Written in June 28, 2021 - 🕒 4 min. read

Hey everyone! Today I’m finally back with another game devlog for Super Ollie, and I’m going to implement a classic element of a 2D platformer, a falling spike.

Falling spikes

First I will create the spike class, nothing crazy:

class Spike extends GameObjects.Sprite {
    constructor({
        scene,
        x = 0,
        y = 0,
        asset = 'spike',
        enablePhysics = true,
        addToScene = true,
        frame,
        name,
    }) {
        super(scene, x, y, asset, frame);
        this.setOrigin(0, 1);
        this.setName(name || asset);

        if (addToScene) {
            scene.add.existing(this);
        }

        if (enablePhysics) {
            scene.physics.add.existing(this);
            this.body.setAllowGravity(false);
            this.body.setImmovable(true);
        }
    }
}

Now in my dataLayer loop (which I explained on the devlog 8) I will add a case for the spikes to be added.

// in the scene create function
const dataLayer = map.getObjectLayer('data');
dataLayer.objects.forEach((data) => {
    const { x, y, name, height, width } = data;

    if (name === 'spike') {
        // TODO add spike logic here
    }
});

Before going into the logic, my goal is to be able to add a spike wherever I want on Tiled, and the code will parse it and magically make it work.

How does a falling spike works on platformer games? Well, when the player walks under one, it will fall on them, either right away or a couple of milliseconds later, and have that we need a custom collider object, that when the player interacts with it, it will trigger the spike to fall.

This is exactly what I want to automate, create a logic so that the spike will automatically decide where the custom collider object will be in the map.

if (name === 'spike') {
    const spike = new Spike({
        scene: this,
        x,
        y,
    });

    const customCollider = createColliderFromGameObjectToGround(
        this,
        spike,
        mapSize,
        this.mapData.dynamicLayers.ground
    );

    const { y: spikeY } = spike;
    const collider = this.physics.add.overlap(
        customCollider,
        this.player,
        () => {
            // TODO handle spike falling
        }
    );

    this.physics.add.overlap(
        spike,
        this.player,
        () => {
            // TODO handle spike hitting the player
        }
    );
}

The createColliderFromGameObjectToGround function will have all the logic to get the spike position, find the nearest ground tile from that position, and place a custom collider there.

Inside this new function, I’m going to need 2 other functions, createInteractiveGameObject, that I showed on the devlog 15, and a new function called isBelowTileCollidable, that will receive a game object and check if the tile below that object is collidable or not.

const isBelowTileCollidable = (
    gameObject,
    dynamicLayer // single layer or GameObject Group
) => {
    const { x, y, height } = gameObject;
    const layers = dynamicLayer?.getChildren?.() || [dynamicLayer];

    let tile = null;
    let result = false;
    layers.forEach((layer) => {
        if (result) {
            return;
        }

        tile = layer.getTileAtWorldXY(
            x - 0.5,
            y + height + 0.5
        );

        if (tile) {
            result = tile?.properties?.collideUp;
        }
    });

    return result;
};

For the createColliderFromGameObjectToGround function, I will create a loop of the size of the map height, and from the spike, position start looking at the nearest ground tile, I will use the some function, so I can early return from the loop when a ground tile is found.

const createColliderFromGameObjectToGround = (
    scene,
    gameobject,
    mapSize,
    mapLayer
) => {
    let customCollider;
    let result = false;

    // TODO improve the size of this array
    new Array(mapSize.height / gameobject.height).fill(null).some(
        (val, index) => {
            const posY = gameobject.y + (TILE_HEIGHT * (index + 1));
            result = isBelowTileCollidable({
                    ...gameobject,
                    y: posY,
                },
                mapLayer
            );

            if (result) {
                const spikeHeight = gameobject.height + (posY - gameobject.y);
                customCollider = createInteractiveGameObject(
                    scene,
                    gameobject.x,
                    // posY,
                    posY - spikeHeight + gameobject.height,
                    gameobject.width,
                    // spike.height,
                    spikeHeight,
                    gameobject.name
                );
            }

            return result;
        }
    );

    return customCollider;
};

With these 2 functions in place, I can come back to my dataLayer loop and work on the spike logic, for now, I want the spike to fall when the player interacts with the trigger, and then make the spike reappear in the ceiling after 2 seconds.

if (name === 'spike') {
    const spike = new Spike({
        scene: this,
        x,
        y,
    });

    const customCollider = createColliderFromGameObjectToGround(
        this,
        spike,
        mapSize,
        this.mapData.dynamicLayers.ground
    );

    // if for some reason the collider was not created, do nothing.
    if (customCollider) {
        const { y: spikeY } = spike;
        const collider = this.physics.add.overlap(
            customCollider,
            this.player,
            () => {
                if (!spike.body.allowGravity) {
                    spike.body.setAllowGravity(true);
                    this.time.delayedCall(2000, () => {
                        spike.body.setAllowGravity(false);

                        spike.body.setAcceleration(0, 0);
                        spike.body.setVelocity(0, 0);
                        spike.setY(spikeY);
                        spike.setActive(true);
                        spike.setVisible(true);
                    });
                }
            }
        );

        this.physics.add.overlap(
            spike,
            this.player,
            () => {
                if (spike?.visible) {
                    this.gameHud.decreasePlayerLife(5);
                    spike.setActive(false);
                    spike.setVisible(false);
                }
            }
        );
    } else {
        spike.destroy();
    }
}

With debug mode on, this is what the collider looks like in the game:

Spike colliders
Spike colliders

That’s all for today, see you in the next devlog!

Tags:


Post a comment

Comments

No comments yet.