« Back to Blog

Phaser Platformer Series: 25 Lighting and Enhancements

posted on 10 September 2021

In this tutorial we’ll be adding some visual enhancements to our game. There is some juicing to be done here! We’re going to add some more animation to the coins, add in some background elements to our tilemap, animate some tiles and also add some lighting. Here’s what we’ll be making:

See the Pen
Phaser Platformer Series: 25 Lighting and Enhancements
by Digitherium (@digitherium)
on CodePen.

If you are viewing on a mobile, you can open a version of it here without all the codepen chrome taking up screen space so you play it in landscape mode.

The first thing we’ll do is edit our preload function to load our new tilemap, new sprites and tiles:

this.load.image('background-tiles', '/map/environment-background.png');
this.load.tilemapTiledJSON('map', '/map/enhancedmap.json');

this.load.spritesheet('lava',
    'map/lava.png', { frameWidth: 32, frameHeight: 32 }
);

this.load.spritesheet('torch',
    'torch.png', { frameWidth: 16, frameHeight: 32 }
);

this.load.spritesheet('eye-left',
    'eye-left.jpg', { frameWidth: 16, frameHeight: 16 }
);

this.load.spritesheet('eye-right',
    'eye-right.jpg', { frameWidth: 16, frameHeight: 16 }
);

We’ve got new tilemap JSON, a new tileset of background tiles and also some spritesheets for animations that we’ll dig into later.

Next, we’ll add some lighting. We create a new function called setupLights and call it from the create function after calling setupTilemap:

gameScene.setupLights = function() {
    this.lights.enable();
    //set ambient colour whole scene. This is set to quite dark for contrast with lights
    this.lights.setAmbientColor(0x666666);

    //light at entrance to level
    this.lights.addLight(0, 340, 300).setColor(0xffffff).setIntensity(3.0);
    //lava lights
    this.lights.addLight(440, 360, 100).setColor(0xeb840b).setIntensity(1.0);
    this.lights.addLight(540, 360, 100).setColor(0xffea00).setIntensity(1.5);
    this.lights.addLight(640, 360, 100).setColor(0xffea00).setIntensity(1.5);
    this.lights.addLight(740, 360, 100).setColor(0xeb840b).setIntensity(1.0);

    //bright white light on snakes statue
    this.lights.addLight(1280, 300, 300).setIntensity(2.0);
    //exit light next to particle effects
    this.lights.addLight(2192, 60, 300).setColor(0xfad050).setIntensity(4.0);
    //small lava pit lights
    this.lights.addLight(2032, 400, 80).setColor(0xffea00).setIntensity(3.0);
}

Lights are enabled, and then an ambientColor is set. This is like a general background lighting colour that is cast across the whole level. Individual lights are then added with x and y coordinates and a radius in px. We can also change the colour of each light with setColour and how strong the light is with setIntensity.

We could place these lights in an object layer in Tiled, but as there is no equivalent sprite or image to represent the light, we can’t really use Tiled to visualise how the lights will look. It’s just easier to place them manually in code as we need to keep moving them around and changing the intensity and radius settings until we are happy with the look.

The first light sits at the beginning of the level (hence the 0 x coordinate). It makes it look like daylight seeping into a cave which suits our level well. The next four lights are placed on top of the lava giving it a nice glow. There there are a couple more lights in the background, one more for the second lava pit glow and finally one for the end square to try and make it look more like an exit.

Those lights are now positioned, but we need to tell Phaser what sprites should be affected by them. We do this using

setPipeline('Light2D')

We can use this on tile layes and sprites. For instance when we create the player sprite in placePlayer we just add this line:

this.player = this.physics.add.sprite(tile.x + (tile.width / 2), tile.y - (tile.height / 2), 'hero-small');
this.player.setPipeline('Light2D');

We add the lighting to the baddies too. For each baddie we create in our placeBaddies function, we add the line:

baddie.setPipeline('Light2D');

In our setupTilemap function we need to turn this on for each layer we want affected like this:

this.ground = this.map.createLayer('platforms', tiles).setPipeline('Light2D');

Any tile layer or sprite that we want to be affected by lighting, we have to add this setPipeline function. We have to be careful here though, as it starts to really hammer performance if there are too many sprites. I tried putting it on the ladders, but as they are sprites individually placed and there are lots of them it really noticably slowed down mobile. Any sprite we don’t set the lighting on will visible stand out a bit as it won’t be as dark, so it’s something of a balancing act.

The idea with the lighting was a nice bit of atmosphere, so whilst adding it to the ladders would look nice, it’s not essential. The main thing is the effect of the player and the baddies walking from shadow into light, and changing colour slightly because of the sheer brightness of the lava. So we’ll add lights as much as possible without slowing down the performance. With a bit of experimentation, I found that means adding it to powerup boxes, shells and the torches (we’ll get to the torches shortly).

Ansimuz’s lovely tileset comes with some sprite sheets for some nice animations – bubbling lava, and some flickering torches. These are perfect for more atmosphere, and we can even pair up these things with the lighting to really bring it to life.

In tiled we create another object layer for ‘bubbling-lava’ and ‘torches’. We load in some new tilesets and just use the sprite sheets for lava and the torch. We then place torch sprites on our ‘torches’ object layer, and bubbling lava all over the top part of our existing lava tiles in it’s own ‘bubbling-lava’ object layer.

We can then load in these object layers like all the rest. We’ll first need to load in the new spritesheets in our preload function:

this.load.spritesheet('lava',
    'map/lava.png', { frameWidth: 32, frameHeight: 32 }
);

this.load.spritesheet('torch',
    'torch.png', { frameWidth: 16, frameHeight: 32 }
);

With a spritesheet we specify the width and height of each frame as well as the path to the image. Next in create we call our new function createLava:

gameScene.createLava = function() {
    //define our bubbling lava animation, it is 3 frames long
    this.anims.create({
        key: 'bubbling',
        frames: this.anims.generateFrameNumbers('lava', { start: 0, end: 2 }),
        frameRate: 3,
        repeat: -1
    });

    //place the sprites for the bubbling lava
    this.bubblingLava = this.physics.add.staticGroup();
    this.bubblingLavaLayer = this.map.getObjectLayer('bubbling-lava');
    this.bubblingLavaLayer.objects.forEach(element => this.placeLavaBubbles(element));
}

We define a new animation and call it ‘bubbling’. We tell it to use our lava sprite, and that it has 3 frames (start at 0, and end at 2). We set the speed of the animation with frameRate. We set this to 3 which is very slow, if you enter a higher number the animation will be faster.

For each tile we find in the ‘bubbling-lava’ object layer we call placeLavaBubbles:

gameScene.placeLavaBubbles = function(tile) {
    if (tile.index != -1) {
        //place sprite
        var bubbles = this.bubblingLava.create(tile.x + (tile.width / 2), tile.y - (tile.height / 2), 'lava');
        //reduce size of hit area
        bubbles.body.setSize(32, 16);
        //start animation
        bubbles.anims.play('bubbling', true);
        //set depth so player sits behind the bubbls
        bubbles.depth = 2;
        //turn on lights for this sprite
        bubbles.setPipeline('Light2D');
    }
}

We add the sprite as usual, but then we change the size of the physics body. We do this so if the player falls in the lava it looks like they make contact with it and aren’t bouncing off the transparent part of the image. We then start ‘bubbling’ animation and turn on the lighting for this sprite.

Next up, we’ll add a background tile layer using some more of Ansimuz’s tileset. There are some nice background tiles in there, so we’ll boot up Tiled and add a new tile layer and use the background tiles. Our setupTileMap function ends up looking like this:

//create layers from their names in tiled. 
this.background = this.map.createLayer('background', bgTiles).setPipeline('Light2D');
this.ground = this.map.createLayer('platforms', tiles).setPipeline('Light2D');
this.lava = this.map.createLayer('lava', tiles).setPipeline('Light2D');
this.foreground = this.map.createLayer('grass', tiles);
//change depth so this layer sits in front of everything
this.foreground.depth = 10;

//setup collisions for each tile layer we want. 0,600 are tile indexes
//setCollisionBetween(start, stop [, collides] [, recalculateFaces] [, layer])
this.map.setCollisionBetween(0, 600, true, false, 'platforms');
this.map.setCollisionBetween(0, 600, true, false, 'lava');

We are only setting collisions on the ‘platforms’ and ‘lava’ layers – everything else is just eye candy. Lighting it turned on for the background, platforms, and lava layers. Some of Ansimuz’s tiles make up a sort of Aztec God style face with torches for eyes which is hella’ cool so had to make it into the level. There are also some animated torch sprites with flickering flames. I wanted to combine these with lighting so it looks like the torches are where the light emits from.

The Aztec God statue flame eyes were just flat images, so I did some cutting and pasting and repurposed the torch flame frames so the eyes were also animated. The God head is just sat on the ‘background’ layer in Tiled, but then each eye is also a separate object layer ‘left-eye’ and ‘right-eye’.

We load in all our torch sprites, and place lights in right place by calling a new function createTorches, from our create function like this:

gameScene.createTorches = function() {
    //standard torch flicker
    this.anims.create({
        key: 'flicker',
        frames: this.anims.generateFrameNumbers('torch', { start: 0, end: 2 }),
        frameRate: 3,
        repeat: -1
    });

    //right eye
    this.anims.create({
        key: 'flicker-right',
        frames: this.anims.generateFrameNumbers('eye-right', { start: 0, end: 2 }),
        frameRate: 3,
        repeat: -1
    });

    //left eye
    this.anims.create({
        key: 'flicker-left',
        frames: this.anims.generateFrameNumbers('eye-left', { start: 0, end: 2 }),
        frameRate: 3,
        repeat: -1
    });

    //grab torches layer from Tiled data and place torches
    //placeTorches expects the following:
    //object, sprite name, light radius, animation key
    this.torchesLayer = this.map.getObjectLayer('torches');
    this.torchesLayer.objects.forEach(element => this.placeTorches(element, 'torch', 100, 'flicker'));

    this.leftEyeLayer = this.map.getObjectLayer('eye-left');
    this.leftEyeLayer.objects.forEach(element => this.placeTorches(element, 'left-eye', 150, 'flicker-left'));

    this.rightEyeLayer = this.map.getObjectLayer('eye-right');
    this.rightEyeLayer.objects.forEach(element => this.placeTorches(element, 'right-eye', 150, 'flicker-right'));
}

Just like the lava, we start by defining the animations. Each animation only has 3 frames, so other than the image they are using, they are all the same. We grab the object layers for the torches and the statues torch eyes and call a new function placeTorches. As well as passing this the object data to position our sprite, we also pass in the sprite key, a radius to use for our light, and the animation to use. placeTorches looks like this:

gameScene.placeTorches = function(tile, sprite, radius, animationKey) {
    if (tile.index != -1) {
        //create light at the top of the tile. Use the passed in radius
        this.lights.addLight(tile.x + (tile.width / 2), tile.y - (tile.height), radius).setIntensity(2.0);
        //Place the sprite itself. Use the key passed in
        var torch = this.add.sprite(tile.x + (tile.width / 2), tile.y - (tile.height / 2), sprite);
        //kick off animation
        torch.anims.play(animationKey, true);
        //turn on lights
        torch.setPipeline('Light2D');
    }
}

This works just like our placeLavaBubbles function, except we also create a light along with the torch.

Finally, just to draw a bit more attention to the coins, we’ll animate them so they are constantly rotating. We edit placeCoin so it has a rotation tween:

gameScene.placeCoins = function(tile) {
    if (tile.index != -1) {
        //object layer
        var coin = gameScene.coins.create(tile.x + (tile.width / 2), tile.y - (tile.height / 2), 'coin');
        this.tweens.add({
            targets: coin,
            angle: 360,
            repeat: -1,
            ease: 'Linear',
            duration: 3000
        });
    }
}

And there we have it. We’ve got a much more complete level, with some nice background visuals in. We’ve got much more exciting lava that bubbles and glows (have a go at walking across the bridge and notice how our hero now lights up). We’ve got flickering torches that cast light, a spooky godhead, and some lovely spinning coins.

chromeless mobile version
view all the code on codepen
download the source on github.