Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


12. October 2014

 

The following is a guest tutorial written by Guntis at MightyEditor showing how you can easily use MightyEditor and Phaser to create a game.

 

 

This article will give you an example on how to use popular HTML5 game development framework Phaser and editor based on top of it - MightyEditor. This tutorial will take about an hour and only ~90 lines of actual code. Following game development aspects will be introduced: sprites, sprite animations, physics, collision, game states, player death/revival.

 

Requirements

 

You should use the newest version of Google Chrome. Other browsers should work, but are not tested.

 

Creating a project

 

In this tutorial we will create a coin collecting-spike avoiding platformer.

Go to http://mightyeditor.mightyfingers.com/ and click on "Create New Project". Enter the name of your game. In this tutorial, our game will be called "SockMan"

 

1

 

Next, locate the settings panel in the bottom-right and change the worldHeight, worldWidth, viewportHeight, and viewportWidth values to 1026x578, as shown in the image below. Viewport is the size of the in-game camera, but we won't be dealing with camera controls in this tutorial. 

 

2

 

Uploading game assets

 

Next up - game assets. First, download the assets and extract them. Then locate the asset manager in the upper-right corner and either select the Upload File/Upload Folder options or simply drag and drop the files from your file explorer straight to the manager panel.

 

3 

 

Creating the world

 

Select the stamp icon, then, in the asset panel, click on the image you want to place, bg_wall.png, for now and then click on the map grid to  place the asset on the map. Note, that the dark grey area represents the world/viewport. CTRL-click snaps the image to grid and you can move the image around afterwards by selecting the arrow tool in the left side menu. More accurate placement is possible by using the 'settings' menu in bottom-right. Next, place the 'grass_tile.png' asset like described above and you should you get a world like in the picture below.

 

4

 

Next up - grouping the placed objects together. Locate the objects panel in the middle-right and either select Add Group and then select and drag the objects in the newly created group or select the objects and then click on Group Selected Objects (SHIFT-click selects multiple objects). After that, rename the group to "bg" (double-click on group name) and your project should look like the image below.

 

 5

 

Note: it can be useful to lock (small lock icon on the right side) the background and any other groups/objects in place, so they become unclickable and don't mess with placing other objects.

 

Adding foreground objects

 

After placing background, we will add (almost) the rest of the objects to the game world - boxes, grass tiles, chest and spikes. To do that, simply use the stamp tool to place each object on the map (remember about CTRL to snap to grid) and group them accordingly - boxes and grass in "objects", spikes in "spikes" group and leave the chest 'groupless'. Everything should look similar to the image below, but feel free to create your own layout.

 

6

 

As for the actual character and coins, things get a little different, because there are multiple frames per image for animations. Select the character.png asset and in the Settings panel set its frameWidth and frameHeight values to 100px,the anchorX to 0.5 and anchorY values to 0.7.

 

7

After that just place the character sprite on the left side of the map by using the usual stamp tool. Repeat these step with the coin spritesheet, only changing the frameWidth to 113px and frameHeight to 71px. Both anchors are 0.5. Place multiple coins on the map and group them together in a 'coins' group. Finally, select the the text icon (big T) in the toolbar and place a "0" in the top right corner of your game world. In the objects panel, rename it to "gold". Now your game world should look like this and we can turn to actual coding.

 

8 

 

Switching to source editor

 

You can switch to the source editor in the top-left of the screen. Editor keyboard shortcuts can be found here.

 

9

 

Game states

 

Coding is done by using the Phaser development framework (homepage and documentation). There are different states (separate parts of the game like intro, menu, gameplay etc.). The MightyEditor gives you four states by default: boot, load, menu and play, but, in this tutorial, we will only be coding in the play state and without any menus, so we should just go straight to the play state. Switch to the 'menu.js' file and call the switch to the play state.

window.SockMan.state.menu = {
        create: function() {

            this.game.state.start("play");
        },

 

You can now click on the Open Game button in the top panel and the game will load... to a blank screen. The graphics must be initialized beforehand. To do that, we will switch to the play.js state. We need to use the mt.create function in the create method like this:

 

create: function() {
    this.bg = mt.create("bg");
    this.character = mt.create("character");
    this.spikes = mt.create("spikes");
    this.chest = mt.create("chest");
    this.objects = mt.create("objects");
    this.coins = mt.create("coins");
    this.gold = mt.create("gold");
},

 

This initializes and creates the game sprites and sprite groups. Now, when opening the game, the visuals will be there, but the image is static.

 

Adding Physics

 

To add movement, first we must enable physics on the character. To do that, head back to the Map Editor and select your character sprite. In the bottom-right, use the Physics panel to enable it. Set these values:

  • enable: 1
  • immovable: 0
  • size-width: 40
  • size-height: 70
  • gravity-allow: 1
  • gravity-y:1000
  • collideWorldBounds: 1

 

10

 

In the end, everything should look like this:  Note: the size parameters are for the physics body, so the game interacts only with the area you want, not the blank parts or the sprite. Your character should now fall through the floor and stay on the edge of the game (that's what collideWorldBounds is for). To fix that, we must enable collision for the objects group.  To do that, just change the physics enable parameter of the group and add this piece of code in your update method (a function, looped 60 times per second):

 

this.game.physics.arcade.collide(this.character, this.objects.self, function(character, object) {
            console.log('Collision detected');
        }, null, this);

And all should be well. The collision detection's first two arguments check for interaction between two entities - the character sprite and a group of objects - and the third argument is a function which does something when these two objects (the character and a separate object of the group) do collide. Congratulations! Your character no longer falls through the floor tiles (grass/boxes).

 

Movement

 

But we need to move. To do that, the keyboard controls must be enabled beforehand. Simply put these two lines in the create method:

 

this.cursors = this.game.input.keyboard.createCursorKeys();
this.space = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

 

The keyboard.createCursorKeys() method initializes the arrow keys. Other keys, like SPACEBAR, SHIFT or CTRL must be initialized like shown above. After that, define actions to do when a key is pressed in the update method:

 

if (this.cursors.left.isDown) {
    this.character.body.velocity.x = -200;
} else if (this.cursors.right.isDown) {
    this.character.body.velocity.x = 200;
} else {
    this.character.body.velocity.x = 0;
}
if (this.space.isDown || this.cursors.up.isDown) {
    this.character.body.velocity.y = -550;
}

 

Arrow keys control your character, alternatively you can jump with SPACEBAR. Or maybe we should say - fly! But we only want to jump, not fly. To do that, one must enable the character to jump only if it is standing on the ground. Delete the current piece of jump code (the one with space and cursors.up) and instead place this in your collision detection function, so it looks like this:

 

this.game.physics.arcade.collide(this.character, this.objects.self, function(character, object) {
    if (this.space.isDown || this.cursors.up.isDown) {
        if (object.body.touching.up) {
            this.character.body.velocity.y = -550;
        }
    }

}, null, this);

 

Adding Character Animations

 

Animations, just like mostly everything, must first be initialized. In the create method, add these lines of code:

this.character.animations.add('stand', [0, 1, 2, 3], 10, true);
this.character.animations.add('run', [6, 7, 8, 9, 10, 11], 10, true);
this.character.animations.add('jump', [12, 13], 10, false);
this.character.animations.add('die', [18, 19], 10, false);
this.character.animations.play('stand');

 

These lines describe animations for your character. The first argument is the 'key' of the animation, the second - an array of frames which this particular animation uses, the third - frames per second and the last argument describes whether the animation should loop. The last line starts an animation when you launch the game. The rest of the animations is played according to situation, when certain condition is met, like a button press. The rest is added in the update method like this:

 

if (this.cursors.left.isDown) {
    this.character.body.velocity.x = -200;
    this.character.animations.play('run');
    this.character.scale.x = -1;
} else if (this.cursors.right.isDown) {
    this.character.body.velocity.x = 200;
    this.character.animations.play('run');
    this.character.scale.x = 1;
} else {
    this.character.body.velocity.x = 0;
    this.character.animations.play('stand');
}

 

And for jump animation, update your collision detection function with this:

 

this.game.physics.arcade.collide(this.character, this.objects.self, function(character, object) {
    if (this.space.isDown || this.cursors.up.isDown) {
        if (object.body.touching.up) {
            this.character.animations.play('jump');
            this.character.body.velocity.y = -550;
        }

    }
}, null, this);

 

At this point you might be wondering why you can't see a jump animation. It's because it gets overwritten by the 'run' or 'stand' animations since their conditions are met as well. We must invent another variable which tells the animations whether the character is in the air. Long story short, after putting this variable in, all of the update method will look like this:

 

update: function() {
    var standing = false;
    this.game.physics.arcade.collide(this.character, this.objects.self, function(character, object) {
        if (object.body.touching.up) {
            standing = true;
        } else standing = false;
        if (this.space.isDown || this.cursors.up.isDown) {
            if (object.body.touching.up) {
                this.character.animations.play('jump');
                this.character.body.velocity.y = -550;
                standing = false;
            }

        }
    }, null, this);

    if (this.cursors.left.isDown) {
        this.character.body.velocity.x = -200;
        if (standing) this.character.animations.play('run');
        this.character.scale.x = -1;
    } else if (this.cursors.right.isDown) {
        this.character.body.velocity.x = 200;
        if (standing) this.character.animations.play('run');
        this.character.scale.x = 1;
    } else {
        this.character.body.velocity.x = 0;
        if (standing) this.character.animations.play('stand');
    }
}

 

Collecting Coins

 

Before you are able to collect coins, their physics must be enabled, the physics body size set to 24x24 and offsetY to 10.

 

 

Next, we initialize the animation:

 

this.coins.self.callAll('animations.add', 'animations', 'collect', [7, 8], 10, false);

 

Note the difference between this line and those used to initialize the character's animations. Since the character was a lone sprite but the coins are all in a group, we must use the callAll method which initializes the animation for each and every child of the group separately. The first argument of the method is the method you would normally use, the second is the context and the rest is identical to adding animations as usual. After animating, we determine overlapping between the character and coins, making the coin disappear  when touch ends and the animation is done playing, and adding +1 to the counter in the upper-right corner:

 

this.game.physics.arcade.overlap(this.character, this.coins.self, function(character, coin) {
    coin.body = null;
    var coinCollect = coin.animations.play('collect');
    coinCollect.killOnComplete = true;
    var newPoints = parseInt(this.gold._text) + 1;
    this.gold.setText(newPoints);
}, null, this);

 

The coin.body is set to null, disabling the physics body. Otherwise the points would be added as long as the animation is still playing. since the character overlaps the animation.

 

Spikes, Life and Death

 

Next up is being able to kill the character when it touches the spikes. To do this, you must enable the physics of your 'spikes' group and set the body height to 20px and the offsetY parameter to 44px - this changes the Y coordinate from which the body is calculated. Pretty much an alternative to anchorY. After that comes the now usual collision detection:

 

this.game.physics.arcade.collide(this.character, this.spikes.self, function(character, spike) {
    if (character.alive) character.animations.play('die');
    character.body.velocity.x = 0;
    character.body.velocity.y = 0;
    character.alive = false;
}, null, this); 

 

The only problem now is that, after dying, you can still move. To avoid this, check if the character.alive is true before movement:

 

var standing = false;
this.game.physics.arcade.collide(this.character, this.objects.self, function(character, object) {
    if (object.body.touching.up) {
        standing = true;
    } else standing = false;
    if ((this.space.isDown || this.cursors.up.isDown) && character.alive) {
        if (object.body.touching.up) {
            this.character.animations.play('jump');
            this.character.body.velocity.y = -550;
            standing = false;
        }

    }
}, null, this);
if (this.character.alive) {
    if (this.cursors.left.isDown) {
        this.character.body.velocity.x = -200;
        if (standing) this.character.animations.play('run');
        this.character.scale.x = -1;
    } else if (this.cursors.right.isDown) {
        this.character.body.velocity.x = 200;
        if (standing) this.character.animations.play('run');
        this.character.scale.x = 1;
    } else {
        this.character.body.velocity.x = 0;
        if (standing) this.character.animations.play('stand');
    }
}

 

And finally, we add the ability to revive and send your character to start position by pressing SPACEBAR (set the character coordinates to your starting coordinates, viewed in the Map Editor):

 

if (this.space.isDown && !this.character.alive) {
    this.character.revive();
    this.character.x = 68;
    this.character.y = 452;
}

 

Achieving the Goal

 

One last thing - making the game do something when touching the big chest. This time we won't be using physics but check for overlapping differently (viable for single sprites only, not groups). First, add this checkOverlap function after the update method (don't forget to add a coma after the ending brace of the update method):

 

checkOverlap: function(spriteA, spriteB) {
    var boundsA = spriteA.getBounds();
    var boundsB = spriteB.getBounds();
    return Phaser.Rectangle.intersects(boundsA, boundsB);
}

 

And finally - call this function from within update:

 

if (this.checkOverlap(this.character, this.chest)) {
    this.game.state.start("play");
}

 

In this case, the 'play' state is restarted as soon as you touch the chest.

 

Congratulations!

 

Your game is now be fully playable and the code should closely resemble this:

 

"use strict";
window.SockMan.state.play = {
    create: function() {
        this.bg = mt.create("bg");
        this.character = mt.create("character");
        this.spikes = mt.create("spikes");
        this.chest = mt.create("chest");
        this.objects = mt.create("objects");
        this.coins = mt.create("coins");
        this.gold = mt.create("gold");

        this.cursors = this.game.input.keyboard.createCursorKeys();
        this.space = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

        this.character.animations.add('stand', [0, 1, 2, 3], 10, true);
        this.character.animations.add('run', [6, 7, 8, 9, 10, 11], 10, true);
        this.character.animations.add('jump', [12, 13], 10, false);
        this.character.animations.add('die', [18, 19], 10, false);
        this.character.animations.play('stand');
        this.coins.self.callAll('animations.add', 'animations', 'collect', [7, 8], 10, false);
    },

    update: function() {
        var standing = false;
        this.game.physics.arcade.collide(this.character, this.objects.self, function(character, object) {
            if (object.body.touching.up) {
                standing = true;
            } else standing = false;
            if ((this.space.isDown || this.cursors.up.isDown) && character.alive) {
                if (object.body.touching.up) {
                    this.character.animations.play('jump');
                    this.character.body.velocity.y = -550;
                    standing = false;
                }
            }
        }, null, this);
        if (this.character.alive) {
            if (this.cursors.left.isDown) {
                this.character.body.velocity.x = -200;
                if (standing) this.character.animations.play('run');
                this.character.scale.x = -1;
            } else if (this.cursors.right.isDown) {
                this.character.body.velocity.x = 200;
                if (standing) this.character.animations.play('run');
                this.character.scale.x = 1;
            } else {
                this.character.body.velocity.x = 0;
                if (standing) this.character.animations.play('stand');
            }
        }

        this.game.physics.arcade.overlap(this.character, this.coins.self, function(character, coin) {
            coin.body = null;
            var coinCollect = coin.animations.play('collect');
            coinCollect.killOnComplete = true;
            var newPoints = parseInt(this.gold._text) + 1;
            this.gold.setText(newPoints);
        }, null, this);

        this.game.physics.arcade.collide(this.character, this.spikes.self, function(character, spike) {
            if (character.alive) character.animations.play('die');
            character.body.velocity.x = 0;
            character.body.velocity.y = 0;
            character.alive = false;
        }, null, this);

        if (this.space.isDown && !this.character.alive) {
            this.character.revive();
            this.character.x = 68;
            this.character.y = 452;
        }

        if (this.checkOverlap(this.character, this.chest)) {
            this.game.state.start("play");
        }
    },

    checkOverlap: function(spriteA, spriteB) {
        var boundsA = spriteA.getBounds();
        var boundsB = spriteB.getBounds();
        return Phaser.Rectangle.intersects(boundsA, boundsB);
    }
};

 

Full game project: http://mightyeditor.mightyfingers.com/#pe45-copy

Final game: http://mightyeditor.mightyfingers.com/data/projects/pe45/phaser/index.html

The end result:

 

Click here to open in a new window.

Programming , , ,

blog comments powered by Disqus

Month List

Popular Comments