BT

Developing Motoric Games with HTML5 - The Making of VeloMaze

Posted by Raimo Tuisku on Dec 19, 2012 |

HTML5 is becoming more and more a platform for games. But in some cases the gaming domain set considerable restrictions on the way how the facilities of HTML5 can be used. This especially true for interfaces that provide access to the hardware of the used device.

In the beginning of November 2012 I ended up joining a group called copypastel and decided to share my knowledge on game development in the third annual NodeKO competition. Despite the short amount of time and my aggressive down-scoping the project, I believe the result is worth sharing with my gaming-related and technical audience. I'm also going to share the technical background of the game and how to build this stuff on top of the various web technologies. The technologies used in the game were: Node.js, express (serving static content), Socket.io (letting the client and server communicate about ball transfers back and forth), Sylvester.js (a vector library for the physics engine) and jQuery.

So what is VeloMaze? VeloMaze is maze owned by the most Node-like dinosaur, Velociraptor. Velociraptor wants the ball to move on in the maze, forever. Due to the continuum of the maze, it basically never ends. However, each time you pass a level, you are causing your next player more trouble: he or she will get another ball! Isn't that fascinating? But that is what life in the maze is all about.

The game is especially good for groups in same place and everybody has a phone. It seems to be rather common today. Here's also a pitch video of the system requirements of the game.

The most important system requirement is an accelerometer. Accelerometer is a device which measures acceleration. Devices which have an accelerometer usually return either the angle of the gravity or the gravity vector. This is possible in certain browsers, as shown in various internet posts:

You can notice from the system requirement video that some laptops have an accelerometer too. It is included in fairly new MacBook Pro laptops (mine is from 2009 and it has one) to prevent hard drive breakage when you drop the device. I believe laptop-turning-based gaming is a field many people haven't stepped at yet! The following diagram demonstrates how the application architecture was structured in very high level.

Development of the game itself was pretty easy, however full support for all the possible browser and accelerometer combinations would have been much more work than the 48 hours our team had. Therefore testing with newest Android, for example, was not done so I was positively surprised to realize that it works really well! But luck is only one of the key factors in success. In the following paragraphs I'm going to explain how the gameplay was built and what actually makes the application playable.
Reading the accelerometer is quite simple, but a bit cumbersome and the lack of standards is making it a bit harder than it should be. After trying to look for how to accomplish the task with various different platform and browser combinations in addition to having quick polls inside our team on what to support, I ended up with the following:

/* Here we see if the browser supports DeviceOrientationEvent (links to W3C).*/
if (window.DeviceOrientationEvent) {
    window.addEventListener('deviceorientation', function(e) {
      // We get angles from the event “e” and we simply convert them to radians.
      leftRightAngle = e.gamma /90.0*Math.PI/2;
      frontBackAngle = e.beta /90.0*Math.PI/2;
    }, false);
} else if (window.OrientationEvent) { // Another options is the Mozilla version of the same thing
    window.addEventListener('MozOrientation', function(e) {
      // Here we get lengths as a unit, so scaling them to angles, seemed to work quite ok.
      leftRightAngle = e.x * Math.PI/2;
      frontBackAngle = e.y * Math.PI/2;
    }, false);
} else {
    // Naturally, most people without any browser support are going to get this.
    setStatus('Your device does not support orientation reading. Please use Android 4.0 or later, iOS (MBP laptop 
is fine) or similar platform.');
}

The result works with a reasonably new Chrome and according to some people it also worked with a relatively new Safari on iOS (but not with the Safari I had at hand). I decided to avoid looking for a solution which would allow reading the accelerometer in all the possible browsers due to the fact that the coding part of Node Knockout was only 48 hours and the game mechanics were still unwritten.
I decided to use Sylvester, a vector and matrix math library for the collision detection. I could have used Box2D JS to save some time, but due to my existing experience with Sylvester and the simplicity of the needed collision tests, I used Sylvester. The code for testing if balls falls into the hole looks like this:

function checkBallHole(ball, hole, dropped) {
    // Defining the hole and ball positions as vectors using Sylvester.
    var holeVector = $V([hole.x, hole.y]);
    var ballVector = $V([ball.x, ball.y]);
    // Simply calculating the distance with the vector operation in Sylvester
    if (ballVector.distanceFrom(holeVector) < hole.r) {
      // calling the given callback with the ball position
      dropped(ballVector);
    }
}

So nothing really complicated here: if your ball center is inside the whole, the callback "dropped" gets called. This code is run on every frame, so everybody who has developed games in their past knows that this implementation allows the ball to go over a hole during a frame. However, this is not a problem as you can actually get a ball over a hole also in real life if you push it fast enough.
The game has also walls, so the collision with those needs to be detected too. Sylvester provides a way to calculate the distance from a line segment, which I used in this case. The code in all its simplicity is below.

// Calculate the impact vector in the collision of a given ball and walll
function impactBallByWall(ball, wall) {
    var ballVector = $V([ball.x, ball.y]);
    // Define the wall as a line segment (x1,y1) (x2,y2)
    var wallSegment = Line.Segment.create(
	              $V([wall.sx, wall.sy]),
	              $V([wall.dx, wall.dy]));
	
    // Calculate the point in the wall where the ball is closest (and most probably collides)
    var collisionPoint = wallSegment.pointClosestTo(ballVector)
	              .to2D(); // needed by sylvester to convert 3D to 2D vector
	
    // Then see the distance in the current frame (does not say anything about between frames)
    var dist = collisionPoint.distanceFrom(ballVector);
	
    // Naively assume that collision happens only if the distance of the ball and wall is less than ball radius
    if (dist < ball.r) {
        // Adjust this to a suitable value. Larger inverse mass means greater impact (and smaller mass)
	var inverseMassSum = 1/100.0;
	// The vector from ball to the point where it collides
	var differenceVector = collisionPoint.subtract(ballVector);
	var collisionNormal = differenceVector.multiply(1.0/dist);
	// This deep the ball is in the wall
	var penetrationDistance = ball.r-dist;
	// The ball’s velocity at the time of the collision
	var collisionVelocity = $V([ball.vx, ball.vy]);
	
        // From dot product we get the an impact speed
	var impactSpeed = collisionVelocity.dot(collisionNormal);
	
	if (impactSpeed >= 0) {
	    // Calculate the impulse. The kinetic energy gets 2-1-0.4=0.6 times smaller on each collision
	    var impulse = collisionNormal.multiply(
	               (-1.4)*impactSpeed/(inverseMassSum));
	    // The impulse affects only on the ball, because the wall were decided to be static
	    var newBallVelocity = $V([ball.vx, ball.vy]).add(
	               impulse.multiply(inverseMassSum));
	    // write the values back to the original object
	    ball.vx = newBallVelocity.e(1);
	    ball.vy = newBallVelocity.e(2);
       }
    }
}

I made several false (but close enough to the truth) assumptions when developing the ball and wall collision. First of all, the walls have zero thickness (instead of actual 5 pixels) and again, I am NOT calculating what happens between the frames. This obviously leads to the problem that balls are able to get through the walls in the game. This could be tested fairly easily by creating a line segment of the ball's movement during the frame and figuring if the ball delta segment intersects the wall segment. Then one would have to calculate the position where the ball would have collided with the wall. In the code snippet above, it would be then assigned to the variable "collisionPoint" (see the picture below).

I am a big friend of Canvas and WebGL, but we planned to use DOM and jQuery for rendering because we didn't need any of the effects in canvas or WebGL apart from rolling the ball (which would have been quite neat to do, bummers!). Using the DOM for rendering the scenery made scaling a bit hard, but other than that it was really easy to implement. I wrote the following function for drawing any sprite in our game.

    // Sets the DOM element attribute to reflect the sprite object
    setElementPosition: function(element, sprite) {
        // Sync the sprite dimensions
	sprite.width = (maze.getSquareWidth() * sprite.r * 2);
	sprite.height = (maze.getSquareHeigth() * sprite.r * 2);
	var x = sprite.x;
	var y = sprite.y;
	/* Calculate the value of the style attribute left: and top: in absolute positioning
	* So that the point (x,y) is in the center point of the sprite (makes distance calculations easy)
	*/
	var newLeft = (x * maze.getSquareWidth()  - element.width() / 2.0);
	var newTop = (y * maze.getSquareHeigth()  - element.height() / 2.0);
	// Prevent the sprite from shaking because of the constant input from the motion sensor
	// by defining a threshold for showing the ball move in the screen.
	// This is a really huge threshold and should’ve been chosen smaller for some devices.
	if (thresholded(element.css('left') - newLeft, 5) !== 0) {
	    // Set the x position of the DOM element
	    element.css('left', parseInt(newLeft) + 'px');
	}
	if (thresholded(element.css('top') - newTop, 5) !== 0) {
	    // Set the y position of the DOM element
	    element.css('top', parseInt(newTop) + 'px');
	}
	// Set the dimensions to the DOM element
	element.css('width', sprite.width + 'px');
	element.css('height', sprite.height + 'px');
	// The ball DOM element consists of several layers (all divs), so resize them all.
	element.find('div').each(function () {
	$(this).css('width', sprite.width + 'px');
	$(this).css('height', sprite.height + 'px');
	});
	// Debug information on the position of a sprite. The debug information gets shown by hitting ‘enter’.
	element.find('.location').html('('+parseInt(sprite.x*10)/10.0+','+parseInt(sprite.y*10)/10.0+')');
    },

I made a real-time scaling for the viewport of the game, which is why the width and height are calculated on every frame. That is unfortunately not visible in the game, because of the failed attempt to control browser rotation programmatically (there is no interface for that, so it needs be hacked). So we just ended up instructing people to turn off the automatic browser rotation in their phone, as shown in the image below.

So all this accelerometer reading, physics engine running and DOM rendering is then wrapped in a main loop. I placed all the main loop code in a function "update" and call it every 100 milliseconds (I know, this is not often enough, but it looked good on my machine so forgot the value there) like this.

	window.setInterval(function() { update(); }, 100);

The full source code of the client can be found here.
BTW, I am very disappointed that the new Retina MacBook Pros do not have an accelerometer (like one of our players reported), because they have SSD drives with no moving parts! So it might be the industry of laptop-turning-based games is coming to an end...  - @raimo_t

About the Author

Raimo Tuisku is an API / Integrations Developer in UserVoice, a customer communication product. He finished his masters degree in computer science last year and has been a professional programmer for 10 years. He worked for three Finnish software companies building products and secure interfaces before moving to San Francisco Bay Area. In addition to web and integrations Raimo enjoys developing 3D games, designing software architectures, building social mobile games and traveling around the world meeting new people. You can follow @raimo_t in Twitter to know more about his on-going projects in the fields of APIs, WebGL, HTML5 Canvas and mobile HTML5 games.

 

 

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread
Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2014 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT