Wednesday, December 16, 2015

Let's Add More Weapons to my Phaser Platformer

After adding the health meter earlier this week, the playable sandbox level for my platformer built with Phaser was looking much more complete. There are just a few more things I need to add before I switch my focus to level design and make a legitimate game out of it. One of those things is more weapons.

My plan for the game is to make it similar to a classic 8-bit Mega Man game. In those, after defeating one of the bosses, Mega Man could use the weapon from that boss. To make that happen in my game, I'd have to expand the weapons capability. I needed to add the ability to switch to different weapons that each had different qualities.

To begin, I defined an array of weapon objects. Each object would have its own name, velocity attributes, and power to damage enemies. For each of these weapon types, I will create a Phaser Group to manage them. All the weapon groups will be added as children of an overall weapons group. Look at this code from my create function:

function create() {

    // ... other stuff ...

    weaponsGroup = game.add.group();
    weapons = [];
    weapons.push({
       name: 'pencil',
       lifespan: 1500,
       velocity: {
           x: 500,
           y: -400
       },
       spin: 50,
       power: 10
    });

    weapons.push({
        name: 'flask',
        lifespan: 1500,
        velocity: {
            x: 600,
            y: -600
        },
        spin: 50,
        power: 20
     });

    weapons.push({
        name: 'hammer',
        lifespan: 1500,
        velocity: {
            x: 600,
            y: -400
        },
        spin: 50,
        power: 30
     });

    weapons.push({
        name: 'brace',
        lifespan: 1500,
        velocity: {
            x: 800,
            y: -200
        },
        spin: 80,
        power: 40
     });

    for (var w = 0; w < weapons.length; w++) {
        var wg = game.add.group();
        wg.createMultiple(3, weapons[w].name, 0, false);
        weapons[w].sprites = wg;
        weaponsGroup.add(wg);
    }

    // ... other stuff ...

}

The overall group, weaponsGroup, is created first. Then I set up an array of 4 weapon types. The name is important because I have it set up to match the sprite asset key for that weapon. I then loop over the weapons array and create a group just for a set of those weapons. I set that group as the sprites field on the weapon object - I'll need to reference it later. Also, those groups are added as children of the weaponsGroup.

At this point I have a group of weapons groups. Any of these can be use to strike an enemy, so they must be checked for enemy collisions. A slight change in my update function takes care of that. The overlap function can take an array of groups in addition to a single group, and the children field of a Group object is the array of child groups!

function update() {

    // ... other collision checks ...

    game.physics.arcade.overlap(enemies, weaponsGroup.children, hurtEnemy, null, this);

    // ... other stuff ...

}

Okay, so how do I have the frog throw a particular type of weapon? First, I added a variable to the game called weaponIndex. It is set to the index of whatever weapon is currently active. So far, I have 4 weapons which means that the index can be 0, 1, 2, or 3. Then in my throwSomething function, I get the weapon object out of the weapons array using the index and use its properties to perform the toss.

function throwSomething() {

    // has it been long enough? can we throw something yet?
    if (game.time.now > nextFire) {

        nextFire = game.time.now + FIRE_RATE;

        // see if a weapon is available from the group
        var weapon = weapons[weaponIndex].sprites.getFirstExists(false);
        if (weapon) {

            weapon.exists = true;
            weapon.anchor.setTo(0.5, 0.5);
            weapon.reset(frog.body.position.x+20, frog.body.position.y-20);
            game.physics.arcade.enable(weapon);

            weapon.lifespan = weapons[weaponIndex].lifespan;
            weapon.body.angularVelocity = weapons[weaponIndex].spin;
            weapon.body.velocity.y = weapons[weaponIndex].velocity.y;
            weapon.power = weapons[weaponIndex].power;
            if (frog.scale.x == 1) {
                weapon.body.velocity.x = weapons[weaponIndex].velocity.x;
            } else{
                weapon.body.velocity.x = -weapons[weaponIndex].velocity.x;
            }
            
        }

    }

}

This code is very similar to what I had before, when the only weapon was the pencil. The differences are that the sprite, lifespan, velocity, and power are pulled from the weapon object in the weapons array instead of always being hard-coded to the same values.

One final change was to the function that handles hurting the enemies when there is a collision between a weapon and an enemy. Previously, I used a fixed damage value for the enemy. Now, the damage to the enemy is based on the power field of the particular weapon that is hitting it. For example, it takes 4 hits with a pencil to kill a toad but only 1 hit from a curly brace.

function hurtEnemy(e, w) {

    if (!e.immune) {
        e.immune = true;
        e.alpha = 0.5;

        // damage to enemy = power of weapon
        e.damage(w.power);

        w.exists = false;

        game.time.events.add(200, function() {
            e.immune = false;
            e.alpha = 1;
        }, this);
    }

}

The hurtEnemy function is what gets called when overlap is detected between a sprite in the enemies group and a sprite in one of the weapons groups. The first parameter e will be the enemy object and the second parameter w will be the weapon that hit it. Since my throwSomething function set the power field on the weapon sprite to match the power field on the weapon definition, I can read that property here to control the amount of damage to the enemy.

Want to give it a try for yourself? Play the game here: http://amphibian.com/eight-bit/. Just hit 1 through 4 on the keyboard to switch between the different weapons. If you'd like to see the complete source code, it's on GitHub.

As for comics today, this is the first of a 5-part Christmas story. It roughly follows the pattern of Dicken's A Christmas Carol, but...well, you'll see. They're a bit longer than the normal ones, but don't take too much longer to read. I hope you enjoy them!

Amphibian.com comic for 16 December 2015