All About HTML5 Game Development, from Publishing to Monetization

Add the main player in an ExcaliburJS game scene - 2/11

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);
    }
}

                
The 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?

Player with no associated graphics

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 Excalibur ImageSource. 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 Excalibur Animations.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 the PlayerGraphics.animations variable
  • textures to have a collection of named textures, referring to the PlayerGraphics.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!

An animated player in our game

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!

Related Articles