JavaScript Game Tutorial - Space Invaders Part 2 - User Input

This is the second part in a series about creating a Space Invaders clone in JavaScript. It is highly recommended to start from the beginning as each part builds directly upon the previous.

Space Invaders Tutorial Series
Part 0 Beginner JavaScript Game Tutorial For Professional Use
Part 1 Math Classes and Game Entity Structure
Part 2 User Input
Part 3 Enemy Behavior
Part 4 Collision Detection and Projectiles
Part 5 Sprites and User Interface
Part 6 Optimization
Part 7 3D Renderer
Part 8 Events and Audio

Introduction

In Part 1, we created the necessary structure for the Entities and added the math classes we need. In this part, we will add user input and be able to move the player around on screen.

We are going to support two input methods, keyboard and touch. This allows the game to be played on a computer or on a touch screen device. There are three actions the player can perform - move left, move right and fire. These are the controls:

Action Keyboard Touch
Move Left Left Arrow Touch left side
Move Right Right Arrow Touch right side
Fire Spacebar Touch middle


Goals

The goals for this part are:

  • Update the Player class to be able to move left and right
  • Create the PlayerActions object
  • Hook into the DOM input events for keyboard and touch


Player

The Player object needs to be able to perform three actions: move left, move right and fire. Here is what we need to accomplish this.

Player
Property Type Description
movingLeft Boolean true if the player is moving left
movingRight Boolean true if the player is moving right
Function Return Type Description
moveLeft( enable ) none if enabled is true, makes the player move left, if false, stops the player moving left
moveRight( enable ) none if enabled is true, makes the player move right, if false, stops the player moving right
fire() none fires a projectile
updateDirection() none update direction based on movingLeft and movingRight
//
// Player Object
//
function Player(position, speed, direction) {
    Entity.call(this, position, speed, direction);

    this.width = 20;
    this.height = 10;

    this.movingLeft = false;
    this.movingRight = false;
}
Player.prototype = Object.create(Entity.prototype);

Player.prototype.updateDirection = function () {
    var direction = new Vector2d(0, 0);
    if( this.movingLeft ) {
        direction = vectorAdd( direction, new Vector2d(-1, 0) );
    }
    if( this.movingRight ) {
        direction = vectorAdd( direction, new Vector2d(1, 0) );
    }

    this.direction = direction;
};

Player.prototype.moveRight = function (enable) {
    this.movingRight = enable;
    this.updateDirection();
};

Player.prototype.moveLeft = function (enable) {
    this.movingLeft = enable;
    this.updateDirection();
};

Player.prototype.fire = function () {
    console.log("Fire to be implemented");
};

Player.prototype.update = function (dt) {
    Entity.prototype.update.call(this, dt);
};

We will implement fire() later, when we introduce projectiles. Don't forget to remove the old movement code from the update function. In fact, it would be perfectly safe to delete the update function; we won't be using it again.

In the Game object, we have to change the instantiation of the player to speed it up and set the direction to (0, 0).

this.addEntity(new Player( new Vector2d(100, 175), 90, new Vector2d(0, 0)));


User Input Overview

User input is handled using the regular DOM input event system; the HTML Canvas has no special input methods. For our purposes, we need to know when an input starts and ends, ie when a key is pressed and released or when a finger touches the screen and when it releases (the DOM events 'keydown', 'keyup', 'touchstart', 'touchend' and 'touchcancel'). A new object PlayerActions funnels both input types into one object that calls the functions on the Player object.


Player Actions

The PlayerActions object is responsible for actually executing commands on the Player object. The actions are identified by the strings: "moveLeft", "moveRight", and "fire". Every input event has a unique identifier of some sort. Keyboard events have the keycode of the key pressed, while touch events have an identifier of which finger is touching the screen to account for multitouch screens. The input code will pass this identifier and the string of the action to the PlayerActions object. For example, the input code calls playerActions.startAction(id, "moveLeft") to make the player move left. It calls endAction(id) to stop the action associated with id.

Player Actions
Property Type Description
_ongoingActions Array an array of currently active playerActions
Function Return Type Description
startAction( id, playerAction ) none start the playerAction with id and store it in ongoingActions
endAction( id ) none stop the action with id and remove it from ongoingActions
//
// Player Actions
//
var playerActions = (function () {
    var _ongoingActions = [];

    function _startAction(id, playerAction) {
        if( playerAction === undefined ) {
            return;
        }

        var f,
            acts = {"moveLeft":  function () { if(game.player()) game.player().moveLeft(true); },
                    "moveRight": function () { if(game.player()) game.player().moveRight(true); },
                    "fire":      function () { if(game.player()) game.player().fire(); } };

        if(f = acts[playerAction]) f();

        _ongoingActions.push( {identifier:id, playerAction:playerAction} );
    }

    function _endAction(id) {
        var f,
            acts = {"moveLeft":  function () { if(game.player()) game.player().moveLeft(false); },
                    "moveRight": function () { if(game.player()) game.player().moveRight(false); } };

        var idx = _ongoingActions.findIndex(function(a) { return a.identifier === id; });

        if (idx >= 0) {
            if(f = acts[_ongoingActions[idx].playerAction]) f();
            _ongoingActions.splice(idx, 1);  // remove action at idx
        }
    }

    return {
        startAction: _startAction,
        endAction: _endAction
    };
})();


Keyboard Input

Keyboard input is the easiest to implement. On the keydown event, we start the appropriate player action based on which key was pressed. On the keyup event, we stop the player action associated with the key that was released. I use an object as a lookup table for the keybindings. You could easily change the bindings this way, or include multiple bindings for the same action. In order to find what the numeric keycode of each key is, go to keycode.info.

Keyboard Input
Property Type Description
keybinds Object a lookup table matching keycodes to a player action string
Function Return Type Description
keyDown( e ) none start the player action based on which key is pressed
keyUp( e ) none stop the player action
//
// Keyboard
//
var keybinds = { 32: "fire",
                 37: "moveLeft",
                 39: "moveRight" };

function keyDown(e) {
    var x = e.which || e.keyCode;  // which or keyCode depends on browser support

    if( keybinds[x] !== undefined ) {
        e.preventDefault();
        playerActions.startAction(x, keybinds[x]);
    }
}

function keyUp(e) {
    var x = e.which || e.keyCode;

    if( keybinds[x] !== undefined ) {
        e.preventDefault();
        playerActions.endAction(x);
    }
}

document.body.addEventListener('keydown', keyDown);
document.body.addEventListener('keyup', keyUp);


Touch Input

Touch input is more complicated because we need to know where the screen was touched. The coordinates given in the touch event is in page coordinates. We have to convert that into game world coordinates manually. The player action is based on the location of the touch on the screen. The left 20% moves the player left, the right 20% moves the player right and the middle fires the weapon.

Touch Input
Function Return Type Description
getRelativeTouchCoords( touch ) Object returns an object with the touch location x and y in game world coordinates
touchStart( e ) none start the player action based on the location of the touch
touchEnd( e ) none stop the player action
//
// Touch
//
function getRelativeTouchCoords(touch) {
    function getOffsetLeft( elem ) {
        var offsetLeft = 0;
        do {
            if( !isNaN( elem.offsetLeft ) ) {
                offsetLeft += elem.offsetLeft;
            }
        }
        while( elem = elem.offsetParent );
        return offsetLeft;
    }

    function getOffsetTop( elem ) {
        var offsetTop = 0;
        do {
            if( !isNaN( elem.offsetTop ) ) {
                offsetTop += elem.offsetTop;
            }
        }
        while( elem = elem.offsetParent );
        return offsetTop;
    }

    var scale = game.gameFieldRect().width / canvas.clientWidth;
    var x = touch.pageX - getOffsetLeft(canvas);
    var y = touch.pageY - getOffsetTop(canvas);

    return { x: x*scale,
             y: y*scale };
}

function touchStart(e) {
    var touches = e.changedTouches,
        touchLocation,
        playerAction;

    e.preventDefault();

    for( var i=touches.length-1; i>=0; i-- ) {
        touchLocation = getRelativeTouchCoords(touches[i]);

        if( touchLocation.x < game.gameFieldRect().width*(1/5) ) {
            playerAction = "moveLeft";
        }
        else if( touchLocation.x < game.gameFieldRect().width*(4/5) ) {
            playerAction = "fire";
        }
        else {
            playerAction = "moveRight";
        }

        playerActions.startAction(touches[i].identifier, playerAction);
    }
}

function touchEnd(e) {
    var touches = e.changedTouches;
    e.preventDefault();

    for( var i=touches.length-1; i>=0; i-- ) {
        playerActions.endAction(touches[i].identifier);
    }
}

var canvas = document.getElementById("game-layer");
canvas.addEventListener("touchstart", touchStart);
canvas.addEventListener("touchend", touchEnd);
canvas.addEventListener("touchcancel", touchEnd);


Conclusion

In this part, we developed the user input system. The Player object has the needed movement functions and the PlayerActions object ties them together to the user input events.

Here is the road map for the rest of this series:

  • Part 3. Enemy behavior, including descending and moving as a group.
  • Part 4. Projectiles and collision detection. The game will be playable at this point.
  • Part 5. Sprites and UI, handling title screens, menu, etc.
  • Part 6. Optimize the memory usage to ensure a stable frame rate.
  • Part 7. Switch to a 3D Renderer.
  • Part 8. Add an Event System and Audio.

I hope this tutorial has been useful. I know it doesn't look like much now, but we are well on our way to a fully featured game.

I have packaged the code for the full tutorial series for anyone interested in downloading it.

Question or Comment?