Add the main player in an ExcaliburJS game scene - 2/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 an animated character to our Excalibur game, Sword Adventure.
As a first step, we will just load the assets needed to show it on the screen. Therefore, we are going to write some tests with jest and ts-mockito. After tests are done, we will be ready to add a motion controller to move our character, read about it here!
Add an actor to our Excalibur scene
Extending the Excalibur Actor class
First thing first, let's create the Typescript class that we will use to instantiate our actor.
Create an actors
folder under the src
folder, and create in it a Player.ts
file. Our project structure now looks as
sword-adventure
/src
/index.ts
/Game.ts
/services
/EngineOptionsFactory.ts
/DomService.ts
/scenes
PlayScene.ts
/actors
Player.ts
/index.html
/package.json
/tsconfig.json
/webpack.common.js
/webpack.development.js
/webpack.production.js
Following Excalibur documentation and examples, we know that the right way to go is to extend the Actor
class. Then, going back to the empty Player
class we've created, let's add some content to it:
export class Player extends Actor {
public type: ActorType = 'player';
constructor({pos, color, collisionType, collider, collisionGroup}: ActorArgs, {tag, collisionGroupKey}: ActorConfig) {
super({
pos,
color,
collisionType,
collider,
collisionGroup: CollisionGroupManager.create(collisionGroupKey),
});
this.addTag(tag);
}
}
export type ActorConfig = {tag:string, collisionGroupKey:string};
Let's have a look at what we have here.
ActorTags
We've created a module, ActorTags
, which we'll use to collect all the actor-related strings we're going to use while developing the game. Two examples of these strings are the collision group key and the actor tag. The module looks as
export module ActorTags {
export const COLLISION_GROUPS: { [key in ActorType]: string } = {
player: 'player_collision_group'
}
export const TAGS: { [key in ActorType]: string } = {
player: 'player_tag'
};
}
This module is placed under src/config
, so that our project structure looks as:
sword-adventure
/src
/index.ts
/Game.ts
/services
/EngineOptionsFactory.ts
/DomService.ts
/scenes
PlayScene.ts
/actors
Player.ts
/config
ActorTags.ts
GeneralGameConfig.ts
/index.html
/package.json
/tsconfig.json
/webpack.common.js
/webpack.development.js
/webpack.production.js
Constructor
In the constructor we just call the base method of the Excalibur Actor
class.
It seems the recipe is over! We can go back to the PlayScene
class, to add our Player
to it.
Add the actor to the scene
In one of the previous tutorials, we've set up a PlayScene
which for the moment is the only scene of our game. We're ready now to add the Player
actor to it. To do so, let's add a few lines to it:
export class PlayScene extends Scene {
public onInitialize(): void {
const {width, height} = Configs.game.size;
const args: ActorArgs = {
pos: vec(width / 2, height / 2),
collisionType: CollisionType.Active,
collider: Shape.Box(100, 100),
};
const config: ActorConfig = {tag: TAGS.player, collisionGroupKey: COLLISION_GROUPS.player};
const player: Player = new Player(args, config);
this.add(player);
}
}
onInitialize()
method of Excalibur Scene
is called once in the lifecycle of the Scene
. Read more about scenes here.In the onInitialize()
method we are instantiating and adding our actor to the scene.
Run now in your terminal:
npm run serve
If you open your browser at localhost:9000
you will see... nothing! How so? Where is our character?
Game debug
There is a convenience method of Excalibur Engine
class that maybe can help us. Go back to Game
class and, in the getInstance()
method, just add a line to show debug tools:
public static getInstance(): Game {
...
this.game.showDebug(true);
return this.game;
}
Try now to open again your browser. You should see now an empty box. Is that our character?
Registering graphics to actors
Another look at Excalibur documentation reveals to us that we are not using any graphics for our character. This is the reason why we don't see anything in the scene, even if in debug
mode we see that the actor is there.
Before seeing anything, we need to:
- Load player images
- Assign the images to the actor
As with any type of problem, there are multiple possible solutions.
In this tutorial we will see the solution we've adopted while developing another game. In our opinion, it is a solution that allows us to keep the code sufficiently tidy even if we have to add many other characters to the game, and load many other assets accordingly. The price to pay for this type of order is a greater complexity of the code than what would be enough for our game Sword Adventure, which is very simple and will have few elements. However, one of the goals of the Sword Adventure project is to create a template with pre-built code to develop more complex games. So let's go see a possible solution for loading resources and assigning them to our main character.
Resources loading
In our Game
class, in the method startCustomLoader()
we instantiate a Loader
which takes an array of Loadables
.
In one of the previous tutorials, we've seen the Single Responsibility Principle. Our goal is to adopt it even when loading resources. That is, we would like to build a variable that will act as a collector for all the resources while keeping the resources split. It's easier to do it rather than tell it, so let's do it!
Create a AllResources.ts
file in the config
folder. In this file create the allResources
variable which will be the collector of all the resources of our game:
export const allResources: ImageSource[] = [];
This allResources
variable will the collector of all the resources of our game. At the moment is empty, we will fill it in a minute. Before doing that, go back to Game
class and modify the startCustomLoader()
so that the loader takes the allResources
variable as parameter:
public startCustomLoader() {
const loader: Loader = new Loader(allResources);
this.logLoadingProgress(loader);
...
return Game.getInstance().start(loader);
}
We are ready now to load the image assets to display our player. We're going to use Kenney assets, which are available under a CC0 license and are therefore suitable for our needs.
Download the Toon character 1 assets pack from Kenney's site. Create an assets
folder under src
and place there the assets file. We've kept only Robot
assets, as you can see in our Github repository. We've also renamed the folders to avoid blank spaces in the directories' names.
Now that we have some assets for our main character, we're ready to load them. Create a graphics
folder under the config
directory. Inside the former, create a file named PlayerGraphics.ts
. Our project structure now looks as
sword-adventure
/src
...
/config
ActorTags.ts
GeneralGameConfig.ts
/graphics
PlayerGraphics.ts
...
We're going to use PlayerGraphics
for declaring the resources of the player character. It's done as:
export module PlayerGraphics {
const botSpriteSheetSource = new ImageSource(botSpriteSheet);
const botHurtSource = new ImageSource(botHurt);
const botSheet: SpriteSheet = SpriteSheet.fromImageSource({
image: botSpriteSheetSource,
grid: {
rows: 5,
columns: 9,
spriteWidth: 96,
spriteHeight: 128
}
});
export const sprites: SpriteConfig = {
sheet: botSpriteSheetSource,
hurt: botHurtSource
}
export const animations: ActorAnimation = {
idle: Animation.fromSpriteSheet(botSheet, range(0, 0), 200),
run: Animation.fromSpriteSheet(botSheet, range(25, 27), 200)
}
}
The types used in this module are under src/types/GraphicTypes.ts
and contains some utility types.
export type ActorGraphic = {
[key in ActorType]: GraphicConfig[]
}
export type SpriteConfig = {
sheet: ImageSource
} & {
[key in GraphicName]: ImageSource
}
export type ActorAnimation = { [key in AnimationName]: Animation };
export type GraphicConfig = { name: GraphicName, graphic: Graphic };
export type AnimationName = 'idle' | 'run';
export type GraphicName = 'hurt';
export type ActorAnimations = {
[key in ActorType]: AnimationConfig[]
}
export type AnimationConfig = { name: AnimationName, animation: Animation };
Let's have a look at the PlayerGraphics
module, step by step. There are only two variables that are exported (and then visible outside the module): sprites
and animations
.
sprites
: here we declare the ExcaliburImageSource
. We're using both a single image both a spritesheet so that we learn to load and use both of them.animations
: we will benefit from this variable in a very short time, to animate our player character. Here we're using the ExcaliburAnimations.fromSpriteSheet()
method to build animation from the Kenney's sprite sheets.
Now we can go back to the allResources
variable we've created before and use it to load the player assets declared in the PlayerGraphics
module:
const playerImages: ImageSource[] = Object.values(PlayerGraphics.sprites);
export const allResources: ImageSource[] = [
...playerImages
]
And that's it! The resources are loaded. Now it's time to use them!
Use the graphics
After having a look at Excalibur documentation, we know that we can register graphics to actors by string keys. We want to take advantage of this feature as much as possible.
To begin with, we create a module that contains the associations between the keys and the graphics. Let's place it near PlayerGraphics
module and call it AllGraphics
:
export module AllGraphics {
export const animations: ActorAnimations = {
player: [
{name: 'idle', animation: PlayerGraphics.animations.idle},
{name: 'run', animation: PlayerGraphics.animations.run},
]
}
export const textures: ActorGraphic = {
player: [
{graphic: toSprite(PlayerGraphics.sprites.hurt), name: 'hurt'},
]
}
function toSprite(imageSource: ImageSource): Sprite {
return imageSource.toSprite();
}
}
Here we are using:
animations
to have a collection of named animations, referring to thePlayerGraphics.animations
variabletextures
to have a collection of named textures, referring to thePlayerGraphics.sprites
variable. We are not interested in registering the whole robot spritesheet, then we are just declaring the hurt texture.
Now that we have a sort of configuration with named assets, it's time to register them to our player character. We don't have yet anything which has this responsibility. Let's create then a GraphicService
class:
@Service()
export class GraphicService {
public registerActorAnimations(actorType: ActorType, actor: Actor): void {
const animationElement: AnimationConfig[] = AllGraphics.animations?.[actorType];
animationElement?.forEach(({name, animation}) => {
actor.graphics.add(name, animation);
});
}
public registerActorGraphics(actorType: ActorType, actor: Actor): void {
const graphicInfo: GraphicConfig[] = AllGraphics.textures?.[actorType];
graphicInfo?.forEach(({name, graphic}) => {
actor.graphics.add(name, graphic);
});
}
}
This service is just wrapping the actor.graphics.add()
Excalibur method. The service needs to receive the actor keys to look up the AllGraphic
configuration we've just written. If there is a configuration, the configuration being a couple (name, animation) or (name, graphic), the service adds it to the actor by calling actor.graphics.add(name, graphic)
.
We have the feeling that registering graphics is something we are going to do for every actor that we'll add to our game. It seems a good idea to have a base class that performs this operation, and to extend this class for every kind of actor we need. Let's go and give this idea a try.
First we create a wrapper class named ExcaliburActor
which extends the Excalibur Actor
.
export abstract class ExcaliburActor extends Actor {
abstract type: ActorType;
private graphicService: GraphicService = Container.get(GraphicService);
constructor({pos, color, collisionType, collider}: ActorArgs, {collisionGroupKey, tag}: ActorConfig) {
super({
pos,
color,
collisionType,
collider,
collisionGroup: CollisionGroupManager.create(collisionGroupKey),
});
this.addTag(tag);
}
public onInitialize(_engine: Engine) {
this.graphicService.registerActorGraphics(this.type, this);
this.graphicService.registerActorAnimations(this.type, this);
}
}
export type ActorConfig = { tag: string, collisionGroupKey: string };
As you can see, the onInitialize()
method calls the GraphicsService
to register the actor animations and textures.
Given this abstract class, the Player
class becomes as
export class Player extends ExcaliburActor {
public type: ActorType = 'player';
public onInitialize(_engine: Engine) {
super.onInitialize(_engine);
this.graphics.use('idle');
}
}
In the onInitialize()
method, after calling the base class, we set the graphics
to use the idle
animation. We've registered the animation when calling the GraphicsService.registerActorAnimations()
method, so... this should really do the magic!
Let's run again
npm run serve
and open your browser at localhost:9000
. You should finally see our player character standing and running!
Conclusions
In this tutorial, we've added an animated character to our Excalibur scene. That was a lot to go through. But more is coming: in the next tutorial, we're going to write some unit tests with jest!
Did you like this article and wish there were more? Donate on Paypal so that I can write more!