Interacting with the Game - Creating the Basic Game - HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

part 2

Creating the Basic Game

Chapter 8

Interacting with the Game

in this chapter

• Capturing user input

• Working with touch events

• Binding inputs to actions

• Adding visual feedback to actions

In the previous chapter, you implemented the first parts of the game display. So far, it’s just a static rendering of the jewel board, and the game does not react to any user input at all. In this chapter, you discover how to implement a new module that captures user input and lets the display and the rest of the game react to these inputs.

Before doing that, however, you walk through the different types of inputs available in the browser, paying special attention to touch-based input as it is found in mobile devices such as smartphones and tablets. Using this knowledge, you can then return to the game and build a system that encapsulates the native input events and lets you translate them into game actions.

After you learn how to implement the user input, you see how to attach game actions to the display module to allow the player to select and swap jewels.

Capturing User Input

You probably already know how to use the basic keyboard and mouse events in desktop browsers, so I don’t spend much time on them here. More interesting is how these events behave on mobile devices with touch screens.

Mouse events on touch devices

Touch-enabled devices such as smartphones and tablets rarely come equipped with a mouse. Instead, they depend solely on interaction with the touch screen to navigate through applications and web sites. As you see in a bit, you use a set of touch-based events when targeting these devices, but using them doesn’t help much when most of the web has been built without any regard for touch screens. Not to worry, most devices solve this problem transparently by automatically firing mousedown, click, and mouseup events when the user taps the screen. If the site works with a mouse, there’s a good chance it also works just fine on a touch-based device.

One thing doesn’t translate easily to a touch screen, though, and that’s the hover state. With a real mouse, hovering the pointer over a UI element is natural and often provides, for example, additional information in the form of tooltips or just a bit of eye candy. Some designs even rely onmouseover events to trigger actions such as unfolding menus, displaying extra buttons, and so on.

This state of “almost but not quite” just doesn’t exist on a touch device. You’re either touching the screen or you aren’t. There’s no way to mimic the mouseover event because there’s no way to tell where the finger is until it actually touches the screen. This also has consequences for CSS because the :hover pseudo-class no longer makes sense.

The virtual keyboard

Both Android and iOS feature a virtual keyboard that automatically appears whenever the user needs to input text. For example, tapping an input field or a text area automatically brings up the keyboard.

Keyboard events such as keypress, keydown, and keyup do go through to JavaScript, so it is possible to map certain functions to keys on the virtual keyboard. However, the events aren’t fired until you release the key, so you can’t actually tell when the key is pressed, only when it is released. The events also fire only once, unlike in desktop browsers where they fire continuously when the key is kept pressed.

You don’t have much control over this keyboard. There’s no nice and easy way to disable it, and you can’t force it to pop up either. If you really need to be able to toggle the keyboard manually, you can place an input field out of view and toggle the keyboard by using the blur() andfocus() methods. See Listing 8.1 for an example. You can find the code in the file 01-virtualkeyboard.html.

Listing 8.1 Toggling the Virtual Keyboard

<button id=”toggleButton”>Toggle Keyboard</button><br/>

<span id=”output”></span>

<script>

function toggleKeyboard() {

var kbToggle = document.getElementById(“kbToggle”);

// create element if it doesn’t exist

if (!kbToggle) {

kbToggle = document.createElement(“input”);

kbToggle.id = “kbToggle”;

kbToggle.style.position = “absolute”;

kbToggle.style.left = “-1000px”;

document.body.appendChild(kbToggle);

// keep the focus

kbToggle.addEventListener(“blur”, function() {

kbToggle.focus();

}, false);

}

// switch classes and focus

if (kbToggle.className == “on”) {

kbToggle.className = “off”;

kbToggle.blur();

} else {

kbToggle.className = “on”;

kbToggle.focus();

}

}

// output pressed key

document.addEventListener(“keypress”, function(e) {

document.getElementById(“output”).innerHTML

= “You pressed: “ + String.fromCharCode(e.charCode);

}, false);

// toggle keyboard on click

document.getElementById(“toggleButton”).addEventListener(

“click”, toggleKeyboard, false);

In general, I advise against using the keyboard for anything other than text input. Not only is its functionality limited, but it also eats up a significant portion of the screen real estate.

Touch events

Both iOS and Android devices have had exposed touch events in the browser since early versions. You can detect whether the browser supports touch events by using Modernizr:

if (Modernizr.touch) {

// touch events are supported

}

Alternatively, you can test if, for example, the ontouchstart property exists on some element:

if (“ontouchstart” in document.createElement(“div”)) {

// touch events are supported

}

The touchstart event is fired whenever the user places a finger on the screen. The other two touch events that you need to know about are touchmove and touchend. The touchmove event fires when the finger moves across the screen, and the touchend event fires when the user removes the finger. These three touch events behave much like the mousedown, mousemove, and mouseup events.

In most regards, touch event objects are just like any other event object, but they have a few extra properties that you need to know and understand. Event objects coming from mouse events carry with them information about the mouse position, for example, via the clientX and clientYproperties:

document.body.addEventListener(“click”, function(e) {

alert(e.clientX + “, “ + e.clientY);

}, false);

Touch events have similar data about the touch position, but instead of providing it directly on the event object, they have a property called touches. The touches property is a list of all the currently active touches on the screen. To get information about the touch event, you must grab the first touch object from the touches list. A touch object has the same coordinate properties that you know from regular mouse event objects, that is, clientX/Y, screenX/Y, and so on. The example in Listing 8.2 shows how to retrieve the coordinates of the touch event. The example is located in the file 02-touch.html.

Listing 8.2 Using Touch Events

Touch X: <input id=”touchx”><br/>

Touch Y: <input id=”touchy”>

<script>

var touchx = document.getElementById(“touchx”),

touchy = document.getElementById(“touchy”);

document.addEventListener(“touchmove”, function(e) {

touchx.value = e.touches[0].clientX;

touchy.value = e.touches[0].clientY;

e.preventDefault();

}, false);

</script>

This short example simply prints the x and y coordinates in the corresponding input elements. Note that, because it accesses only the first element of the touches list, this event works only with the first finger that touches the screen. As you see in the next section, however, working with multiple touch objects also is easy.

Multitouch

The touches array lists all active touches, and if more than one finger is touching the screen, you don’t know which one is the relevant touch object. The event object provides two additional lists that you can use to get around this problem: targetTouches and changedTouches. ThetargetTouches list contains only the touch objects that are active on the target element. So, if a touchstart event fires on, say, a div element, the targetTouches list lists only the touch objects on that specific div, whereas the touches list might contain other, unrelated touch objects. ThechangedTouches list adds a further restriction and lists only the touch objects involved in that specific event. So, if you have two fingers on the div and move only one, the targetTouches list in the touchmove event contains both touch objects, but the changedTouches gives you only the one that moved.

note.eps

A quick note about multitouch support before I show you an example of how to use it. In Android 2.3 and any earlier versions, you are not able to take advantage of multitouch events in the browser. Events for extra touches simply don’t fire, even on devices that otherwise support multitouch. The touches, targetTouches, and changedTouches arrays never have more than one element. Android 3.0, which was developed with mainly tablets in mind, has some limited multitouch support in the browser on devices such as the Motorola Xoom, but at least for a little while still, iOS dominates this area.

The first example is a multitouch-enabled feature that lets you drag multiple elements around at the same time. Listing 8.3 shows the code, which you also can find in the file 03-multidrag.html.

Listing 8.3 Multitouch Drag

<div id=”dragme1”></div>

<div id=”dragme2”></div>

<script>

var el1 = document.getElementById(“dragme1”),

el2 = document.getElementById(“dragme2”);

el1.addEventListener(“touchstart”, drag, false);

el2.addEventListener(“touchstart”, drag, false);

function drag(e) {

var touch = e.targetTouches[0],

x = touch.clientX, // save orig. position

y = touch.clientY,

rect = this.getBoundingClientRect();

e.preventDefault();

function move() {

var newX = touch.clientX,

newY = touch.clientY;

this.style.left = (rect.left + newX - x) + “px”;

this.style.top = (rect.top + newY - y) + “px”;

}

this.addEventListener(“touchmove”, move, false);

this.addEventListener(“touchend”, function() {

this.removeEventListener(“touchmove”, move);

}, false);

};

</script>

The drag() function starts by grabbing a touch object from the targetTouches list and saving the original position. Ignore any extra fingers on the same elements, so picking the first element of targetTouches is fine. The drag() function then attaches a handler to the touchmove event that uses the difference between the new and old coordinates to move the div element. Had the drag() function used touches rather than targetTouches, you couldn’t be sure that the touch object was actually the one that was on that element.

tip.eps

As you can see in Listing 8.3, you need to retrieve the touch object only once in the touchstart event. When the touchmove event fires later, the properties of the touch object are updated automatically.

Another common feature in mobile applications is the two-finger pinch-zoom gesture. When two fingers touch the screen and move either toward or away from each other, you can use the relative change in distance as a scaling factor. You can apply this factor to anything you want, be it the whole page or just a single element. Listing 8.4 shows an example of how you can create such a pinch-zoom feature. The code is located in the file 04-pinchzoom.html.

Listing 8.4 Multitouch Pinch-zoom Effect

var el = document.getElementById(“mydiv”);

el.addEventListener(”touchstart”, startZoom, false);

function startZoom(e) {

e.preventDefault();

if (e.targetTouches.length != 2) {

return;

}

var touch1 = e.targetTouches[0],

touch2 = e.targetTouches[1],

dX = touch2.clientX - touch1.clientX,

dY = touch2.clientY - touch1.clientY,

startDist = Math.sqrt(dX * dX + dY * dY),

scale = +this.getAttribute(”data-scale”) || 1;

this.addEventListener(”touchmove”, zoom, false);

this.addEventListener(”touchend”, end, false);

function zoom() {

var dX = touch2.clientX - touch1.clientX,

dY = touch2.clientY - touch1.clientY,

newDist = Math.sqrt(dX * dX + dY * dY),

newScale = scale * newDist / startDist;

this.style.webkitTransform = ”scale(” + newScale + ”)”;

this.setAttribute(”data-scale”, newScale);

}

function end() {

this.removeEventListener(”touchmove”, zoom);

this.removeEventListener(”touchend”, end);

}

}

When the user touches the element, the startZoom() function acts if exactly only two fingers are touching that element, that is, if the length of targetTouches equals 2. The initial distance between the two touch events is stored, so you can compare it to calculate a new scale factor in the subsequent touchmove events. The scale factor is also stored as an attribute on the element. The startZoom() function then adds the touchmove event handler that does the actual zooming. When zoom() is triggered, the coordinates of at least one of the touch objects are changed. The ratio of new distance to the old distance is used as the scaling factor when setting the CSS scaling transformation. Finally, the touchend event handler cleans up by removing the touchmove and touchend handler functions.

Gestures

In addition to the touch* family of events, iOS devices also support a set of gesture events. These events can be useful for creating features such as the pinch-zoom effect I showed you in Listing 8.4. The three gesture events are

• gesturestart

• gesturechange

• gestureend

Gesture events are built on top of touch events and don’t expose as much low-level information. Instead, they carry information that might be useful when acting on multitouch events. None of the gesture events react to single-touch input; only when the second finger touches the screen does the gesturestart event fire. The gesturechange event fires whenever one of the touch points moves. The event object that this event sends has a few cool properties. Most interesting, perhaps, are the scaling and rotation values exposed through the scale and rotation properties. The scalevalue indicates the relative change in distance between the touch points since the gesturestart event. So, if scale is equal to 2.0, the distance has doubled since the beginning. Similarly, the rotation value indicates the number of degrees the touch points have rotated around their common center. Listing 8.5 shows how you can use those properties to create, for example, a touch-enabled zoom and rotate feature. You can find the example in the file 05-gesture.html.

Listing 8.5 Gesture-based Zoom and Rotate

var el = document.getElementById(“mydiv”);

el.addEventListener(”gesturestart”, gestureStart, false);

function gestureStart(e) {

var rot = +this.getAttribute(”data-rot”) || 0,

scale = +this.getAttribute(”data-scale”) || 1;

function change(e) {

this.style.webkitTransform =

”rotate(” + (rot + e.rotation) + ”deg) ” +

”scale(” + (scale * e.scale) + ”)”;

e.preventDefault();

}

function end(e) {

this.setAttribute(”data-rot”, rot + e.rotation);

this.setAttribute(”data-scale”, scale * e.scale);

this.removeEventListener(”gesturechange”, change);

this.removeEventListener(”gestureend”, end);

}

this.addEventListener(”gesturechange”, change, false);

this.addEventListener(”gestureend”, end, false);

}

These events definitely simplify the code for these types of features, but remember that they are iOS specific. Only devices such as iPhones and iPads support the gesture events, and there’s no sign that other browser vendors are planning to adopt them.

Simulating touch events

Sometimes being able to use and test touch events can be useful even if you are on, for example, a desktop PC with no touch support. Phantom Limb by Vodori is a nice tool that intercepts mouse events and translates them to touch events. It’s a small JavaScript library that you have to include in your page. You can find the script at www.vodori.com/blog/phantom-limb.html.

Phantom Limb implements only some basic functionality, though. For example, only the touches list is created, ignoring changedTouches and targetTouches. Even so, it does provide an easy way out if you need to test a touch-only interface on the desktop.

Touch in the future

Touch events are still in the process of being standardized. The W3C is working on a new touch events specification. This specification codifies some of the touch functionality that is already working in the wild, and it introduces some new events and behavior not yet implemented in any browsers. Among some of the interesting additions are touchenter, touchleave, and touchcancel events. The touchenter and touchleave events fire whenever a touch point enters or leaves an element. This mimics the behavior of the mouseenter and mouseleave events. The touchcancelevent is triggered whenever the user is currently touching the screen but is interrupted by, for example, moving outside the document or the native UI interfering.

Other noteworthy future plans include information about the amount of pressure applied to the screen and rotation angles for multitouch events.

Input events and canvas

Working with user input and canvas can be a bit tricky. Because a canvas element behaves like a bitmap image and the structure of the painted content isn’t retained, you really cannot attach event handlers to anything other than the canvas element itself. You might know perfectly well what the pixels mean, but the browser doesn’t, so you have no way to attach event handlers directly to game sprites or any other art you’ve drawn on the canvas.

If you want to add mouse or touch interactions to the canvas, you need to keep track of object positions yourself. That way, you can attach a single event handler on the canvas element and then search the list of objects to see whether any of them should react. If the elements that have drawn on the canvas are all described by paths, you can save the path data and use the ctx.isPointInPath() method to test whether a touch or mouse event is inside one of the paths. Listing 8.6 shows an example of this approach. The code for this example is located in the file 06-canvaspath.html.

Listing 8.6 Detecting Mouse Events on Canvas

<canvas id=”canvas” width=”400” height=”300”></canvas>

<script>

var canvas = document.getElementById(“canvas”),

ctx = canvas.getContext(“2d”);

ctx.beginPath();

ctx.moveTo(100, 50);

ctx.lineTo(250, 200);

ctx.lineTo(150, 250);

ctx.lineTo(200, 300);

ctx.lineTo(50, 250);

ctx.lineTo(150, 150);

ctx.fillStyle = “teal”;

ctx.fill();

canvas.addEventListener(“touchmove”, function(e) {

hitTest(e.targetTouches[0]);

e.preventDefault();

}, false);

canvas.addEventListener(“mousemove”, hitTest, false);

function hitTest(e) {

var rect = canvas.getBoundingClientRect(),

x = e.clientX - rect.left,

y = e.clientY - rect.top,

inPath = ctx.isPointInPath(x, y);

ctx.fillStyle = inPath ? “orange” : “teal”;

ctx.fill();

}

</script>

The example in Listing 8.6 declares a path on the canvas and then uses the ctx.isPointInPath() method in the mousemove and touchmove event handlers to decide which color to fill the path. That shows how you can use that method to attach behavior to specific areas on the canvas. Of course, this approach is useful as long as you can describe the area with a path. You also need to keep track of the points that make up the path and, if necessary, set it up again in each test. This approach is not always ideal, and it doesn’t solve all problems, but, depending on the task at hand, it might do the trick. More complex problems can sometimes be solved by testing for, for example, non-transparent pixel values using the canvas image data methods.

The ctx.isPointInPath() method has uses other than user input. Consider, for example, a game in which a projectile is fired at a target. If the outline of the target can be described by a path, the ctx.isPointInPath() method makes it trivial to determine whether the projectile has hit the target.

Building the Input Module

The input module is responsible for capturing user input in the form of keyboard, mouse, and touch events and translating these events into a set of game events. An example could be that clicking on the game board with the mouse should trigger a “select jewel” event in the game. Tapping the touch screen on a mobile device should trigger the same event. Other modules, such as the game screen module, can then bind handler functions to these events so that the appropriate actions are taken when the user interacts with the game. Start by creating a new module to handle user input and placing it in the file input.js. Listing 8.7 shows the initial contents of this module.

Listing 8.7 The Input Module

jewel.input = (function() {

var dom = jewel.dom,

$ = dom.$,

settings = jewel.settings,

inputHandlers;

function initialize() {

inputHandlers = {};

}

function bind(action, handler) {

// bind a handler function to a game action

}

function trigger(action) {

// trigger a game action

}

return {

initialize : initialize

};

})();

To create control bindings between input events and game actions, you give each input a keyword. For example, mouse clicks have the input keyword CLICK, a press of the Enter key has the keyword KEY_ENTER, and so on. The controls structure is stored in the game settings in loader.js in a format like this:

var jewel = {

...

settings : {

...

controls : {

KEY_UP : “moveUp”,

KEY_LEFT : “moveLeft”,

KEY_DOWN : “moveDown”,

KEY_RIGHT : “moveRight”,

KEY_ENTER : “selectJewel”,

KEY_SPACE : “selectJewel”,

CLICK : “selectJewel”,

TOUCH : “selectJewel”

}

}

};

Using keywords in this manner makes it easy to change the game controls without having to modify the game code. You could even enable user-defined controls, optionally saving them in the local browser storage.

The bind() function, discussed in a bit, is used to attach handler functions to game actions. For example, a game action might employ the keyword selectJewel. Whenever the input module detects some form of user input that should trigger that action, all the handler functions are called one by one.

The inputHandlers object keeps track of the bindings between input events and game actions. For each game action, a property on the inputHandlers object is an array of all the handler functions associated with that action. So, for example,inputHandlers[“selectJewel”] holds all the functions you need to call when the selectJewel action happens.

The other function stub, trigger(), is for the function that does the function calling. It simply takes an action name as its argument and calls any functions in the corresponding array in inputHandlers. If trigger() is called with any additional arguments, they are passed on to the handler functions. This way, handler functions can have access to information such as coordinates in the case of mouse events.

Handling input events

Jewel Warrior supports three types of input: mouse, keyboard, and touch. The mouse and keyboard events make sense only on the desktop, and touch events, for the most part at least, are relevant only on mobile devices. That means that some overlap exists in terms of reactions to these input events. Because the input mechanics in this game are very simple, a user should be able to play the game using just one type of input.

Mouse input

The only type of mouse input you need to consider for Jewel Warrior is clicking, so the only mouse event that you need to worry about is the mousedown event. You could also listen for the click event, but because it fires only when the mouse button is released, using the mousedown event can make the game appear a bit more responsive. When the user clicks the jewel board and a game action is bound to the CLICK event, the handler functions for this action must be called. Listing 8.8 shows the DOM event handler added to the initialize() function in input.js.

Listing 8.8 Capturing Mouse Clicks

jewel.input = (function() {

...

function initialize() {

inputHandlers = {};

var board = $(“#game-screen .game-board”)[0];

dom.bind(board, “mousedown”, function(event) {

handleClick(event, “CLICK”, event);

});

}

...

})();

The DOM event object is passed on to a second function, handleClick(), along with the name of the game action. This behavior happens so that the same logic can be reused for touch events. The handleClick() function calculates the relative coordinates of the click and, from those, the jewel coordinates. Finally, the action is triggered, sending the jewel coordinates as parameters. Listing 8.9 shows the handleClick() function.

Listing 8.9 Handling Click Events

jewel.input = (function() {

...

function handleClick(event, control, click) {

// is any action bound to this input control?

var action = settings.controls[control];

if (!action) {

return;

}

var board = $(“#game-screen .game-board”)[0],

rect = board.getBoundingClientRect(),

relX, relY,

jewelX, jewelY;

// click position relative to board

relX = click.clientX - rect.left;

relY = click.clientY - rect.top;

// jewel coordinates

jewelX = Math.floor(relX / rect.width * settings.cols);

jewelY = Math.floor(relY / rect.height * settings.rows);

// trigger functions bound to action

trigger(action, jewelX, jewelY);

// prevent default click behavior

event.preventDefault();

}

...

})();

Touch input

The touch event functionality is almost identical to the mouse event handling. Instead of the mousedown event, you now listen for the touchstart event and use the input keyword TOUCH. Instead of passing the event object as the third argument to handleClick(), you must now pass the relevant touch object, that is, event.targetTouches[0]. Listing 8.10 shows how.

Listing 8.10 Capturing Touch Input

jewel.input = (function() {

...

function initialize() {

inputHandlers = {};

var board = $(“#game-screen .game-board”)[0];

dom.bind(board, “mousedown”, function(event) {

handleClick(event, “CLICK”, event);

});

dom.bind(board, “touchstart”, function(event) {

handleClick(event, “TOUCH”, event.targetTouches[0]);

});

}

...

})();

Because event objects and touch objects both store their coordinates in clientX and clientY properties and those are the only properties used in handleClick(), passing both kinds of objects is safe.

Keyboard input

Finally, there’s the keyboard. You can use a few different events to detect keystrokes. The keydown, keyup, and keypress events all fire, but the various browsers don’t always agree on how to handle the keypress event, and the keyup event just doesn’t feel right. That leaves the keydownevent, which works out great for the responsiveness of the application as it fires as soon as the key is pressed. The event object that this event creates has a keyCode property that holds the numeric code of the key that was pressed. For example, the following snippet alerts the key code when you press any key:

element.addEventListener(“keydown”, function(event) {

alert(“You pressed: “ + event.keyCode);

}, false);

Working with numeric codes can get a bit confusing, and remembering which keys have which codes is hard. To make this code a bit easier to work with, you can use some type of a structure that maps codes to key names. Listing 8.11 shows a keys object that does just that.

Listing 8.11 Adding Key Codes

jewel.input = (function() {

var keys = {

37 : “KEY_LEFT”,

38 : “KEY_UP”,

39 : “KEY_RIGHT”,

40 : “KEY_DOWN”,

13 : “KEY_ENTER”,

32 : “KEY_SPACE”,

65 : “KEY_A”,

66 : “KEY_B”,

67 : “KEY_C”,

... // alpha keys 68 - 87

88 : “KEY_X”,

89 : “KEY_Y”,

90 : “KEY_Z”

};

...

})();

The alphabetical A-Z keys have sequential codes, starting at 65 for the A key and going up to 90 for the Z key. I defined only these as well as a few special keys such as the arrow keys, Enter, and space.

You can now use these key names as input keywords together with the CLICK and TOUCH events you’ve already implemented. Now just listen for the keydown event and trigger the appropriate action, as shown in Listing 8.12.

Listing 8.12 Capturing Keyboard Input

jewel.input = (function() {

...

function initialize() {

...

dom.bind(document, “keydown”, function(event) {

var keyName = keys[event.keyCode];

if (keyName && settings.controls[keyName]) {

event.preventDefault();

trigger(settings.controls[keyName]);

}

});

}

...

})();

Implementing game actions

Now let’s revisit the game screen module in screen.game.js and start implementing action handlers for the input events.

We need to add a new piece of information to the game screen module. When the user clicks on the jewel board, the jewel at that location is activated. You can use a simple object to hold the information about where the cursor is and whether the jewel at that position is active. Listing 8.13 shows the cursor object and its initialization.

Listing 8.13 Initializing the Cursor

jewel.screens[“game-screen”] = (function() {

var board = jewel.board,

display = jewel.display,

cursor;

function run() {

board.initialize(function() {

display.initialize(function() {

cursor = {

x : 0,

y : 0,

selected : false

};

display.redraw(board.getBoard(), function() {});

});

});

}

return {

run : run

};

})();

The cursor object has three properties. The x and y properties are the jewel coordinates, and the selected property is a boolean value indicating whether the jewel is selected or if the cursor is just sitting passively at that position. If the jewel is selected, the game tries to swap with the next activated jewel.

You also need an easy way to update these cursor values. Listing 8.14 shows a setCursor() function that sets the cursor values and also tells the game display to update the rendering of the cursor.

Listing 8.14 Setting the Cursor Properties

jewel.screens[“game-screen”] = (function() {

...

function setCursor(x, y, select) {

cursor.x = x;

cursor.y = y;

cursor.selected = select;

}

...

})();

Selecting jewels

The input module can trigger the following actions:

• selectJewel

• moveLeft

• moveRight

• moveUp

• moveDown

The selectJewel action selects a jewel on the board or, if possible, swaps two jewels in case another jewel is already selected. The move* actions move the cursor around the board.

A player can select a jewel on the board in various ways. For example, tapping the jewel on the touch screen and clicking on it with the mouse both trigger the selectJewel game action. This action should call a selectJewel() function that determines the appropriate action. Listing 8.15 shows the code for the function.

Listing 8.15 Selecting Jewels

jewel.screens[“game-screen”] = (function() {

...

function selectJewel(x, y) {

if (arguments.length == 0) {

selectJewel(cursor.x, cursor.y);

return;

}

if (cursor.selected) {

var dx = Math.abs(x - cursor.x),

dy = Math.abs(y - cursor.y),

dist = dx + dy;

if (dist == 0) {

// deselected the selected jewel

setCursor(x, y, false);

} else if (dist == 1) {

// selected an adjacent jewel

board.swap(cursor.x, cursor.y,

x, y, playBoardEvents);

setCursor(x, y, false);

} else {

// selected a different jewel

setCursor(x, y, true);

}

} else {

setCursor(x, y, true);

}

}

...

})();

If another jewel was already selected, you can use the distance between the two jewels to determine the appropriate action. You might remember from the board module that a distance of 1 means that the two positions are adjacent, a distance of 0 means that the same jewel was selected again, and any other distance means that some other jewel was selected. If the player selects the same jewel twice, the jewel is deselected by calling the setCursor() function with false as the value of the selected parameter. If the selected jewel is a neighbor of the already selected position, you try to swap the two jewels by calling the board.swap() function. The fifth argument to the swap() method is the callback function; here, a function called playBoardEvents() is passed. We get to that function in a bit. The final case, where a totally different jewel was selected, is taken care of by simply moving the cursor to that position and enabling the selected parameter.

The playBoardEvents() function passed to board.swap() is called whenever the board module finishes moving around jewels and updating the board data. The swap() function calls its callback function with a single argument, which is an array of all the events that took place between the old and new state of the board. You can now use those events to, for example, animate the display. Later, you also see how to add sound effects to these board events, but right now, take a look at the function in Listing 8.16.

Listing 8.16 Sending Board Changes to the Display

jewel.screens[“game-screen”] = (function() {

...

function playBoardEvents(events) {

if (events.length > 0) {

var boardEvent = events.shift(),

next = function() {

playBoardEvents(events);

};

switch (boardEvent.type) {

case “move” :

display.moveJewels(boardEvent.data, next);

break;

case “remove” :

display.removeJewels(boardEvent.data, next);

break;

case “refill” :

display.refill(boardEvent.data, next);

break;

default :

next();

break;

}

} else {

display.redraw(board.getBoard(), function() {

// good to go again

});

}

}

...

})();

If the events array contains any elements, the first event is removed from the array and stored in the boardEvent variable. The next() function is a small helper that calls playBoardEvents() recursively on the rest of the events. The event objects in the events array all have a type property that indicates the type of the event and a data property that holds any data relevant to that specific event. Each type of event triggers a different function on the display module. These functions don’t exist yet but are all asynchronous functions that you use to animate the display. The next()function is passed as a callback function to make sure the rest of the events are processed after the animation finishes.

Moving the cursor

Let’s turn our attention to the functions moveLeft, moveRight, moveUp, and moveDown. As their names imply, they must move the cursor a single step in one of the four directions.

To do that, use a generic moveCursor() function that takes two parameters, an x and a y value, and moves the cursor the specified number of steps along either axis. As was the case with selectJewel(), this function has two different behaviors depending on whether a jewel is currently selected. If the player has already selected a jewel, moveCursor() should instead select the new jewel rather than simply move the cursor. Because you already have the selectJewel() function, you can just pass the new coordinates on to that function. If no jewel is selected, the position of the cursor is changed by calling setCursor(). The moveCursor() code is shown in Listing 8.17.

Listing 8.17 Moving the Cursor

jewel.screens[“game-screen”] = (function() {

var settings = jewel.settings,

...

function moveCursor(x, y) {

if (cursor.selected) {

x += cursor.x;

y += cursor.y;

if (x >= 0 && x < settings.cols

&& y >= 0 && y < settings.rows) {

selectJewel(x, y);

}

} else {

x = (cursor.x + x + settings.cols) % settings.cols;

y = (cursor.y + y + settings.rows) % settings.rows;

setCursor(x, y, false);

}

}

...

})();

Now that the generic moveCursor() function is implemented, you can easily add directional move functions that move the cursor in one of the four directions. Depending on the direction, these functions simply add or subtract 1 from one of the cursor coordinates, as shown in Listing 8.18.

Listing 8.18 Directional Move Functions

jewel.screens[“game-screen”] = (function() {

...

function moveUp() {

moveCursor(0, -1);

}

function moveDown() {

moveCursor(0, 1);

}

function moveLeft() {

moveCursor(-1, 0);

}

function moveRight() {

moveCursor(1, 0);

}

...

})();

Binding inputs to game functions

With both the game functions and input events defined, you can return to the input module in input.js and bind the two sets together in the inputHandlers object. First, implement the bind() function introduced earlier in this chapter. This function takes two parameters: the name of a game action and a function that should be attached to that action. You can see the function in Listing 8.19.

Listing 8.19 The Bind Function

jewel.input = (function() {

...

function bind(action, handler) {

if (!inputHandlers[action]) {

inputHandlers[action] = [];

}

inputHandlers[action].push(handler);

}

return {

initialize : initialize,

bind : bind

};

})();

The bind() function is also exposed to the world so the other game modules can use it.

Responding to inputs

Next up is the trigger() function that was also mentioned earlier. This is the function you used in the DOM event handlers to trigger game actions. The trigger() function shown in Listing 8.20 takes a single argument, the name of a game action, and calls any handler functions that have been bound to that action.

Listing 8.20 Triggering Game Functions

jewel.input = (function() {

...

function trigger(action) {

var handlers = inputHandlers[action],

args = Array.prototype.slice.call(arguments, 1);

if (handlers) {

for (var i=0;i<handlers.length;i++) {

handlers[i].apply(null, args);

}

}

}

...

})();

If any handlers are bound to the specified action — that is, if a property with that name is on the inputHandlers object — all the handler functions are called. Any arguments passed to trigger() beyond the named action argument are extracted by borrowing the slice() method from Array. The resulting array of argument values is then used when calling the handler functions via apply().

note.eps

You need to be careful with error handling in this implementation of trigger(), especially if you are making a game or framework where you have no control over the handler functions. If one of the bound handlers fails and throws an error, the loop is interrupted and no other handlers are called. Whether this is a problem depends on the specific project, but it’s something to keep in mind. To ensure that a single handler can’t break everything, you can wrap the apply() call in a try-catch statement. That does, however, add a bit of extra overhead to the process.

Initializing the input module

You just need to attach the functions in the game screen module to the action names from the input module. This is where the input.bind() method comes in. After initializing the input module in screen.game.js, simply bind the functions to the relevant actions using bind(), as shown in Listing 8.21. The binding should happen only once, so make sure you add the firstRun test to avoid multiple bindings.

Listing 8.21 Binding Inputs to Game Actions

jewel.screens[“game-screen”] = (function() {

var board = jewel.board,

display = jewel.display,

input = jewel.input,

cursor,

firstRun = true;

function run() {

if (firstRun) {

setup();

firstRun = false;

}

...

}

function setup() {

input.initialize();

input.bind(“selectJewel”, selectJewel);

input.bind(“moveUp”, moveUp);

input.bind(“moveDown”, moveDown);

input.bind(“moveLeft”, moveLeft);

input.bind(“moveRight”, moveRight);

}

...

})();

Remember to add the new input.js script to the second stage in loader.js. You can add it just before the screen modules to make sure that it’s available for the game screen to use:

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

...

},{

load : [

“loader!scripts/input.js”,

“loader!scripts/screen.main-menu.js”,

“loader!scripts/screen.game.js”,

“loader!images/jewels”

+ jewel.settings.jewelSize + “.png”

]

}

]);

}

You can now interact with the game, but clicking the jewels gives no visual feedback and trying to swap jewels will likely just produce errors. It’s time to make the board display react to your input.

Rendering the cursor

The input module is now properly linked to the game mechanics, but the display module should also indicate which jewel is currently selected. The display module needs to keep track of the cursor position in much the same way as the game screen module. You mimic that by adding acursor object to the display module in display.canvas.js.

note.eps

The remainder of the book focuses on the graphics in the canvas display. If you want to experiment further with the DOM-based display module, I encourage you to give it a go. Trying to implement the game graphics using different technologies is a good exercise.

Access to the cursor is exposed via the setCursor() function shown in Listing 8.22. If this function is called without any parameters, it clears the cursor by setting it to null; otherwise, it updates the coordinates.

Listing 8.22 Adding the Cursor to the Display Module

jewel.display = (function() {

var cursor,

...

function clearCursor() {

if (cursor) {

var x = cursor.x,

y = cursor.y;

clearJewel(x, y);

drawJewel(jewels[x][y], x, y);

}

}

function setCursor(x, y, selected) {

clearCursor();

if (arguments.length > 0) {

cursor = {

x : x,

y : y,

selected : selected

};

} else {

cursor = null;

}

renderCursor();

}

return {

initialize : initialize,

redraw : redraw,

setCursor : setCursor

};

})();

The clearCursor() function clears the jewel at the cursor position and redraws the jewel. Listing 8.23 shows the simple clearJewel() helper function. This function simply clears a square on the canvas at the specified coordinates.

Listing 8.23 Clearing a Jewel Position

jewel.display = (function() {

...

function clearJewel(x, y) {

ctx.clearRect(

x * jewelSize, y * jewelSize, jewelSize, jewelSize

);

}

...

})();

Now you can get to rendering the cursor. You could indicate where the cursor is in many ways; I chose to simply add a highlight effect by drawing the jewel an extra time using the lighter composite operation. Listing 8.24 shows the cursor rendering.

Listing 8.24 The Cursor Rendering Function

jewel.display = (function() {

...

function redraw(newJewels, callback) {

...

renderCursor();

}

function renderCursor() {

if (!cursor) {

return;

}

var x = cursor.x,

y = cursor.y;

clearCursor();

if (cursor.selected) {

ctx.save();

ctx.globalCompositeOperation = “lighter”;

ctx.globalAlpha = 0.8;

drawJewel(jewels[x][y], x, y);

ctx.restore();

}

ctx.save();

ctx.lineWidth = 0.05 * jewelSize;

ctx.strokeStyle = “rgba(250,250,150,0.8)”;

ctx.strokeRect(

(x + 0.05) * jewelSize, (y + 0.05) * jewelSize,

0.9 * jewelSize, 0.9 * jewelSize

);

ctx.restore();

}

...

})();

Now you just need to link the setCursor() function in the game screen module to the one in the display module. Listing 8.25 shows the necessary addition to screen.game.js.

Listing 8.25 Updating the Displayed Cursor

jewel.screens[“game-screen”] = (function() {

...

function setCursor(x, y, select) {

cursor.x = x;

cursor.y = y;

cursor.selected = select;

display.setCursor(x, y, select);

}

...

})();

The cursor is now automatically updated and rendered when the user moves it or selects a jewel. Figure 8-1 shows what the cursor looks like.

Figure 8-1: The jewel cursor

9781119975083-fg0801

Reacting to game actions

The player can now select jewels on the jewel board, but the game fails when the player tries to swap two jewels because the playBoardEvents() function calls some display functions that are not yet implemented. In the next chapter, you learn how to create animated responses to the board events, but for now, you can add simpler versions that just update the board instantly.

The missing functions in display.canvas.js are moveJewels(), removeJewels(), and refill(). Listing 8.26 shows the temporary functions.

Listing 8.26 Temporary Display Functions

jewel.display = (function() {

...

function moveJewels(movedJewels, callback) {

var n = movedJewels.length,

mover, i;

for (i=0;i<n;i++) {

mover = movedJewels[i];

clearJewel(mover.fromX, mover.fromY);

}

for (i=0;i<n;i++) {

mover = movedJewels[i];

drawJewel(mover.type, mover.toX, mover.toY);

}

callback();

}

function removeJewels(removedJewels, callback) {

var n = removedJewels.length;

for (var i=0;i<n;i++) {

clearJewel(removedJewels[i].x, removedJewels[i].y);

}

callback();

}

return {

...

moveJewels : moveJewels,

removeJewels : removeJewels,

refill : redraw

};

...

})();

The moveJewels() function iterates through all the specified jewels in two separate loops, first clearing the old positions and then drawing the jewels at their new positions. It’s important to iterate through in two steps lest you accidentally clear a freshly drawn jewel. The removeJewels()function is even simpler because it just clears all the specified positions. The refill() function is just an alias of redraw() for now.

Try loading up the game now. You can now select jewels and swap them to form chains using mouse, touch, and keyboard input. It can be difficult to follow the board reactions because the changes happen instantly, but you will deal with that problem in the next chapter when you implement game animations.

Summary

Over the course of this chapter, you learned how to intercept the native input events coming from the browser and turn them into game actions. You learned about user input in desktop browsers as well as on touch-enabled mobile devices. With a few simple examples, you saw how to easily create touch-based gestures such as zooming and rotating using the multitouch capabilities in devices like the iPhone.

You also implemented a cursor object in the Jewel Warriors game and enabled jewel selection and swapping via keyboard, mouse, and touch. The game is finally taking shape and is now at a stage where there is actual gameplay. In the next chapter, you learn how to make the game more interesting by tying animation and effects to the game actions that you just implemented.