Gameplay in HTML5: Input

User input in Web games

Event handlers

  • Event handlers are functions which respond to events such as mouse movement and key presses.
  • While the idea is simple and the set of events reasonably small, there are unfortunately several different ways to refer to them and set their handlers.
  • See, e.g., Quirks Mode: Introduction to Events
  • jQuery and similar libraries smooth over these differences.
  • The most general way is to use jQuery's on() and off() functions to set and clear handlers.
  • The most common events have their own jQuery methods, such as mousedown().
  • These are are methods of DOM element objects, so they deal with events that happen to an element, e.g. when the mouse is over the element or the element has keyboard focus.
  • The event handler function is passed an argument representing the event that occurred.
  • Example:
        $("button").click(
            function( event )
            {
                var element = $( event.target );
                console.log( element.text() + " button clicked" );
                return false; //prevent bubbling
            }
        );
                  

Mouse events

Events:
  • mousedown
  • mouseup
  • mousemove
  • click
  • mouseleave (jQuery)
Event properties:
  • button (0=left, 1=middle, 2=right)
  • clientX, clientY
  • target

Keyboard events

Events:
  • keydown
  • keyup
  • keypress
The keypress event provides standard character codes, taking the shift state into account (e.g. 97 ("a") or 65 ("A")). But not all keys correspond to characters. For example, the arrow keys do not.
The keydown and keyup provide "raw" key codes. Unfortunately these vary across both platforms and browsers. (See Jon Wolter, JavaScript Madness: Keyboard Events.)
Jonathan Tang has written KeyCode.js, which converts the event keycodes to in a consistent, "normalized" manner.

Touch events

Events:
  • touchstart
  • touchmove
  • touchend
Event properties:
  • touches (array; each element has clientX, clientY properties)
  • targetTouches (same as above, but only touches on the element)
  • changedTouches (same as above, but only touches which changed). Need to use this for touchend.
Brian Carstensen has written Phantom Limb, which allows us to test a Web site's touch interface on a PC. Simply including phantom-limb.js converts mouse events to touch equivalents.

The Spin controller

Overview

I use the SpinScreen.js module as the controller for this game, in the MVC sense.
It responds to events by invoking methods on the model, which may change the latter's state, and informing the view of these events or changes.
The events handled by the controller include the passage of time as well as input from devices.

Input.js

There are some general-purpose functions related to input. Two useful routines help translate keyboard events.
The first relies on keycode.js to get normalized key codes (from keydown and keyup events), and then adds a string name based on this:
         function translateKeyEvent( event )
         {
             var translated = KeyCode.translate_event( event ),
                 keyCodeNames =
                 {
                     8: "Backspace",
                     9: "Tab",
                     ...
                     36: "Home",
                     37: "left",    //arrow
                     38: "up",      //arrow
                     ...
                     46: "Delete",
                     47: "/",
                     48: "0",
                     ...
                     90: "Z",
                     91: "[",
                     ...
                     123: "F12",
                     126: "`",
                     144: "NumLock",
                     ...
                     222: "'"
                 };
             translated.name = keyCodeNames[ translated.code ];
             return translated;
         }
          
The second returns the string (usually just one character) corresponding to a char code (from a keypress event):
         function translateCharEvent( event )
         {
             return String.fromCharCode( event.charCode );
         }
          

First inputs: arrow keys

Setting and clearing event handlers

In SpinScreen.js, we add functions to set and clear event handlers. (Previously one of these handlers was set in an init() function and called only once. Now we'll set it each time we visit the screen.)
         function setEventHandlers( )
         {
             $("#spinScreen button").click(
                 function( event )
                 {
                     var target = $(event.target),
                         action = target.attr( "name" );
                     app.showScreen( action );
                 }
             );
             $(document).keydown( handleKeyDown );
         }

    //-------------------------------------------------------------------------

         function clearEventHandlers( )
         {
             $(document).off( "keydown" );
             $("#spinScreen button").off( "click" );
         }
            
Notice that we have to set the key handler on the document element. Only elements which can gain focus can handle events, and, unfortunately, neither a canvas nor its containing div is eligible.

Arrow keys

For the arrow keys, we can conveniently use the event's name field (as set in our key code translator) as the direction to move() in the model:
         function handleKeyDown( event )
         {
             var evt = app.input.translateKeyEvent( event );
             if ( (evt.name === "left") || (evt.name === "right") ||
                  (evt.name === "up") || (evt.name === "down") )
             {
                 app.spin.model.move( evt.name );
             }
             return false;
         }
            
(It is important to return false here, as the arrow keys otherwise are used by the browser to scroll.)

Example

Example 1

Mouse clicks

Click handler

In setEventHandlers() we add a call to
             $("#spinBoard").click( handleClick );
            
That handler asks the view to convert the event's client coordinates to a model-style location, and then tells the model to move there.
         function handleClick( event )
         {
             var clientPos = { x: event.clientX, y: event.clientY },
                 loc = app.spin.view.clientToLoc( clientPos );
             app.spin.model.moveTo( loc );
             return false;
         }
            

Client-to-location conversion

jQuery provides a convenient offset() method for elements, which provides the offset of an element relative to the document. Since client coordinates refer to the document window, this enables us to convert to coordinates on our canvas. Then we use the tile size and margin we set in the view to get the location of the tile at that point.
         function clientToLoc( pos )
         {
             var boardOffset = $("#spinBoard").offset(),
                 relX = pos.x - boardOffset.left - margins.x,
                 relY = pos.y - boardOffset.top - margins.y,
                 locX = Math.floor( relX / tileSize ),
                 locY = Math.floor( relY / tileSize );
             locX = app.util.clampInt( locX, 0, tilesHoriz );
             locY = app.util.clampInt( locY, 0, tilesVert );
             return {
                 x: locX,
                 y: locY
             };
         }
            

Example

Example 2

Some Spin extras

Toggling clamping

Our model gives us a choice between clamping or wrapping moves. We can give the user access to this option through a hot key. Let's switch to a switch statement:
         function handleKeyDown( event )
         {
             var evt = app.input.translateKeyEvent( event );
             switch ( evt.name )
             {
             case "left":
             case "right":
             case "up":
             case "down":
                 app.spin.model.move( evt.name );
                 break;
             case "C":
                 app.spin.model.setClamping( ! app.spin.model.getClamping() );
                 break;
             }
             return false;
         }
            

Random moves

We now provide a hot key that causes the computer to make random moves. We need a module-level variable to keep track of this option:
         var colorSchemes =
             ...
             randomMoves;
            
Our key handler toggles this:
             switch ( evt.name )
             {
             ...
             case "R":
                 randomMoves = ! randomMoves;
                 break;
             ...             
            
Now we add a function to make a random move if this flag is set:
         function updateRandomMoves( time )
         {
             if ( randomMoves )
             {
                 makeRandomMove( );
             }
         }

    //-------------------------------------------------------------------------

         function makeRandomMove( )
         {
             var directions = [ "left", "right", "up", "down" ],
                 r = app.stdRandom.integer( 4 );
             app.spin.model.move( directions[ r ] );
         }
            
We call the first function in our screen's update():
         function update( )
         {
             var now = app.gameTime.getSeconds();
             updateRandomMoves( now );
             app.spin.model.update( now );
             app.spin.view.update( );
         }
            

Normal and fast random moves

Making a random move on every update is pretty fast. Let's make the default be an update around once per second and call the previous functionality fastRandomMoves. We'll need some more variables. (The interval 3/π isn't anything magic, just a number a bit less than one (second) and "irrational" so that the circles stop at a variety of places in their orbits.)
         var colorSchemes =
             ...
             randomMoves,
             randomMoveInterval = 3.0 / Math.PI,
             lastMoveTime,
             fastRandomMoves;
            
We'll add a hot key and modify the previous one:
             switch ( evt.name )
             {
             ...
             case "R":
                 randomMoves = ! randomMoves;
                 lastMoveTime = app.gameTime.getSeconds();
                 break;
             case "F":
                 fastRandomMoves = ! fastRandomMoves;
                 if ( fastRandomMoves )
                 {
                     randomMoves = true;
                 }
                 break;
             }
            
And our new update function:
         function updateRandomMoves( time )
         {
             if ( randomMoves )
             {
                 if ( fastRandomMoves )
                 {
                     makeRandomMove( );
                 }
                 else
                 {
                     if ( time >= lastMoveTime + randomMoveInterval )
                     {
                         lastMoveTime = time;
                         makeRandomMove( );
                     }
                 }
             }
         }
            

Example

Example 3

The Beach controller

Arrow keys

Key event handler

As with Spin, BeachScreen.js will be the controller for the Beach MVC, and most of the set-up is the same.
Let's set a keyboard handler that adds balls when arrow keys are pressed.
         function handleKeyDown( event )
         {
             var evt = app.input.translateKeyEvent( event );
             switch ( evt.name )
             {
             case "left":
             case "right":
                 app.beach.model.addBall( evt.name );
                 break;
             }
             return false;
         }
            

Example

Example 4

Mouse events

Click event handler

Our handler asks the view to determine which side of the board the user clicked on. Then it adds a ball on that side.
         function handleClick( event )
         {
             var clientPos = { x: event.clientX, y: event.clientY },
                 side = app.beach.view.clientToSide( clientPos );
             app.beach.model.addBall( side );
             return false;
         }
            

Client-to-side conversion

Once again we use jQuery's offset() method, and from there the computation is easy.
         function clientToSide( pos )
         {
             var boardOffset = $("#beachBoard").offset(),
                 boardX = pos.x - boardOffset.left;
             return ( boardX < canvas.width / 2 ) ? "left" : "right";
         }
            

Example

Example 5

Automation

Objective

As with Spin, we will provide an option to make things happen on the screen without user input. In this case we will automatically add balls periodically.
We will limit the number of balls that can be on the board at one time, but gradually increment this limit up to a high number and then gradually lower it again, and then go back up and down indefinitely.
We will also add an option to keep the limit high.

Key event handler

The key event handler will set some module-level variables when the hot keys are pressed.
             switch ( evt.name )
             {
             ...
             case "A":
                 automatic = ! automatic;
                 ballLimit = 0;
                 limitIncr = 1;
                 nextBallTime = nextLimitChangeTime = app.gameTime.getSeconds();
                 break;
             case "M":
                 manyBalls = ! manyBalls;
                 if ( manyBalls )
                     ballLimit = maxBalls;
                 break;
             }
            

The automation routine

This function first adds a ball, but only if a fixed time interval has passed and we are below the limit on the number of balls.
Then changes the limit, again after a fixed time interval. Note that the limitIncr changes between 1 and -1, so that the limit cycles up and down.
Like the normal-paced random moves in Spin, this sytem involves checking whether a certain amount of time has passed before doing something. Many situations in animation do the same. We have a choice between tracking the last update time, as we did in Spin, or the next update time, as we do here. Which one to use is largely a matter of preference.
         function updateAutomation( time )
         {
             var numBalls,
                 r;
             if ( automatic === false )
                 return;
             if ( time > nextBallTime )
             {
                 nextBallTime = time + nextBallInterval;
                 numBalls = app.beach.model.getBalls().length;
                 if ( numBalls < ballLimit )
                 {
                     r = app.stdRandom.integer( 2 );
                     app.beach.model.addBall( (r === 0) ? "left" : "right" );
                 }
             }
             if ( (time > nextLimitChangeTime) && (manyBalls === false) )
             {
                 nextLimitChangeTime = time + nextLimitChangeInterval;
                 ballLimit += limitIncr;
                 if ( ballLimit < 1 )
                 {
                     ballLimit = 1;
                     limitIncr = 1;
                 }
                 else if ( ballLimit > maxBalls )
                 {
                     ballLimit = maxBalls;
                     limitIncr = -1;
                 }
             }
         }
            

Example

Example 6

Homework #7

http://EpsilonDelta.us/UW_GameplayHTML5/Homework/Homework_7.html