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.