DEV Community

Chris Wright
Chris Wright

Posted on • Updated on

Loading server-generated Tilemaps with Phaser

Phaser is a fantastic framework for creating games with web technologies. There are many features built in that make creating games a breeze. One of my favourites is Tilemaps. Whether you're developing a side-scrolling platformer or an epic turn-based RPG, you'll likely need to take advantage of Tilemaps to render your level. I'd like to touch briefly on how to work with them and then demonstrate how to leverage an API to dynamically serve level information.

Note: This article contains a brief primer for working with Loaders and Tilemaps in Phaser. If you're already familiar with these concepts, feel free to jump to the good stuff.

File Loaders

Phaser makes it incredibly easy to load a variety of file types into your game using Loaders. There are many built-in Loaders for primitive file types like images and audio, as well as for custom Phaser objects like Spritesheets, Plugins, and Tilemaps. We'll be focusing on the Tilemap Loader for our purposes, but feel free to check out all of the possible Loaders in the Labs.

Each time you invoke a Loader, you are required to pass in a key and a path to the file (there are exceptions to this but let's pretend that's not the case right now). The key must be unique as it will be used to reference the loaded asset later on. Below is an example of what this may look like (using a map exported from Tiled):

function preload () {
    this.load.tilemapTiledJSON('map', 'data/map.json');
}
Enter fullscreen mode Exit fullscreen mode

You can load as many assets as required by your game, though this will impact your load time. Be sure to optimize your assets as much as possible so you don't keep your players waiting.

Note: Although it is technically possible to load assets anywhere in your game, it is recommended that you load all of your assets in your Scene's preload method as the loading process will be automatically started for you. If you choose to do so elsewhere, be sure to call the Loader.start method. For more information, check out the LoaderPlugin docs.

Creating a static Tilemap

Before we're ready to create our first Tilemap, we'll first need to load in a Tileset. We can expand our preload method to include our Tileset asset:

function preload () {
    this.load.image('tiles', 'assets/tileset.png');
    this.load.tilemapTiledJSON('map', 'data/map.json');
}
Enter fullscreen mode Exit fullscreen mode

Now we're ready to go!

Once again, Phaser is looking out for us with its amazing built-in support for working with Tilemaps. The possibilities are endless but let's concentrate on the basics for the time being. Don't blink while reviewing the following snippet; you might miss it:

function create () {
    const map = this.make.tilemap({
        key: 'map',
    });
    const tileset = map.addTilesetImage('tileset', 'tiles');
    const layer = map.createStaticLayer(0, tileset);
}
Enter fullscreen mode Exit fullscreen mode

And that's really all it takes to create a basic Tilemap in Phaser. First, we make a new Tilemap (note the key corresponding to our JSON file), add a tileset using our image, and create a static layer. You should now see your map in your Scene.

We've glossed over many aspects of working with Tilemaps in order to get to the real meat of this article. I definitely recommend you check out the comprehensive list of demos over in the Labs, including examples of how to handle things like collision and multiple layers. If you're feeling particularly adventurous, try your hand at Dynamic Tilemaps.

Loading server data

Sometimes you may not want to use a single map in your game. Maybe you want the user to be able to select from a large pool of available maps but don't want to bloat your bundle size. Or maybe you want to cycle maps out on a regular interval but don't want to force the user to download constant updates every time. We can solve these problems by having maps downloaded from a server on demand.

Remember the exception earlier where we noted that you must provide a file path to the Loader? As it turns out, you're not limited to just linking to static files. You could, for example, build an API that returns the required JSON. Let's explore that now.

Note: I'll be using micro to quickly bootstrap an API but you can use anything you'd like.

We'll need the most barebones project you've ever seen. Create a new project and include a copy of your static map JSON. You should end up with a structure similar to this:

|- index.js
|- map.json
|- package.json
Enter fullscreen mode Exit fullscreen mode

Your index.js should look like the following snippet. If you're using a different setup then be sure to do the equivalent. The idea at this point is just to read in the static file and return its contents with the request.

const map = require('./map.json');

module.exports = (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*'); // You should probably change this

    return map;
}

Enter fullscreen mode Exit fullscreen mode

Finally, start the engines:

npx micro
Enter fullscreen mode Exit fullscreen mode

If all is well, you should be able to visit your API and have the map data be returned to you. Next, we'll need to update our preload method:

function preload () {
    this.load.image('tiles', 'assets/tileset.png');
    this.load.tilemapTiledJSON('map', 'http://localhost:3000'); // Be sure to update the URL with your own
}
Enter fullscreen mode Exit fullscreen mode

You should still see your map, exactly as you had before. Let's spice it up a little bit.

Choose your own adventure

The next logical step is to be able to load different levels depending on user action (e.g., selecting a level from a menu, progressing to the next after beating a level). Updating the API to support this will be trivial with our current setup. Let's update our API to accept a level selection as part of the query string.

const qs = require('qs');
const url = require('url');

const level1 = require('./level1.json');
const level2 = require('./level2.json');

module.exports = (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');

    const {
        level = 1,
    } = qs.parse(url.parse(req.url).search, {
        ignoreQueryPrefix: true,
    });

    if (level === 2) {
        return require('./level2.json');
    }

    return require('./level1.json');
}
Enter fullscreen mode Exit fullscreen mode

Admittedly, this continues to be a rather naive implementation, but it demonstrates the basic concept. Our API now accepts a level to load. At the moment, the only level we can request is level 2, as everything else will default to the first level. Now, how can we use this in our game?

Scenes in Phaser can be initialized with data when they're started. We can then store that data for later use. In the example below, I've opted to use the registry to store the data, but you can use whichever approach you prefer.

function init ({ level }) {
    this.registry.set('level', level);
}

function preload () {
    // ...

    const level = this.registry.get('level');
    this.load.tilemapTiledJSON('map', `http://localhost:3000?level=${level}`);
}
Enter fullscreen mode Exit fullscreen mode

Note: No default is required here; the API will take care of that.

The last piece of the puzzle is to trigger the level load. The first time our game is run, the level stored in the registry will be undefined so the API will know to return the first level for us. In our hypothetical situation, let's assume the user has completed the level and is now shown a prompt to continue to the next level.

this.input.keyboard.once('keyup_SPACE', () => {
    const level = this.registry.get('level');

    this.scene.restart('level', {
        level: level + 1,
    });
});
Enter fullscreen mode Exit fullscreen mode

The Scene will now restart with the next level set. Alternatively you could set level to 1 in the case of a game over scenario, or even transition to a random bonus level on item pickup.

Conclusion

Traditionally you would have to ship your game with all of its level data bundled in. With this technique, that doesn't always have to be the case. I don't think every game should be architected this way, but it could be useful for those that have a level editor, utilize procedural generation in some way, or offer cross-device play.

Thank you for taking the time to join me on this adventure! I've been wanting to try my hand at writing an article for years and it's never quite worked out until now. Let me know what you think in the comments or over on Twitter.

Latest comments (0)