Infinite game level refinement in Excaliburjs HTML5 game engine - 5/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 internationalization with i18next. 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 add some assets for our horizontal scene. We'll set up three different parallax layers to give a sense of depth to our game.
In the last tutorial we've added a HorizontalParallaxService
which builds the layers of our scene, using temporary colorful rectangles. It's now time to use real images for them. We thank Kenney for their assets, which come with a CC0 licence!
Kenney's assets
First thing first we add a couple of Kenney's assets pack to our repository: we've chosen the voxel pack for the solid base on which our player moves, and the background elements redux pack for the background elements such as castles, bushes, and trees.
Register parallax graphics
Now that we have the asset images, we can load them and use them in our game as we did for the player images in this tutorial. Since it's the same procedure we've done for the player images, we suggest reading the dedicated tutorial and to browse the public repo of this project, looking for these classes:
ParallaxGraphics.ts
for the import and declaration of the new assetsAllGraphics.ts
for the association between theGraphic
name and theGraphic
itselfGraphicService.ts
for theregisterParallaxGraphics
method, which register the named graphics to the actorAllResources.ts
to load the new resources before starting theGame
Use the parallax graphics
Now that it's all set, we can deal with the main topic of this tutorial: replace the colorful rectangles we've used in the previous tutorial with Kenney's assets. Our starting point is this:
We'd like to do as follow:
- Use a castle background for the far away Layer 1
- Replace Layer 2 colored rectangles with two different kind of trees
- Add a Layer dedicated to bushes
- Use a grass tile for the solid Layer on which the player moves.
We start with the HorizontalParallaxConfig
, whose functions buildColorfulRectangles
and buildTile
are the ones responsible for the colored rectangles.
We start by working on the buildTile
function. First thing first we rename it to buildGraphicTile
. Then, we won't use anymore any colored rectangle, we will instead specify how many tiles we need. We change then the function signature as in
function buildGraphicTile(howMany:number, type: ParallaxType)
This done, we would like to exploit the same mechanism we've used for the Player
class to register the actor graphics, that is we are going to call the method registerParallaxGraphics
of the GraphicService
inside an Excalibur onInitialize
lifecycle hook. Let's create, then, a ParallaxTile
class which will look as
export class ParallaxTile extends Actor {
private graphicService: GraphicService = Container.get(GraphicService);
constructor(private parallaxType: ParallaxType) {
super();
}
public onInitialize(_engine: Engine) {
super.onInitialize(_engine);
this.graphicService.registerParallaxGraphics(this.parallaxType, this);
const {graphic, name} = this.getRandomGraphic();
this.graphics.use(name);
}
private getRandomGraphic(): ParallaxGraphicConfig {
const items = AllGraphics.parallaxGraphic[this.parallaxType];
return items[Math.floor(Math.random() * items.length)];
}
}
As you can see, this class looks like the Player
class. To remember how we've designed the GraphicService
, you can read the dedicated tutorial.
We have all the pieces to keep refactoring the buildGraphicTile
function. We'll instantiate as many ParallaxTile
objects as the howMany
variable demands:
function buildGraphicTile(howMany:number, type: ParallaxType, tileSize: TileSize){
const tiles: ParallaxTile[] = [];
for(let i=0; i< howMany; i++){
const layerTile = new ParallaxTile(type, tileSize);
layerTile.anchor = vec(0, 0);
layerTile.addTag(Tags.LAYERS.horizontal[type]);
tiles.push(layerTile);
}
return tiles;
}
And this is enough to obtain what we wanted to achieve:
It seems that all the work we've done to set up the GraphicService
, the AllGraphics
and the allResources
was worth it, since it's saving us a lot of time now!
Adapt to different viewports
The very last thing we miss is to scale the graphics to work on every screen. At this stage, you can see that by changing the screen size of the browser you're working on, the tiles are rendered with images that do not fill well. Luckily we've set up our code in such a way that the scaling can be easily achieved!
Let's start by having a look at the ParallaxTile
class. In the onInitialize
method we get a random graphic and use it. The graphic is an Excalibur GraphicsComponent
, whose width and height can be set. Let's then change the class constructor by requiring also the width and the height of the graphic:
export class ParallaxTile extends Actor {
private graphicService: GraphicService = Container.get(GraphicService);
constructor(private parallaxType: ParallaxType, private tileSize: TileSize) {
super();
}
public getTileSize(): TileSize {
return this.tileSize;
}
public onInitialize(_engine: Engine) {
...
const {graphic, name} = this.getRandomGraphic();
graphic.height = this.tileSize.height;
graphic.width = this.tileSize.width;
...
}
...
}
export type TileSize = { width: number, height: number };
We instantiate the ParallaxTile
objects inside the buildGraphicTile
method, so we should adapt it to our needs:
function buildGraphicTile(howMany:number, type: ParallaxType, tileSize: TileSize){
...
for(let i=0; i< howMany; i++){
const layerTile = new ParallaxTile(type, tileSize);
...
}
...
}
We need just to know the value of the tileSize
variable, but this is easy since we were already computing it before, when we were using colored rectangles: the width and the height of the tiles are given by the getLayerSize
function, which we wrote in the previous tutorial!
After some fine-tuning of the layer velocities in the HorizontalParallaxConfig.headedRightVelocitiesMagnitude
constant and on the heightScaling
and tileNumber
variables given to the getLayerSize
function, which we can rename to getGraphicTileSize
, we're done. This is the final result:
Conclusions
In this tutorial, we've used real image assets for displaying our parallax layers. The amount of work was contained since we've structured our code in a good way, as it was displayed in this tutorial, where we've added the assets for the player character. Now that we have an environment where to move, we can proceed by giving our player a mission: in the next tutorial we'll add another character, the mentor. The mentor and our character will have a little talk thanks to a Twine integration. Thanks to Twine it will be possible to write dialogs that can evolve differently depending on the player's answer.
In the next tutorial. we'll add a mentor to give game instruction to our players, integrating Twinery for managing character dialogs.
Did you like this article and wish there were more? Donate on Paypal so that I can write more!