All About HTML5 Game Development, from Publishing to Monetization

Parallalax effect in HTML5 Games with ExcaliburJS engine - 4/11

Parallalax effect in HTML5 Games with ExcaliburJS engine - 4/11

This is an open-source project on game development with a focus on the HTML5 Excalibur game engine. Check out this feature implementation on this commit in our Github repository!

You can read here the list of features we are going to develop for our Excalibur game, such as data persistence between scenes and between game sessions. If you're interested in game development, follow us on Twitter and Instagram to be caught up on our latest news!

Table of Contents

In this tutorial

In this tutorial, we set up an infinite horizontal map where our main character can move. We will complete the set-up with a horizontal parallax effect.

In the last tutorials we've added a MotionService to move our character, and we wrote a couple of Karma tests for it. Now it's time to make some space for the character to move. This tutorial is split into two parts. In the first section, we understand from a theoretical point of view what we need to implement, without writing any line of code. In the second section, we'll see how the theoretical aspects can be implemented in Excalibur.

Introduction

Tiled Editor: why not?

The Tiled editor is a useful tool to create maps and levels. Excalibur has a plugin to read Tiled maps which works well. The Tiled documentation is complete as it is the Excalibur one. In this tutorial, we want to explore a different topic, which is the creation at runtime of infinite levels. At the end of this tutorial, we'll have a skeleton ready to create horizontal levels as long as we wish, which are created programmatically and thus do not need to be statically prepared in Tiled and loaded through Excalibur.

The parallax effect: suggested reading

In this tutorial we're going to implement the parallax effect in our Excalibur scene. We've taken inspiration from this article, which we suggest if you want to read a good introduction to this topic. In this tutorial, we will assume you know about the parallax effect.

Part one: theoretical aspects

What we are going to implement? The idea in images

The basic idea is as follows:

That is, we would like to set up a horizontal scene featuring:

  • A sense of depth which is given by the layer 1 and layer 2 that forms the background environment, visible in the distance. They are built based on the parallax effect. Layer 1 is further away than the second, and thus moves more slowly concerning the player.
  • Layer 3 represents the solid base on which the character can move
  • The scene camera follows the character along the X axis.

Following the suggestions in the gamedevelopment.tutsplus.com tutorial, we know we need to develop a structure like this one:

Build an infinite level with parallax effect in Excalibur -3

As you can see in the image, we are going to build each layer as a collection of tiles. Every layer will have a different velocity with respect to the player one, resulting in the sense of depth given by the parallax effect.

The Layer 3 is not strictly related to the parallax effect, but represents the solid base on which the player moves. Although it is not part of the parallax effect, we will build it similarly.

How to make this in Excalibur?

We're going to code the layer tiles as Excalibur Actors. Each tile will have its graphics and event listeners. Among the events, as we will see in a minute, we'll be using the exitviewport hook to create an infinite scene.

The Layer 3 tiles will have a solid body so that the player can walk on them.

How to make the scene infinite?

As we were writing a few lines above, we'll render an infinite scene thanks to the exitviewport Excalibur event which is generated by the Actor objects exactly when they leave the viewport. Remember that in our configuration the viewport is bound to the camera, which follows the player along the X axis.

What we want to achieve is building an infinite scene using a finite amount of tiles. We'll use again a sketch to explain what we're going to code:

Build an infinite level with parallax effect in Excalibur - 4

As soon as a tile leaves the viewport, we change its position along the x-axis by moving it after the tile to the far right of the tile queue. In this way, we build an infinite layer programmatically, without a priori building a map in the Tiled editor.

We'll use this mechanism to build all the Layers (1,2 and 3). The differences will be in the number of tiles and their velocities. Since Layer 1 is far away with respect to the player, it will move more slowly, and we'll use just a couple of big tiles. On the other hand, Layer 3 moves at the player speed, so to achieve a dynamic effect we'll use around 8-9 smaller tiles. Layer 2 stays in the middle of the other layers and thus will have 4-5 tiles and an intermediate speed.

How to make it work on different viewport sizes?

We want to release our game on different platforms so that it can be played both on a browser and Android devices. We need to adapt the tiles to the various viewport sizes. This aspect is quite easy since it's enough to compute the tile's size (in the horizontal case, the width) depending on the viewport width and height.

A final note

Just the last note before we begin: at this stage, we are not interested in using real image assets, we are -for now- just interested in coding the main logic. For this reason, we will use colored rectangles as graphics for our tiles. In the next tutorial we'll replace them with better images.

Part two: hands-on code!

In the first section we've seen from a theoretical point of view what we need to build for our infinite horizontal scene. We're going now to see the main aspects of the implementation using the Excalibur game engine.

Remember that at this stage we're using colored rectangles for our tiles. We'll use real images in the next tutorial.

A configurable (un)stoppable HorizontalParallaxService

We want a service to:

  • Configure the number of layers we want in the scene. In our case, we want three Layers
  • Make the layers move as soon as the player moves, with different speeds
  • Make the layers stop as soon as the player stops.

Let's do this one step at a time. First we create a HorizontalParallaxService under the services folder. This service has three methods that we'll serve us as just written above:


@Service()
export class HorizontalParallaxService {

    public configureParallax(sceneKey: SceneKeys, scene: Scene) {
        //todo
    }

    public startParallax(scene:Scene) {
        //todo
    }

    public stopParallax(scene:Scene) {
        //todo
    }

}
    

Where do we call these methods? We feel like the PlayScene is the right place where to configure the layers. We then add a couple of lines to our PlayScene:


export class PlayScene extends Scene {

    ...
    private parallaxService: HorizontalParallaxService = Container.get(HorizontalParallaxService);
    ...

    public onInitialize(): void {
        ...
        this.parallaxService.configureParallax('playLevel', this);
        ...
    }

}

                    

Next step is to start and stop the layers accordingly to the player's motion. It feels natural to use the callback functions in the MotionService class. Taking into account the case when we use the keyboard listeners (the joystick case is analogous), here what it looks like this:


@Service()
export class MotionService {

    ...
    private endMoveFn: () => void;
    private startMoveFn: () => void;
    ...

    public setPlayer(actor: Actor, startMove: () => void, endMove: () => void) {
        this.player = actor;
        this.endMoveFn = endMove;
        this.startMoveFn = startMove;
    }

    ...

    private registerKeyboardListeners(on: boolean): void {
        ...
        if (on) {
            this.player.scene.engine.input.keyboard.on(Release, () => {
                this.player.vel.setTo(0, 0);
                this.player.graphics.use(ActorAnimations.IDLE);
                this.endMoveFn();
            });
            this.player.scene.engine.input.keyboard.on(Hold, (evt) => {
                ...
                this.run(speed, horizontalFlip);
                this.startMoveFn();
            });
        }

    }

    ...

}

                    

Together with the player, we now set also a startMove and a endMove functions which we call when the player starts or stops moving. These functions are defined in the Player class:


export class Player extends ExcaliburActor {

    ...
    private motionService: MotionService = Container.get(MotionService);

    public onInitialize(_engine: Engine) {
        ...
        this.configureMotionService();
    }

    private configureMotionService() {
        const startMoveFn = () => {
            const horizontalParallaxService = Container.get(HorizontalParallaxService);
            const directionFactor = this.vel.x >= 0 ? 1 : -1;
            horizontalParallaxService.startParallax(directionFactor, this.scene);
        }
        const endMoveFn = () => {
            const horizontalParallaxService = Container.get(HorizontalParallaxService);
            horizontalParallaxService.stopParallax(this.scene);
        }
        this.motionService.setPlayer(this, startMoveFn, endMoveFn);
        ...
    }
}
                    

That's it! We have the skeleton of our infinite scene. We just miss the scene itself!

An horizontal infinite scene configuration

As we've said in the first part of this tutorial, we'll use colored rectangles to build our tiles. We can start from them:


function buildColorfulRectangles(tileNumber: number, layerWidth: number, layerHeight: number) {
    const colors: Color[] = [];
    for (let i = 0; i < tileNumber; i++) {
        const randomColor = '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0');
        const color = Color.fromHex(randomColor);
        colors.push(color);
    }
    return colors.map(color => {
        const rectangle = new Rectangle({color, width: layerWidth, height: layerHeight});
        rectangle.origin = vec(0, 0);
        return rectangle;
    });
}

                    

Thanks to the buildColorfulRectangles function we can build the tiles we need for every one of the three layers, which are going to have different numbers of tiles with different dimensions.

From Excalibur documentation, we know that we can instantiate an Actor and then assign a Rectangle to be used as graphics. We can then build our colored rectangle tiles as in:


function buildTile(rectangles: Rectangle[], type: ParallaxType) {
    return rectangles.map(rectangle => {
        const layerTile = new Actor();
        layerTile.anchor = vec(0, 0);
        layerTile.graphics.use(rectangle);
        layerTile.addTag(Tags.LAYERS.horizontal[type]);
        return layerTile;
    });
}

                    

We are now ready to build the three layers of our infinite horizontal scene. We want to write a configuration, called for example:


export const perSceneParallaxConfig: ParallaxConfig<ParallaxType>;

type ParallaxConfig<K extends ParallaxType> = Partial<{ [key in SceneKeys]: Partial<{ [parallaxKey in K]: (scene: Scene) => void }> }>;
export type ParallaxType = 'layer1' | 'layer2' | 'layer3';
                    

We'll export this const in a HorizontalParallaxConfig which you can find on the public repository of this project. This constant is, as its name suggests, an object containing the layer configuration for every scene. For now, our game just has the PlayScene, but we're already thinking about the future when we'll have at least another scene.

Let's have a look now at how to put together all the pieces to build the Layer 1 of our scene. We've said that we want a couple of colored tiles that will give a sense of depth by moving slowly in the distance:


export const perSceneParallaxConfig: ParallaxConfig<ParallaxType>;
    playLevel: {
        // for now we'll use rectangles. Soon we'll use real assets
        layer1: scene => {
            const tileNumber = 2;
            const heightScaling = 1;
            const {layerWidth, layerHeight} = getLayerSize(tileNumber, heightScaling);
            const rectangles = buildColorfulRectangles(tileNumber, layerWidth, layerHeight);
            const tiles = buildTile(rectangles, 'layer1');
        },
}
                    

In these lines of code we're building the colored tiles using the functions we've seen before. The only function we didn't see before is the getLayerSize one. That is the place where we take care of the viewport size, as discussed in the first part of this tutorial:


function getLayerSize(tileNumber: number, layerHeightScale: number) {
    const deviceService = Container.get(DeviceService);
    const {vw, vh} = deviceService.getViewportSize();
    const layerWidth: number = vw / (tileNumber - 1);
    const layerHeight: number = vh * layerHeightScale;
    return {layerWidth, layerHeight};
}
                    

In the getLayerSize function we compute the layer width depending on the number of tiles (the more of them, the smaller they are), and the layer height depending on the fraction of the viewport we want to cover (they can occupy the full viewport height as for Layer 1, or only a small portion of it as for Layer 3)

Auxiliary in this function is the DeviceSize, whose responsibility is to obtain data about the device on where the game is running. For example, in the future, we could use it to understand if the code is running on an Android device. For now, it only computes the viewport size:


@Service()
export class DeviceService {

    public getViewportSize(): ViewportSize {
        const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
        const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
        return {vw, vh};
    }

}

export type ViewportSize = { vw: number; vh: number };
                    

Let's go back to our perSceneParallaxConfig. We have built all the colored tiles we need for the first layer, that is the Layer 1. We miss two things: to give them their starting position and to move them to the further right when they leave the viewport:


export const perSceneParallaxConfig: ParallaxConfig<ParallaxType>;
    playLevel: {
        // for now we'll use rectangles. Shortly we'll use real assets
        layer1: scene => {
            const tileNumber = 2;
            const heightScaling = 1;
            const {layerWidth, layerHeight} = getLayerSize(tileNumber, heightScaling);
            const rectangles = buildColorfulRectangles(tileNumber, layerWidth, layerHeight);
            const tiles = buildTile(rectangles, 'layer1');
            const height: number = scene.camera.viewport.height;
            tiles.forEach((tile, index) => {
                const xPos = index * layerWidth;
                const yPos = height - layerHeight;
                tile.z = ZIndexes.layers.layer1;
                tile.pos = vec(xPos, yPos);
                tile.on('exitviewport', () => {
                    const currentPosX = tile.pos.x;
                    tile.pos.x = currentPosX + tileNumber * layerWidth;
                });
                scene.add(tile);
            });
        },
}
                    

The starting position on the X axis is computed so that the tiles are placed on a horizontal row. The row is placed on the bottom of the viewport (yPos). The 'exitviewport' callback update the tile position so that it moves tileNumber*layerWidth pixel to the right, as we've seen in the first theoretical part.

The ZIndexes module contains the information about the zIndex of all of the Actors of the scene. Remember that also the tiles are created as Actors!


export module ZIndexes {

    export const actors: { [key in ActorType]: number } = {
        player: 100
    };

    export const layers: { [key in ParallaxType]: number } = {
        layer1: 1,
        layer2: 2,
        layer3: 3
    }

}
                    

We've done a lot of work until now, but before seeing something in the scene we still need to write the methods of the HorizontalParallaxService:



@Service()
export class HorizontalParallaxService {

    private queryManagerService = Container.get(QueryManagerService);

    public configureParallax(sceneKey: SceneKeys, scene: Scene) {
        const layerTypes: ParallaxType[] = ['layer1'];
        layerTypes.forEach(layerType => {
            const sceneLayerConfig = HorizontalParallaxConfig.perSceneParallaxConfig[sceneKey][layerType];
            sceneLayerConfig(scene);
        });
    }

    public startParallax(directionFactor: number, scene: Scene) {
        const {layer1: layer1Tag, layer2: layer2Tag, layer3: layer3Tag} = Tags.LAYERS.horizontal;
        [layer1Tag, layer2Tag, layer3Tag].forEach(tag => {
            const tiles: Actor[] = this.queryManagerService.query(tag, scene);
            const {x, y} = HorizontalParallaxConfig.headedRightVelocitiesMagnitude[tag];
            const vel: Vector = vec(x * directionFactor, y);
            tiles.forEach(tile => tile.vel = vel);
        });
    }

    public stopParallax(scene: Scene) {
        const {layer1: layer1Tag, layer2: layer2Tag, layer3: layer3Tag} = Tags.LAYERS.horizontal;
        [layer1Tag, layer2Tag, layer3Tag].forEach(tag => {
            const tiles: Actor[] = this.queryManagerService.query(tag, scene);
            const vel: Vector = vec(0, 0);
            tiles.forEach(tile => tile.vel = vel);
        });
    }

}
                

Let's have a closer look at these methods.

The configureParallax method, which is called in the PlayScene onInitialize method, uses the HorizontalParallaxConfig we've just written.

The startParallax and stopParallax methods, which are the functions we pass to the MotionService.setPlayer method to be called as soon as the player starts or stops moving, are using the QueryManagerService to retrieve all of the tiles of the game scene and update their speed according to the player ones. If the player stops, they stop. If the player moves, they read their speed from the HorizontalParallaxConfig.headedRightVelocitiesMagnitude[tag] variable. There are as many velocities as the number of layers since to give a sense of depth they need to move at different speeds.

Now we are ready to see our code in action. Open the terminal and run


npm run serve
                

Here is what you see:

Build an infinite level with parallax effect in Excalibur - 5

We immediately notice that there is a problem: where is the second tile? We see that there is the first one, colored in pink, while there is no trace of the second since that blue color is the default background of Excalibur games. By debugging the code the solution is quickly found: the exitviewport event for the second tile is called too early. That is, as soon as the scene is initialized, the callback function, which updates the tile position, is executed.

The easiest solution to this problem is to fine-tune to player's initial position so that every tile is initially contained in the viewport. It is enough to see a little portion of them; thanks to this workaround, the exitviewport handler function will not be called at the scene initialization. From a practical point of view, we need to make a slight adjustment to the player initial position, which is set in the onInitialize method of the PlayScene class:


export class PlayScene extends Scene {

    private actorFactory: ActorFactory = Container.get(ActorFactory);
    private parallaxService: HorizontalParallaxService = Container.get(HorizontalParallaxService);

    public onInitialize(): void {
        const playerPos = this.camera.viewport.center;
        const parallaxCorrection = 50;
        const adjustedPlayerPos = playerPos.add(vec(parallaxCorrection, 0));
        const args: ActorArgs = {
            pos: adjustedPlayerPos,
            collisionType: CollisionType.Active,
            collider: Shape.Box(100, 130),
        };
        const player: Player = this.actorFactory.createPlayer(args);
        this.add(player);
        this.parallaxService.configureParallax('playLevel', this);
    }

}
                

The parallaxCorrection variable does the trick we've just thought of. We now obtain what we wanted:

Build an infinite level with parallax effect in Excalibur - 6

Building a sense of depth

We can be quite satisfied with what we've built until now. We can proceed by adding the second layer Layer 2, which should appear closer than Layer 1. We start by adding it to the perSceneParallaxConfig variable in the HorizontalParallaxConfig module:


export const perSceneParallaxConfig: ParallaxConfig<ParallaxType> = {
        playLevel: {
            // for now we'll use rectangles. In the end, we'll use real assets
            layer1: scene => {
                ...
            },
            layer2: scene => {
                const tileNumber = 4;
                const heightScaling = 0.6;
                const {layerWidth, layerHeight} = getLayerSize(tileNumber, heightScaling);
                const rectangles = buildColorfulRectangles(tileNumber, layerWidth, layerHeight);
                const tiles = buildTile(rectangles, 'layer2');
                const height: number = scene.camera.viewport.height;
                tiles.forEach((tile, index) => {
                    const xPos = index * layerWidth;
                    const yPos = height - layerHeight;
                    tile.z = ZIndexes.layers.layer2;
                    tile.pos = vec(xPos, yPos);
                    tile.on('exitviewport', () => {
                        const directionFactor = getDirectionFactor(scene);
                        const currentPosX = tile.pos.x;
                        tile.pos.x = currentPosX + directionFactor * tileNumber * layerWidth;
                    });
                    scene.add(tile);
                });
            },
                

The only differences with reference to the Layer 1 tiles are: the tileNumber

  • Tile number: we'll use more tiles since the second layer is closer to the camera and we want to add more variety to the background
  • heightScaling: we want the Layer 2 tiles to occupy only a fraction of the viewport height, while the Layer 1 tiles occupy the whole of it
  • ZIndex: the Layer 2 z index is higher than the Layer 1 one, since this layer being closer should always be rendered above the farthest one.

That was pretty easy, right? To display the Layer 2 we just need to add it to the layer configuration in the configuration method of the HorizontalParallaxService:


public configureParallax(sceneKey: SceneKeys, scene: Scene) {
    const cameraConfig = HorizontalParallaxConfig.cameraParallaxConfig[sceneKey];
    cameraConfig(scene);
    const layerTypes: ParallaxType[] = ['layer1', 'layer2'];
    layerTypes.forEach(layerType => {
        const sceneLayerConfig = HorizontalParallaxConfig.perSceneParallaxConfig[sceneKey][layerType];
        sceneLayerConfig(scene);
    });
}
                

And this is what we have right now:

Build an infinite level with parallax effect in Excalibur - 7

A solid layer in a physic world

Reflecting on what we wrote in the first part of the tutorial, we only need to implement the third layer. This layer represents the solid base on which the player can move. Let's start working on the meaning of the solid word: we need our tiles and the player to collide with each other, this meaning that we need to set a collider to our tiles. From Excalibur documentation, we know that, given a tile that is an instance of an Excalibur Actor, it's as easy as:


tile.body.collisionType = CollisionType.Fixed;
const collider = Shape.Box(layerWidth, layerHeight, vec(0, 0));
tile.collider.set(collider);
                

The CollisionType is fixed since the basement of the scene should be immovable. All the other features of the Layer 3 tiles are analogous to the other layer ones, so we can add the last configuration to the perSceneParallaxConfig:


export const perSceneParallaxConfig: ParallaxConfig<ParallaxType> = {
        playLevel: {
            // for now we'll use rectangles. In the end, we'll use real assets
            layer1: scene => {
                ...
            },
            layer2: scene => {
                ...
            },
            layer3: scene => {
                const tileNumber = 10;
                const heightScaling = 0.15;
                const {layerWidth, layerHeight} = getLayerSize(tileNumber, heightScaling);
                const rectangles = buildColorfulRectangles(tileNumber, layerWidth, layerHeight);
                const tiles = buildTile(rectangles, 'layer3');
                tiles.forEach(tile => {
                    tile.body.collisionType = CollisionType.Fixed;
                    const collider = Shape.Box(layerWidth, layerHeight, vec(0, 0));
                    tile.collider.set(collider);
                });
                const height: number = scene.camera.viewport.height;
                tiles.forEach((tile, index) => {
                    const xPos = index * layerWidth;
                    const yPos = height - layerHeight;
                    tile.z = ZIndexes.layers.layer3;
                    tile.pos = vec(xPos, yPos);
                    tile.on('exitviewport', () => {
                        const currentPosX = tile.pos.x;
                        const directionFactor = getDirectionFactor(scene);
                        tile.pos.x = currentPosX + directionFactor * tileNumber * layerWidth;
                    });
                    scene.add(tile);
                });
            }
        }
    }
}
                

After having added the Layer 3 configuration in the configureParallax method of the HorizontalParallaxService, as in


public configureParallax(sceneKey: SceneKeys, scene: Scene) {
    const cameraConfig = HorizontalParallaxConfig.cameraParallaxConfig[sceneKey];
    cameraConfig(scene);
    const layerTypes: ParallaxType[] = ['layer1', 'layer2', 'layer3'];
    layerTypes.forEach(layerType => {
        const sceneLayerConfig = HorizontalParallaxConfig.perSceneParallaxConfig[sceneKey][layerType];
        sceneLayerConfig(scene);
    });
}
                

we finally see this kind of result:

Build an infinite level with parallax effect in Excalibur - 8

We've also enabled the game gravity by adding


Physics.gravity = vec(0,200)
                

when creating the Game instance.

Conclusions

In this tutorial, we've done a lot of work, from understanding the parallax effect to implementing it in Excalibur. The work was worth it since now our player moves in an infinite horizontal scene. In the next tutorial, we will replace the colored rectangles used in this tutorial with real image assets. Read more about it here!

Did you like this article and wish there were more? Donate on Paypal so that I can write more!

Related Articles