Saturday, June 27, 2015

Tilemaps with invisible collision layer in Phaser

Phaser, Tiled and Physics

While there are several TileMap tutorials for Phaser out there, I didn't find any using Phaser 2.3 with the P2 physics. For this reason I decided to write my own tutorial about. Phaser is a cool javascript library that for turning a HTML5 canvas elements into games.

Goal

Our goal in this tutorial is to use the program Tiled to create a map for our game. We will have a separate layer for collision data. In phaser we will display the background and foreground layer, but use the invisible collision layer for collision. We will use P2 physics, as some comments suggest that this physic works best with tilemaps.

Creating the map in Tiled - Howto 2.5D

We will use the program Tiled to create the map. Tiled can be downloaded from www.mapeditor.org. Alternatively, there are PPAs for Ubuntu and Fedora on launchpad. Of course we need graphic for our map. I painted all graphics for my game "Tales - Told & Untold" myself using watercolors and then scanned the pictures. I put the tiles used for this tutorial on opengameart.org, where it can be downloaded under a CC-BY license.
In Tiled, we first create a new Map.
We choose "Orientation": "Orthogonal", "Layer format": "CSV", "Map size": 10x10 tiles and "Tile size" 50x50px

Then we need to load these images by clicking Map -> New Tileset.... A dialogue opens where we can upload the picture and enter the size of our tiles. (In my case 50x50).
We do this 3 times to load all 3 different pictures.
In the bottom right of the Tiled window, we can see the 3 tilesets. We can choose Tiles from the tileset and paint them onto our map.

Once the background of our map is complete, we create a new Tile Layer (in the Layers window in the upper right of the Tiled window.) Make sure to select this layer before we continue to paint. From the Tree Tileset, we paint the lower parts of the tree onto the new layer.


 Then we create a third layer (called "Foreground"), select it and paint the upper part of the tree on it. (By clicking the check boxes next to the Layer names, we can show and hide the layers. Be careful: Tiled allows you to paint on an invisible layer (but you cannot see the effect of your painting until you show the layer again). Always make sure the correct layer is selected before painting.


Collision from Tiled to Phaser

There are several ways to save collision data and use it in Phaser. You could attach a property to those tiles which the player cannot pass. In javascript you could read this property and give the corresponding index to Phaser's physic engine. Or you could simple memorize the indices used for collision.
In our case, we could use all tiles on the BackgroundOverlay layer for collision (except the transparent area with id 0), but that reduces our flexibility.

If you want to use many different visible tiles for collision, check out the botanist game blog for a solution to reduce stage loading time. However, we will use a different approach:

For Tales - Told & Untold I use many different tiles which often occur only once on the map. Thus it is much easier for me to use a separate, invisible layer for collision data. The graphic for the collision tile does not matter - I use an "X" to make painting collision data in Tiled easier.


When we save our tilemap, we should choose "json" as format.

For the Player we will use only a simple picture with the size 100x150px.

Setting up the stage in Phaser

Now we come to javascript code with Phaser.
In the index.html file we need to include Phaser with a script tag. Further more we use an element with unique id - here <div id="main">. This is where Phaser will add a canvas element.

<html lang="en">
<head>
 <meta charset="UTF-8" />
 <title>Tutorial: Invisible Collision Layer</title>
 <script  src="phaser.js"></script>
</head>
<body>
  <div id="main">
  </div>
</body>
</html>

Next we use the following javascript:

var game = new Phaser.Game(500, 500, Phaser.CANVAS, 'main')
stateTestmap = {preload: preload,create: create, update: update};
game.state.add('stateTestmap', stateTestmap);
game.state.start('stateTestmap');
 The first line starts the new Phaser game instance. 800,400 is the size of the game window, Phaser.CANVAS tells phaser to render using CANVAS instead of WebGL. While this in theory should be slower, it is definitely much faster in FireFox (see also thebotanistgame.com).
'main' is the id of the div where Phaser will add the canvas.

In the 2nd line we create a game state. "preload", "create" and "update" on the right of the colon are functions which we still have to write.

Preload

 
function preload() {
  game.load.tilemap('Map1', 'PATH/TO/FILE', null, Phaser.Tilemap.TILED_JSON);
  game.load.image('TilesetBG', 'PATH/TO/FILE');
  game.load.image('TilesetTree', 'PATH/TO/FILE');  
  game.load.image('player', 'PATH/TO/FILE');
}


In the preload function, we load all resources which we will later need. In our example we load the tilemap json file, two tileset images and one sprite for the player.
The first parameter is a name by which we will reference the resources in the future.

Create

After preloading, we create the actual game world.
function create() {
  map = game.add.tilemap('Map1');
  map.addTilesetImage('TilesetRiver', 'TilesetBG');
  map.addTilesetImage('TilesTree', 'TilesetTree');
  background = map.createLayer('Tile Layer 1');
  backgroundOL = map.createLayer('BackgroundOverlay');
  player = game.add.sprite(100,100, 'player');
  foreground = map.createLayer('Foreground');
  background.resizeWorld();
  //Physics        
  game.physics.startSystem(Phaser.Physics.P2JS);
  //First we choose which tiles will collide
  map.setCollision(42, true, "Collision");
  //Then the physics engine creates collision bodies from the tiles:
  game.physics.p2.convertTilemap(map, "Collision");
  game.physics.p2.enable(player);
  player.body.fixedRotation = true; // no rotation
  //Remove default collision box
  player.body.clearShapes();  
  //Only the lower part of the player collides
  player.body.addRectangle(100, 100, 0, 25);
  //Controls
  cursors = game.input.keyboard.createCursorKeys();
  player.body.debug = true;
}

First we create a map from the loaded tilemap (from the json file). Then we add the images which are referenced in the tilemap.
Finally, we create the individual layers. The lowest layer is created first, each subsequently created layer overlays the previous one.
We create the player between the backgroundOL layer (lower parts of the trees) and the foreground layer (higher parts of the trees)
After starting the P2 physics system, we first set tile 42 to collide on our collision layer and then convert the "collision"-layer of the  tilemap to collision bodies for the physics engine.
Note that '42' in that case is the global tile id (gid) of the tile which we choose to mark collision. Tiled assigns a global tile id to each tile. The first tileset image we use in tiled has ids starting with 1 (id 0 is reserved for "no tile"). After all tiles in the first image are numbered, the global indices  continue with the next image.
By manually inspecting the JSON file, we can see the "firstgid" property of each index. The "firstgid" of an image is the gid of the upper left tile.
Instead of manually inspecting the JSON file, we could have loaded our collision tileset as the first tileset image in Tiled, to make sure the collision index is always 2.
We add a collision rectangle to the player by calling "player.body.addRectangle". To create the illusion of a third dimension, the upper part of the player can overlay tiles (on the Background or BackgroundOverlay layer) even if these tiles have collision enabled. However, the part of the player that does not collide must not be higher than one tile. The first two arguments we pass to this function are the size of the bounding box. The third and fourth are the x- and y-offset. The offset describes how much the center of the bounding box is moved from the center of the player sprite.
By setting player.body.debug = true, we can make the bounding box visible.

Update

The update function is called at every frame of the game (usually 60 frames per second).
function update() {
  var speed=300;
    if (cursors.left.isDown){
        player.body.velocity.x = -speed;
    } else if (cursors.right.isDown){
        player.body.velocity.x = speed;
    } else {
        player.body.velocity.x = 0;
    }
    if (cursors.up.isDown){
        player.body.velocity.y = -speed;
    } else if (cursors.down.isDown){
        player.body.velocity.y = speed;
    } else {
        player.body.velocity.y = 0;
    }
}
Using the global variable (not good coding style) "cursors" we have defined before, we can set the players speed if arrow keys are pressed.

Let's play

That's it. Time to play

A nasty error

If your game does not work and the following error appears on the console (e.g. Firebug):
"TypeError: Argument 1 of CanvasRenderingContext2D.drawImage could not be converted to any of: HTMLImageElement, HTMLCanvasElement, HTMLVideoElement."
it could mean that one tile from the 3rd tileset image (used for collision) was accidentally placed on a visible layer. As we didn't load this image in the Phaser preload function, of course Phaser cannot display it. Tiles can be accidentally placed on the wrong layer easily in Tiled, because Tiled allows to paint on invisible layers. Further more accidentally painting an invisible tile from an image we didn't load in Phaser can easily lead to this error. Inspecting the tilemap JSON file can help to debug if you run into this issue.

11 comments:

  1. Hei, thank you for the tuto. when I try to complet it I got the error : "Uncaught ReferenceError: placeMenuLOCK is not defined"

    Did I forget something ?

    ReplyDelete
    Replies
    1. I deleted the functions in the update function. But, the collision doesn't work. Humm

      Delete
    2. I am sorry, that was copy-paste error in my update function. I changed the code in the tutorial, it should work now.

      Delete
    3. If you still have any problems, please just ask

      Delete
  2. I learned som much from this thank you!

    ReplyDelete
  3. I got another error message when parsing the tilemap.json:

    Uncaught TypeError: b.trim is not a function

    Any idea? Thanks

    ReplyDelete
  4. Hello, nice post.
    One question. Why on function Create on line 13 (map.setCollision(42, true, "Collision");) is number 42?
    Why 42?
    Thanks

    ReplyDelete
  5. the generated index id of tile used for collisionlayer in the json file...

    ReplyDelete
  6. How can I do callback functions, for example, to the hero loses a life when he is collide with a tree?

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. For reference, if you're using the Arcade Physics system, you need to create a layer, but you can safely make it invisible and not even associate any tileset; all the rendering code will be skipped. Example below, assing that you have a tilemap (preloaded as 'bg') with a BG layer and a Collisions (invisible) layer:

    ```const map = this.game.add.tilemap('bg');
    map.addTilesetImage('tileset', 'bg_tiles'); // tileset for visible map
    const layer = map.createLayer('BG'); // BG is the visible layer
    layer.resizeWorld();

    // We have a second layer, Collisions, and its only tile for now needs to collide
    map.setCollision(162, true, 'Collisions');

    // Creates an invisible layer
    const collisionLayer = map.createLayer('Collisions');
    collisionLayer.visible = false;
    this.game.physics.arcade.enable(this.collisionLayer);

    update() {
    this.game.physics.arcade.collide(this.player, collisionLayer);
    }```

    ReplyDelete