Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Game Input Simplified

$
0
0

Imagine a game character named "Bob the Butcher" standing on his own in a darkened room while hordes of mutant sausage zombies start pouring in through doors and broken windows. At this point, it would be a good idea for Bob to start blasting the sausage zombies into tiny chunks of meat, but how will Bob do that in a cross-platform game? Will the game player have to press one or more keys on a keyboard, click the mouse, tap the screen, or hit a button on a gamepad?

When programming a cross-platform game, this is the type of thing you are likely to spend a lot of your time battling with if you're unprepared for it. If you're not careful, you could end up with massive, spaghetti-like if statements or switch statements to deal with all of the different input devices.

In this tutorial, we are going to make things much simpler by creating a single class that will unify multiple input devices. Each instance of the class will represent a specific game action or behaviour (such as "shoot", "run", or "jump") and can be told to listen to various keys, buttons, and pointers on multiple input devices.

Note:The programming language used in this tutorial is JavaScript, but the technique that is used to unify multiple input devices can easily be transferred to any other cross-platform programming language that provides APIs for input devices.

Shooting the Sausages

Before we start writing the code for the class that we will be creating in this tutorial, let's take a quick look at how the class could actually be used.

// Create an input for the "shoot" action.
shoot = new GameInput();

// Tell the input what to react to.
shoot.add( GameInput.KEYBOARD_SPACE );
shoot.add( GameInput.GAMEPAD_RT );

// During each game update, check the input.
function update() {
    if( shoot.value > 0 ) {
        // Tell Bob to shoot the mutant sausage zombies!
    } else {
        // Tell Bob to stop shooting.
    }
}

GameInput is the class that we will be creating, and you can see how much simpler it will make things. The shoot.value property is a number and will be a positive value if the space bar on a keyboard is pressed or the right trigger on a gamepad is pressed. If neither the space bar nor the right trigger is pressed, the value will be zero.

Getting Started

The first thing we need to do is create a function closure for the GameInput class. Most of the code will we be writing is not actually part of the class, but it needs to be accessible from within the class while remaining hidden from everything else. A function closure allows us to do that in JavaScript.

(In a programming language like ActionScript or C#, you could simply use private class members, but that isn't a luxury we have in JavaScript, unfortunately.)

(function(){
    // code goes here
})();

The rest of the code in this tutorial will replace the "code goes here" comment.

The Variables

The code only needs a handful of variables to be defined outside of functions, and those variables are as follows.

var KEYBOARD = 1;
var POINTER  = 2;
var GAMEPAD  = 3;
var DEVICE   = 16;
var CODE     = 8;

var __pointer = {
    currentX   : 0,
    currentY   : 0,
    previousX  : 0,
    previousY  : 0,
    distanceX  : 0,
    distanceY  : 0,
    identifier : 0,
    moved      : false,
    pressed    : false
};

var __keyboard = {};
var __inputs   = [];
var __channels = [];

var __mouseDetected = false;
var __touchDetected = false;

The constant-like KEYBOARD, POINTER, GAMEPAD, DEVICE and CODE values are used to define input device channels, such as GameInput.KEYBOARD_SPACE, and their use will become clear later on in the tutorial.

The __pointer object contains properties that are relevant to mouse and touch-screen input devices, and the __keyboard object is used to keep track of keyboard key states. The __inputs and __channels arrays are used to store GameInput instances and any input device channels added to those instances. Finally, the __mouseDetected and __touchDetected indicate if a mouse or touch screen has been detected.

Note:The variables don't need to be prefixed with two underscores; that is simply the coding convention I have chosen to use for the code in this tutorial. It helps to separate them from variables defined in functions.

The Functions

Here comes the bulk of the code, so you may want to grab a coffee or something before you begin reading this part!

These functions are defined after the variables in the previous section of this tutorial, and they are defined in order of appearance.

// Initializes the input system.
function main() {
    // Expose the GameInput constructor.
    window.GameInput = GameInput;

    // Add the event listeners.
    addMouseListeners();
    addTouchListeners();
    addKeyboardListeners();

    // Some UI actions we should prevent in a game.
    window.addEventListener( "contextmenu", killEvent, true );
    window.addEventListener( "selectstart", killEvent, true );

    // Start the update loop.
    window.requestAnimationFrame( update );
}

The main() function is called at the end of the code—that is, at the end of the function closure we created earlier. It does what it says on the tin and gets everything running so the GameInput class can be used.

One thing I should bring to your attention is the use of the requestAnimationFrame() function, which is part of the W3C Animation Timing specification. Modern games and applications use this function to run their update or rendering loops because it has been highly optimized for that purpose in most web browsers.

// Updates the input system.
function update() {
    window.requestAnimationFrame( update );

    // Update the pointer values first.
    updatePointer();

    var i        = __inputs.length;
    var input    = null;
    var channels = null;

    while( i -- > 0 ) {
        input    = __inputs[ i ];
        channels = __channels[ i ];

        if( input.enabled === true ) {
            updateInput( input, channels );
        }
        else {
            input.value = 0;
        }
    }
}

The update() function rolls through the list of active GameInput instances and updates the ones that are enabled. The following updateInput() function is quite long, so I won't add the code here; you can see the code in full by downloading the source files.

// Updates a GameInput instance.
function updateInput( input, channels ) {
    // note: see the source files
}

The updateInput() function looks at the input device channels that have been added to a GameInput instance and works out what the value property of the GameInput instance should be set to. As seen in the Shooting the Sausages example code, the value property indicates if an input device channel is being triggered, and that allows a game to react accordingly, perhaps by telling Bob to shoot the mutant sausage zombies.

// Updates the value of a GameInput instance.
function updateValue( input, value, threshold ) {
    if( threshold !== undefined ) {
        if( value < threshold ) {
            value = 0;
        }
    }

    // The highest value has priority.
    if( input.value < value ) {
        input.value = value;
    }
}

The updateValue() function determines if the value property of an GameInput instance should be updated. The threshold is primarily used to prevent analog device input channels, such as gamepad buttons and sticks, that don't reset themselves properly from constantly triggering a GameInput instance. This happens quite often with faulty or grubby gamepads.

Like the updateInput() function, the following updatePointer() function is quite long so I won't add the code here. You can see the code in full by downloading the source files.

// Updates the pointer values.
function updatePointer() {
    // note: see the source files
}

The updatePointer() function updates the properties in the __pointer object. In a nutshell, the function clamps the pointer's position to make sure it doesn't leave the web browser's window viewport, and it calculates the distance the pointer has moved since the last update.

// Called when a mouse input device is detected.
function mouseDetected() {
    if( __mouseDetected === false ) {
        __mouseDetected = true;

        // Ignore touch events if a mouse is being used.
        removeTouchListeners();
    }
}

// Called when a touch-screen input device is detected.
function touchDetected() {
    if( __touchDetected === false ) {
        __touchDetected = true;

        // Ignore mouse events if a touch-screen is being used.
        removeMouseListeners();
    }
}

The mouseDetected() and touchDetected() functions tell the code to ignore one input device or the other. If a mouse is detected before a touch screen, the touch screen will be ignored. If a touch screen is detected before a mouse, the mouse will be ignored.

// Called when a pointer-like input device is pressed.
function pointerPressed( x, y, identifier ) {
    __pointer.identifier = identifier;
    __pointer.pressed    = true;

    pointerMoved( x, y );
}

// Called when a pointer-like input device is released.
function pointerReleased() {
    __pointer.identifier = 0;
    __pointer.pressed    = false;
}

// Called when a pointer-like input device is moved.
function pointerMoved( x, y ) {
    __pointer.currentX = x >>> 0;
    __pointer.currentY = y >>> 0;

    if( __pointer.moved === false ) {
        __pointer.moved = true;
        __pointer.previousX = __pointer.currentX;
        __pointer.previousY = __pointer.currentY;
    }
}

The pointerPressed(), pointerReleased() and pointerMoved() functions handle input from a mouse or a touch screen. All three functions simply update properties in the __pointer object.

After those three functions, we have a handful of standard JavaScript event handling functions. The functions are self-explanatory so I won't add the code here; you can see the code in full by downloading the source files.

// Adds an input device channel to a GameInput instance.
function inputAdd( input, channel ) {
    var i = __inputs.indexOf( input );

    if( i === -1 ) {
        __inputs.push( input );
        __channels.push( [ channel ] );
        return;
    }

    var ca = __channels[ i ];
    var ci = ca.indexOf( channel );

    if( ci === -1 ) {
        ca.push( channel );
    }
}

// Removes an input device channel to a GameInput instance.
function inputRemove( input, channel ) {
    var i = __inputs.indexOf( input );

    if( i === -1 ) {
        return;
    }

    var ca = __channels[ i ];
    var ci = ca.indexOf( channel );

    if( ci !== -1 ) {
        ca.splice( ci, 1 );

        if( ca.length === 0 ) {
            __inputs.splice( i, 1 );
            __channels.splice( i, 1 );
        }
    }
}

// Resets a GameInput instance.
function inputReset( input ) {
    var i = __inputs.indexOf( input );

    if( i !== -1 ) {
        __inputs.splice( i, 1 );
        __channels.splice( i, 1 );
    }

    input.value   = 0;
    input.enabled = true;
}

The inputAdd(), inputRemove() and inputReset() functions are called from a GameInput instance (see below). The functions modify the __inputs and __channels arrays depending on what needs to be done.

A GameInput instance is considered active, and added to the __inputs array, when an input device channel has been added to the GameInput instance. If an active GameInput instance has all of its input device channels removed, the GameInput instance considered inactive and removed from the __inputs array.

Now we arrive at the GameInput class.

// GameInput constructor.
function GameInput() {}

GameInput.prototype = {
    value   : 0,
    enabled : true,

    // Adds an input device channel.
    add : function( channel ) {
        inputAdd( this, channel );
    },

    // Removes an input device channel.
    remove : function( channel ) {
        inputRemove( this, channel );
    },

    // Removes all input device channels.
    reset : function() {
        inputReset( this );
    }
};

Yep, that's all there is—it's a super lightweight class that essentially acts as an interface to the main code. The value property is a number that ranges from 0 (zero) through to 1 (one). If the value is 0, it means that the GameInput instance is not receiving anything from any input device channels that have been added to it.

The GameInput class does have a few static properties, so we will add those now.

// The X position of the pointer within the window viewport.
GameInput.pointerX = 0;

// The Y position of the pointer within the window viewport.
GameInput.pointerY = 0;

// The distance the pointer has to move, in pixels per frame, to
// cause the value of a GameInput instance to equal 1.0.
GameInput.pointerSpeed = 10;

Keyboard device channels:

GameInput.KEYBOARD_A = KEYBOARD << DEVICE | 65 << CODE;
GameInput.KEYBOARD_B = KEYBOARD << DEVICE | 66 << CODE;
GameInput.KEYBOARD_C = KEYBOARD << DEVICE | 67 << CODE;
GameInput.KEYBOARD_D = KEYBOARD << DEVICE | 68 << CODE;
GameInput.KEYBOARD_E = KEYBOARD << DEVICE | 69 << CODE;
GameInput.KEYBOARD_F = KEYBOARD << DEVICE | 70 << CODE;
GameInput.KEYBOARD_G = KEYBOARD << DEVICE | 71 << CODE;
GameInput.KEYBOARD_H = KEYBOARD << DEVICE | 72 << CODE;
GameInput.KEYBOARD_I = KEYBOARD << DEVICE | 73 << CODE;
GameInput.KEYBOARD_J = KEYBOARD << DEVICE | 74 << CODE;
GameInput.KEYBOARD_K = KEYBOARD << DEVICE | 75 << CODE;
GameInput.KEYBOARD_L = KEYBOARD << DEVICE | 76 << CODE;
GameInput.KEYBOARD_M = KEYBOARD << DEVICE | 77 << CODE;
GameInput.KEYBOARD_N = KEYBOARD << DEVICE | 78 << CODE;
GameInput.KEYBOARD_O = KEYBOARD << DEVICE | 79 << CODE;
GameInput.KEYBOARD_P = KEYBOARD << DEVICE | 80 << CODE;
GameInput.KEYBOARD_Q = KEYBOARD << DEVICE | 81 << CODE;
GameInput.KEYBOARD_R = KEYBOARD << DEVICE | 82 << CODE;
GameInput.KEYBOARD_S = KEYBOARD << DEVICE | 83 << CODE;
GameInput.KEYBOARD_T = KEYBOARD << DEVICE | 84 << CODE;
GameInput.KEYBOARD_U = KEYBOARD << DEVICE | 85 << CODE;
GameInput.KEYBOARD_V = KEYBOARD << DEVICE | 86 << CODE;
GameInput.KEYBOARD_W = KEYBOARD << DEVICE | 87 << CODE;
GameInput.KEYBOARD_X = KEYBOARD << DEVICE | 88 << CODE;
GameInput.KEYBOARD_Y = KEYBOARD << DEVICE | 89 << CODE;
GameInput.KEYBOARD_Z = KEYBOARD << DEVICE | 90 << CODE;

GameInput.KEYBOARD_0 = KEYBOARD << DEVICE | 48 << CODE;
GameInput.KEYBOARD_1 = KEYBOARD << DEVICE | 49 << CODE;
GameInput.KEYBOARD_2 = KEYBOARD << DEVICE | 50 << CODE;
GameInput.KEYBOARD_3 = KEYBOARD << DEVICE | 51 << CODE;
GameInput.KEYBOARD_4 = KEYBOARD << DEVICE | 52 << CODE;
GameInput.KEYBOARD_5 = KEYBOARD << DEVICE | 53 << CODE;
GameInput.KEYBOARD_6 = KEYBOARD << DEVICE | 54 << CODE;
GameInput.KEYBOARD_7 = KEYBOARD << DEVICE | 55 << CODE;
GameInput.KEYBOARD_8 = KEYBOARD << DEVICE | 56 << CODE;
GameInput.KEYBOARD_9 = KEYBOARD << DEVICE | 57 << CODE;

GameInput.KEYBOARD_UP    = KEYBOARD << DEVICE | 38 << CODE;
GameInput.KEYBOARD_DOWN  = KEYBOARD << DEVICE | 40 << CODE;
GameInput.KEYBOARD_LEFT  = KEYBOARD << DEVICE | 37 << CODE;
GameInput.KEYBOARD_RIGHT = KEYBOARD << DEVICE | 39 << CODE;
GameInput.KEYBOARD_SPACE = KEYBOARD << DEVICE | 32 << CODE;
GameInput.KEYBOARD_SHIFT = KEYBOARD << DEVICE | 16 << CODE;

Pointer device channels:

GameInput.POINTER_UP    = POINTER << DEVICE | 0 << CODE;
GameInput.POINTER_DOWN  = POINTER << DEVICE | 1 << CODE;
GameInput.POINTER_LEFT  = POINTER << DEVICE | 2 << CODE;
GameInput.POINTER_RIGHT = POINTER << DEVICE | 3 << CODE;
GameInput.POINTER_PRESS = POINTER << DEVICE | 4 << CODE;

Gamepad device channels:

GameInput.GAMEPAD_A       = GAMEPAD << DEVICE | 0  << CODE;
GameInput.GAMEPAD_B       = GAMEPAD << DEVICE | 1  << CODE;
GameInput.GAMEPAD_X       = GAMEPAD << DEVICE | 2  << CODE;
GameInput.GAMEPAD_Y       = GAMEPAD << DEVICE | 3  << CODE;
GameInput.GAMEPAD_LB      = GAMEPAD << DEVICE | 4  << CODE;
GameInput.GAMEPAD_RB      = GAMEPAD << DEVICE | 5  << CODE;
GameInput.GAMEPAD_LT      = GAMEPAD << DEVICE | 6  << CODE;
GameInput.GAMEPAD_RT      = GAMEPAD << DEVICE | 7  << CODE;
GameInput.GAMEPAD_START   = GAMEPAD << DEVICE | 8  << CODE;
GameInput.GAMEPAD_SELECT  = GAMEPAD << DEVICE | 9  << CODE;
GameInput.GAMEPAD_L       = GAMEPAD << DEVICE | 10 << CODE;
GameInput.GAMEPAD_R       = GAMEPAD << DEVICE | 11 << CODE;
GameInput.GAMEPAD_UP      = GAMEPAD << DEVICE | 12 << CODE;
GameInput.GAMEPAD_DOWN    = GAMEPAD << DEVICE | 13 << CODE;
GameInput.GAMEPAD_LEFT    = GAMEPAD << DEVICE | 14 << CODE;
GameInput.GAMEPAD_RIGHT   = GAMEPAD << DEVICE | 15 << CODE;
GameInput.GAMEPAD_L_UP    = GAMEPAD << DEVICE | 16 << CODE;
GameInput.GAMEPAD_L_DOWN  = GAMEPAD << DEVICE | 17 << CODE;
GameInput.GAMEPAD_L_LEFT  = GAMEPAD << DEVICE | 18 << CODE;
GameInput.GAMEPAD_L_RIGHT = GAMEPAD << DEVICE | 19 << CODE;
GameInput.GAMEPAD_R_UP    = GAMEPAD << DEVICE | 20 << CODE;
GameInput.GAMEPAD_R_DOWN  = GAMEPAD << DEVICE | 21 << CODE;
GameInput.GAMEPAD_R_LEFT  = GAMEPAD << DEVICE | 22 << CODE;
GameInput.GAMEPAD_R_RIGHT = GAMEPAD << DEVICE | 23 << CODE;

To finalize the code, we simply need to call the main() function.

// Initialize the input system.
main();

And that's all of the code. Again, it's all available in the source files.

Run Away!

Before we wrap the tutorial up with a conclusion, let's take a look at one more example of how the GameInput class can be used. This time, we will give Bob the ability to move and jump because the hordes of mutant sausage zombies might become too much for him to handle alone.

// Create the inputs.
var jump = new GameInput();
var moveLeft = new GameInput();
var moveRight = new GameInput();

// Tell the inputs what to react to.
jump.add( GameInput.KEYBOARD_UP );
jump.add( GameInput.KEYBOARD_W );
jump.add( GameInput.GAMEPAD_A );
moveLeft.add( GameInput.KEYBOARD_LEFT );
moveLeft.add( GameInput.KEYBOARD_A );
moveLeft.add( GameInput.GAMEPAD_LEFT );
moveRight.add( GameInput.KEYBOARD_RIGHT );
moveRight.add( GameInput.KEYBOARD_D );
moveRight.add( GameInput.GAMEPAD_RIGHT );

// During each game update, check the inputs.
function update() {
    if( jump.value > 0 ) {
        // Tell Bob to jump.
    } else {
        // Tell Bob to stop jumping.
    }

    if( moveLeft.value > 0 ) {
        // Tell Bob to move/run left.
    } else {
        // Tell Bob to stop moving left.
    }

    if( moveRight.value > 0 ) {
        // Tell Bob to move/run right.
    } else {
        // Tell Bob to stop moving right.
    }
}

Nice and easy. Bear in mind that the value property of GameInput instances ranges from 0 through to 1, so we could do something like changing Bob's movement speed using that value if one of the active input device channels is analog.

if( moveLeft.value > 0 ) {
    bob.x -= bob.maxDistancePerFrame * moveLeft.value;
}

Have fun!

Conclusion

Cross-platform games all have one thing in common: they all need to deal with a multitude of game input devices (controllers), and dealing with those input devices can become a daunting task. This tutorial has demonstrated one way to handle multiple input devices with the use of a simple, unified API.


Viewing all articles
Browse latest Browse all 728

Trending Articles