All About HTML5 Game Development, from Publishing to Monetization

HTML5 game controllers: keyboard and virtual joystick in ExcaliburJS - 3/11

HTML5 game controllers: keyboard and virtual joystick in ExcaliburJS - 3/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 scene transitions. 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 a motion controller for our game character.

In the last tutorial we've added an animated character to our game. Now it's time to make it move! We'll move the player using keyboard inputs or using a nipplejs virtual joystick for touch-capable interfaces (e.g. smartphones).

Keyboard controller

Set up keyboard events listeners

We'll start by setting up keyboard listeners to move the player with the keyboard. From Excalibur documentation and from the source code of their gallery, we know that this is as easy as writing a couple of lines of code:


player.scene.engine.input.keyboard.on(Event.EventTypes.Release, () => {
    player.vel.setTo(0, 0);
    player.graphics.use(ActorAnimations.IDLE);
});
player.scene.engine.input.keyboard.on(Event.EventTypes.Hold, (evt) => {
    let dir = Vector.Down;
    let horizontalFlip = false;
    switch (evt.key) {
        case Input.Keys.A:
        case Input.Keys.Left:
            dir = Vector.Left;
            horizontalFlip = true;
            break;
        case Input.Keys.D:
        case Input.Keys.Right:
            dir = Vector.Right;
            break;
        case Input.Keys.S:
        case Input.Keys.Down:
            dir = Vector.Down;
            break;
        case Input.Keys.W:
        case Input.Keys.Up:
            dir = Vector.Up;
            break;
        default:
        return;
}
player.vel.setTo(dir.x, dir.y);
player.graphics.use(ActorAnimations.RUN);
                

We've taken these lines from the Excalibur gallery example. The code is quite easy to follow: the keyboard listener of the engine is set to listen for the Hold and Release events. Depending on the event, the player will stay still with zero velocity or move in a direction determined by the keyboard input. Here the engine listens to a WASD controller.

The only thing that we've added here is a module called ActorAnimations which is a simple collector of strings:


export module ActorAnimations {

    export const IDLE: AnimationName = 'idle';
    export const RUN: AnimationName = 'run';
    export const HURT: GraphicName = 'hurt';

}
export type AnimationName = 'idle' | 'run';
export type GraphicName = 'hurt';

                    

Set up a MotionService to move the player

Now that we know what it's needed to listen to keyboard events, it's time to register them. This is the kind of task that seems right to be placed in a dedicated class. In our game, there will be only one controlled character, so we can use a service that keeps a reference to the only Excalibur actor which can be controlled by the user. Let's call this service MotionService.


@Service()
export class MotionService {

    private player: Actor;
    private speedMultiplier: number = 80;

    public setPlayer(actor: Actor) {
        this.player = actor;
    }

    public registerKeyboardListeners(on: boolean): void {
        const {Hold, Release} = Events.EventTypes;
        if (!on) {
            [Hold, Release].forEach(event => {
                this.player.scene.engine.input.keyboard.off(event);
            })
        }
        if (on) {
            this.player.scene.engine.input.keyboard.on(Release, () => {
                this.player.vel.setTo(0, 0);
                this.player.graphics.use(ActorAnimations.IDLE);
            });
            this.player.scene.engine.input.keyboard.on(Hold, (evt) => {
                let dir = Vector.Down;
                let horizontalFlip = false;
                switch (evt.key) {
                    case Input.Keys.A:
                    case Input.Keys.Left:
                        dir = Vector.Left;
                        horizontalFlip = true;
                        break;
                    case Input.Keys.D:
                    case Input.Keys.Right:
                        dir = Vector.Right;
                        break;
                    case Input.Keys.S:
                    case Input.Keys.Down:
                        dir = Vector.Down;
                        break;
                    case Input.Keys.W:
                    case Input.Keys.Up:
                        dir = Vector.Up;
                        break;
                    default:
                        return;
                }
                const speed: Speed = {x: dir.x * this.speedMultiplier, y: dir.y * this.speedMultiplier};
                this.run(speed, horizontalFlip);
            });
        }
    }

    private run({x, y}: Speed, horizontalFlip: boolean) {
        this.player.vel.setTo(x, y);
        this.player.graphics.use(ActorAnimations.RUN);
        this.player.graphics.getGraphic(ActorAnimations.RUN).flipHorizontal = horizontalFlip;
    }
}
    

The MotionService can be configured to register or unregister keyboard events (by calling registerKeyboardListeners(on: boolean)) and keeps a reference to the Actor that will be moved (the private player:Actor) with the keyboard inputs.

The last detail that needs to be taken care of is the horizontal flip to apply to the player graphics if the user is moving it towards the left. This is accomplished in the run method, which changes the actor graphics to the RUN one, and if necessary flips it. We've registered actor graphics in one of the previous tutorials, you can read about it here!

Last but not least, we need to set the player to the MotionService, and enable the keyboard listeners by calling registerKeyboardListeners(true). In order to do so, let's reopen our Player.ts class, which represents the Excalibur actor we're going to move. We'll add a couple of lines in its onInitialize() method:


export class Player extends ExcaliburActor {

    public type: ActorType = 'player';
    private motionService: MotionService = Container.get(MotionService);

    public onInitialize(_engine: Engine) {
        super.onInitialize(_engine);
        this.graphics.use(ActorAnimations.IDLE);
        this.motionService.setPlayer(this);
        this.motionService.registerKeyboardListeners(true);
    }

}
                    

And this is it! Now we can move the player using a WASD keyboard controller.

Set up a virtual joystick with nipplejs

We've set up the keyboard listeners for moving the player with the keyboard. But in the future, we'll release our cross-platform game on smartphones too, and on those devices, it would be impossible to play. We need to set up a different kind of motion controller. We've got more than luck on our side since the nipplejs the library does exactly what we need, and it does it well!

Let's start by installing the nipplejs dependency. Run in the terminal:


npm i nipplejs
                    

You should see the new dependency in your package.json.

From the nipplejs documentation we know that we need an HTMLElement with a set position:relative in our DOM. Open then the index.html file and add a div element with id="joystick"in it:


<body>
...
<div class="joystick" id="joystick"></div>
<canvas id="game" style="display: block; margin: auto auto"></canvas>
...
</body>
                    

The joystick CSS class is defined as:


.joystick {
    position: absolute;
    bottom: 200px;
    right: 200px;
}
                    

Now we'll create a dedicated class to manage the virtual joystick. In the src/services folder create a JoystickFactory defined as:


@Service()
export class JoystickFactory {

    private domService: DOMService = Container.get(DOMService);
    private joystickManager: JoystickManager;

    public getJoystick(): JoystickManager {
        if (Util.isDefined(this.joystickManager)) {
            return this.joystickManager;
        }
        const position: { bottom: string; right: string } = {bottom: '35%', right: '20%'};
        const joystickDomElement: HTMLElement = this.domService.getElement('joystick');
        this.joystickManager = nipplejs.create({
            zone: joystickDomElement,
            mode: 'static',
            position,
            color: 'black',
            size: 150,
            shape: 'circle',
        });
        return this.joystickManager;
    }

    public destroy(): void {
        this.joystickManager?.destroy();
        this.joystickManager = undefined;
    }

}
                    

As you can see, the JoystickFactory service is responsible for creating, if not already done, the nipplejs virtual joystick. The DOMService, whose responsibility is to manage DOM elements, retrieves the div we've added in the index.html. That div hosts the virtual joystick.

Now that we're able to create a virtual joystick, we need to make some adjustments to the MotionService so that to be able to use the keyboard controller or the virtual joystick.

Let's back to the MotionService and add a setMotionType public method to choose between the keyboard controller and the virtual joystick one:



@Service()
export class MotionService {

    ...

    public setMotionType(motionType: MotionTypes): void {
        this.motionType = motionType;
        if (this.motionType === 'keyboard') {
            this.registerKeyboardListeners(true);
            this.registerJoystickListener(false);
        }
        if (this.motionType === 'joystick') {
            this.registerKeyboardListeners(false);
            this.registerJoystickListener(true);
        }
    }

    ...

}

export type MotionTypes = 'keyboard' | 'joystick';

                    

The registerKeyboardListeners(on:boolean) method is the same method we've written at the beginning of this tutorial. Now it's time to write the registerJoystickListener(on:boolean) one!

After some reading of the nipplejs documentation, we know that the joystick listeners look very similar to the keyboard ones:


private registerJoystickListener(on: boolean): void {
        if (!on) {
            this.joystickFactory.destroy();
        }
        if (on) {
            const joystickManager: JoystickManager = this.joystickFactory.getJoystick();
            joystickManager.on('move', (event, data) => {
                const {vector, direction} = data;
                if (direction?.x && direction?.y) {
                    const yMapper: number = this.yMapping[direction.y];
                    const speed: Speed = {x: vector.x * this.speedMultiplier, y: Math.abs(vector.y) * yMapper * this.speedMultiplier};
                    const horizontalFlip: boolean = direction.angle === 'left';
                    this.run(speed, horizontalFlip);
                }
            });
            joystickManager.on('end', () => {
                this.player.rotation = toRadians(0);
                this.player.vel.setTo(0, 0);
                this.player.graphics.use(ActorAnimations.IDLE);
            });
        }
    }
                

where the yMapping is a private field of the MotionService to map the nipplejs up and down directions to the Excalibur plus or minus sign:


private yMapping = {up: -1, down: 1};
                    

Both the registerListeners methods can be private since from outside we'll call only the setMotionType of the MotionService.

Let's have a closer look at the registerJoystickListener(on:boolean) method.

When called with on=true, we ask for the joystick to the JoystickFactory we've seen before. After that it's all on the nipplejs joystick: the move event is triggered every time the user move the virtual joystick on the screen. The data object we received contains the direction information (up, left, down, right) and the vector information, which represents the force unit vector. We can use this data to move the player accordingly, as it's done in the handler functions of the joystick manager listeners.

The very last thing we miss is to adjust the Player class:


export class Player extends ExcaliburActor {

    public type: ActorType = 'player';
    private motionService: MotionService = Container.get(MotionService);

    public onInitialize(_engine: Engine) {
        super.onInitialize(_engine);
        this.graphics.use(ActorAnimations.IDLE);
        this.motionService.setPlayer(this);
        this.motionService.setMotionType('keyboard');
    }

}
                

As you can see, now we call the public setMotionType MotionServicemethod instead of the registerKeyboardListeners which has become private. In this moment we don't know on which device the game is running on, so we've chosen to set up the keyboard listeners by default, since we are developing using a browser. To check if the virtual joystick is working, we've added some development buttons to switch between the controllers. You can see the development buttons in the upper toolbar, which naturally won't be displayed in the release version of our game:

Motion controllers in Excalibur - 2

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

Conclusions

In this tutorial, we've set up a motion controller to move our game character. We can configure the controller to use both the WASD keyboard inputs and a nipplejs virtual joystick when using touch-capable interfaces. Since in the future we'll release our game also on the Google Play Store, this is all that we needed!

Next step now is to write some tests for the MotionService we've just created. As you will see in the next tutorial, we'll need to set up a karma runner. We can't use jest as done with the other tests since to mock the Excalibur engine inputs would be quite a lot of work. An integration test is the best way to go, even though Karma requires some amount of configuration.

No more idle talk: let's set up Karma tests!

Related Articles