Monday, July 13, 2015

Starting out with Phaser and the Isometric Plug-in

I mentioned last week how I am finally starting to work on my 404-page game for Amphibian.com. And while I'm doing this I will be learning a new HTML5 game framework, Phaser.

Phaser is a very popular choice for creating modern games for the web browser. It supports both "regular" Canvas and WebGL, and also has mobile optimization as one of its core principles. I thought it would be a good thing to learn. I want to train now for the GitHub Game Off 2016 (assuming there will be one!).

Phaser is a modular framework that supports third-party plugin modules, and while browsing through the Phaser tutorials and documentation I came across the Isometric plugin. It claims to allow easy creation of isometric 3D-style games projected onto the 2D Phaser canvas. The demos looked good, and I've always loved the isometric style so I thought I'd give it a try.

Today, I will document my experiences so far.

First of all, I want to express some general reservations about using Phaser on mobile or desktop. The JavaScript file is enormous - even minified it comes in at 692 KB! Forget about serving the full-size version at 2.63 MB. But I put those reservations aside and gave it a try anyway.

I found the Phaser documentation to be very long-winded. I tend to learn best by just jumping in to a working example and playing around with it, but it was difficult to locate anything both basic and useful at the same time. Eventually I came upon this boilerplate template for starting a new Phaser game using the Isometric plugin. First is the basic HTML page:

<!DOCTYPE html>
<html>
<head>

    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

    <title>Froggy 404</title>

    <link rel="stylesheet" type="text/css" href="css/style.css"/>

    <script src="js/phaser.min.js"></script>
    <script src="js/phaser-plugin-isometric.js"></script>
    <script src="js/game.js"></script>
    
</head>
<body>
  
</body>
</html>

And then the JavaScript code for the game.js file:

var width = window.innerWidth;
var height = window.innerHeight;

var init = function () {

    var game = new Phaser.Game(width, height, Phaser.AUTO, 'test', null, false, true);

    var BasicGame = function (game) { };

    BasicGame.Boot = function (game) { };

    BasicGame.Boot.prototype =
    {
        preload : function() {

            // load game resouces here
            game.load.image('id1', 'path/to/image1.png');
            game.load.image('id2', 'path/to/image2.png');

            // add and configure plugins...

            // set world size, turn off physics, etc.

        },

        create : function() {

            // setup game elements here.
            // create sprites, controls, camera, etc.

        },

        update : function() {

            // handle movement stuff...

            // check for collisions, etc.

        },

        render : function() {

            // special render handling

        }

    };

    game.state.add('Boot', BasicGame.Boot);
    game.state.start('Boot');
 
};

window.onload = init;

The key parts here are that as soon as the page is loaded, you can create the game as seen on line 6. Since by default Phaser will create the Canvas element for you (note that the HTML page is pretty much empty) it needs to know the dimensions. Using window.innerWidth and window.innerHeight will create a full-window game experience. Lines 8, 10, and 12 are setting up a BasicGame class and giving it a function called Boot. This function will handle a game state, and in this simple game there will be only one state. The prototype for the Boot function contains four basic methods: preload, create, update, and render. Those are where you put the actual game code. Lines 49 and 50 set the Boot class to a game state and then start that state.

One other thing to note is that I am NOT using the minified version of the Isometric plugin. Why not? Because I get a JavaScript error using the minified version of the latest release! Booo! Anyway, moving on...

Now to write some actual game code. What do I want my game to do? A lot of stuff, but I have to start small. To demo, I am going to make an area with a tree, a soccer ball, and a frog. The player can move the frog around with the arrow keys and kick the ball. Sounds easy, right?

First, the preload function needs fleshed-out. Using the Phaser game object, I can load the images I'll need for the sprites, set up the Isometric plugin, and start the Isometric physics system. I put the following code in the preload function:

game.load.image('tree2', 'images/tree2.png');
game.load.image('ball', 'images/ball.png');
game.load.image('tile', 'images/ground_tile.png');
game.load.image('frog','images/frog.png');
       
// Add the Isometric plug-in to Phaser
game.plugins.add(new Phaser.Plugin.Isometric(game));

// Set the world size
game.world.setBounds(0, 0, 2048, 1024);

// Start the physical system
game.physics.startSystem(Phaser.Plugin.Isometric.ISOARCADE);

// set the middle of the world in the middle of the screen
game.iso.anchor.setTo(0.5, 0);

Isometric ground tile
Loading images is fairly straightforward. The first parameter is the key to use later when making sprites and the second parameter is the path to the image. I have tree, ball, frog, and ground tile images. After that, it's just a matter of adding the plugin, setting the physics system, and setting up the world size.

Load the page and you won't see anything on the screen yet. The next thing to do is to create the game elements, which happens in the appropriately-named create function. There's a little more to this function. Here is the code I used:

// set the Background color of our game
game.stage.backgroundColor = "0x409d5a";

// create groups for different sprites
floorGroup = game.add.group();
obstacleGroup = game.add.group();

// create the floor tiles
var floorTile;
for (var xt = 1024; xt > 0; xt -= 35) {
    for (var yt = 1024; yt > 0; yt -= 35) {
        floorTile = game.add.isoSprite(xt, yt, 0, 'tile', 0, floorGroup);
        floorTile.anchor.set(0.5);
    }
}

var tree1 = game.add.isoSprite(500, 500, 0, 'tree2', 0, obstacleGroup);
tree1.anchor.set(0.5);
game.physics.isoArcade.enable(tree1);
tree1.body.collideWorldBounds = true;
tree1.body.immovable = true;

var ball = game.add.isoSprite(600, 600, 0, 'ball', 0, obstacleGroup);
ball.anchor.set(0.5);
game.physics.isoArcade.enable(ball);
ball.body.collideWorldBounds = true;
ball.body.bounce.set(0.8, 0.8, 0);
ball.body.drag.set(50, 50, 0);
        
// Set up our controls.
this.cursors = game.input.keyboard.createCursorKeys();

this.game.input.keyboard.addKeyCapture([
    Phaser.Keyboard.LEFT,
    Phaser.Keyboard.RIGHT,
    Phaser.Keyboard.UP,
    Phaser.Keyboard.DOWN
]);

// Creste the player
player = game.add.isoSprite(350, 280, 0, 'frog', 0, obstacleGroup);
player.anchor.set(0.5);

// enable physics on the player
game.physics.isoArcade.enable(player);
player.body.collideWorldBounds = true;

game.camera.follow(player);

The first line sets the game's background color. This is what you see covering the whole Canvas element when there is nothing else drawn there. I use a kind of green color, slightly different from my ground tile so you can see the difference.

Next, I create groups for the different sprites. Grouping them makes it easier to manage different types of objects. The ground tiles do a lot less than the tree and ball, for example. And speaking of the ground tiles, that's the first thing I add. In a pair of loops, I just cover the entire world area with them. The more interesting parts are next. I create a tree sprite by calling game.add.isoSprite and give it the x, y, and z coordinates along with the id of the image, frame index (always 0 because I don't have any animation yet) and the group for this object. For both the tree and ball objects, I enable the isoArcade physics and enable colliding with the world bounds. I don't want the ball being kicked out of the world! The ball as two additional settings, bounce and drag. They are exactly what they sound like - telling the physics engine that the ball should bounce a certain amount when hitting another object and that it should have decreased friction to roll around on the grass.

Setting up player controls is relatively simple as well. Phaser has built-in support for capturing the cursor keys and using them as game input. This just sets up the capture, using them will be in the update function.

Finally, the create function makes the player sprite and sets up its physics much like the tree and ball. The camera is set to follow the player on the last line. This will keep the frog in view as you move around the field.

If you view the game now in your web browser, you should actually see something! But you can't move yet, because we still have to do the update function!

A Frog Soccer Game? Maybe...
The contents of the update function are very simple. Just check for a cursor key pressed and change the velocity of the player. The Phaser engine takes care of the rest!

// Move the player
var speed = 100;

if (this.cursors.up.isDown) {
    player.body.velocity.y = -speed*2;
    player.body.velocity.x = -speed*2;
}
else if (this.cursors.down.isDown) {
    player.body.velocity.y = speed*2;
    player.body.velocity.x = speed*2;
}
else {
    player.body.velocity.y = 0;
    player.body.velocity.x = 0;
}

if (this.cursors.left.isDown) {
    player.body.velocity.x = -speed;
    player.body.velocity.y = speed;
}
else if (this.cursors.right.isDown) {
    player.body.velocity.x = speed;
    player.body.velocity.y = -speed;
}

game.physics.isoArcade.collide(obstacleGroup);
game.iso.topologicalSort(obstacleGroup);

One weird thing here is that I set both x and y velocity for each cursor direction. This is a personal preference and side-effect of the Isometric plugin. In the Isometric view, moving on the X-axis alone moves the player both up/down and left/right but at a 45-degree angle. Same thing for Y-axis movement, but the slope is reversed. It makes sense when you think about it, but I prefer the player motion to match the cardinal direction of the keys. To correct for this, I set both the x and y velocities in each case. The only other things that happen in this function are the collision checks and topological sprite sort at the bottom. These are things that the Isometric plugin takes care of for you - you just have to call them.

One note on that topological sort, however. There are some bugs in it. See this issue on GitHub: custom isoBounds proportions #11. I encountered this myself when I used my other tree image due to its larger dimensions. The topological sort for a 2D projection of 3D space in HTML5 Canvas is a difficult thing to get correct. I know because I had to implement it myself once for a JavaScript game. I'm going to try to figure out what's wrong here and fix it...I really want to use that other tree!

So the only function I haven't touched yet is render. In most cases you can leave it empty, but if you want to do something special with the rendering of the scenes there are some options. This code, for example, will draw some bounding boxes around your sprites for debugging purposes:

obstacleGroup.forEach(function (tile) {
    game.debug.body(tile, 'rgba(189, 221, 235, 0.6)', false);
});


That's what I have so far. The frog can move around the play area and kick the ball. Collisions with the tree, ball, and world edges seem correct. It plays ok on my desktop but the frame rate is not all that good on my phone. I think it might be the Isometric plugin that really slows it down, because most of the Phaser demos performed quite well on mobile. I can't really say if I'm totally sold on Phaser yet...I might have to try making something without the Isometric plugin to see if I really like it.

I need to work on animations next, and look into that topological sort bug some more. I don't have this up anywhere to play yet publicly, but that should come soon. In the mean time, take this code and set up your own game!

And don't forget today's comic!

Amphibian.com comic for 13 July 2015

No comments:

Post a Comment