Click here to Skip to main content
Click here to Skip to main content

UltimateVolley

By , 17 Oct 2012
Rate this:
Please Sign up or sign in to vote.

Ultimate Volley

  1. Introduction
  2. Background
  3. Online live version
  4. Features
  5. Rules of the game
  6. The HTML and CSS
  7. The basic code design
    1. A simple helper
    2. The class diagram
    3. The game algorithm
    4. Controlling the game
    5. The physical side
    6. A little gimmick: replays
    7. Constants to play around
  8. Using the code
  9. Points of Interest
  10. History

Introduction

Every German between 22 and 29 should be familiar with the game Blobby Volley. I am not quite sure if it was that big outside of Germany, but you can trust me that this game was really popular a few years ago. In this game two alien gummy bears are playing volleyball against each other. Those alien gummy bears are the so called blobs. That's why the game has been called Blobby Volley. The game itself is an advanced version of the MS-DOS game Arcade Volley, with a similar set of rules. The original Blobby Volley can be found at Sourceforge.

A screenshot of the original version can be found below:

The original version

Nowadays the game isn't so popular any more. In my opinion one of the reasons is the mostly static gameplay. The game itself is available in a second edition, and this one has also been ported to the web. The available versions, however, are still compatible in the old static feeling. Therefore some kind of fresh air was required to make the game look curly again - if you know what I mean. My version is based on the spin-algorithm a student of mine has written.

I would have loved to changed that spin algorithm, but playing the game (the student built) for about two weeks straight, I found out that the (sometimes buggy) spin-algorithm introduces the most fun I ever had in a computer game. The game itself can be a challenge, and here we already had a tournament and determined a "world champion".

Background

In this article we will go through the basic code details and the most important algorithms. I will explain the concept of the integration and splitting the game into time slices. I will also try to show a hopefully good solution for a cross-browser keyboard implementation. The code that is presented here will be a first stage code. It is experimental, such that anyone can have a look at it and play with it. In the next stage I will package the game into a nice website, which gives the user the configuration possibilities.

The last stage will actually involve hooking up the game with SignalR, such that real time communication and network games are possible. While we will discuss stage 1 here in this article, stage 2 and 3 will either be published in the next article, or will just go live someday. This depends hugely on how much time I can spend on this in the next months, considering the Intel AppUp competition and my workload.

Online live version

You can watch a YouTube video of the online demo:

YouTube video

If you want to give it a try yourself, then just go to my homepage. There you can play around with the same version as the source code package download above.

Features

This version will feature the following aspects (* = not included in stage 1):

  • Additional (unrealistic) spin physics - giving the game an additional skill factor
  • Random location (each with a different background image); currently no influence on ball physics
  • Random first serve
  • Pulse measuring with decreasing moving capabilities (if the pulse is too high)
  • The net can actually shake (when it gets hit by the ball) and has a rounded top
  • Instant replay and replay of every rally
  • Weak computer enemy
  • *Good computer enemy
  • *Online multiplayer with chat and observers
  • *Online lobby and ranking system

Most of those features are already included in the first stage. Currently the computer enemy has just been included for fun. The current implemention makes a move based on a random number without taking any knowledge / computation of the real game into consideration. This will certainly change at some point - but for now we are satisfied with this weak computer enemy.

Rules of the game

In principle it is really close to volley ball. To be more specific it is probably like what volley ball would be, if it would be just for 1 player per team. Each player can have a max. of 3 consecutive ball contacts. If the ball hits the floor its over.

Each player also has those 3 consecutive contacts when serving. Hitting the boundary is and net is allowed as often as possible. Each set is played to 25 points with 2 sets being required for winning a match. Once one player made a point he got the right to serve.

All of those rules can be changed. Some of them can be found in the constants.js file, while others must be change within the specified routines. The max. points and max. sets are set in the Game class (more on that one later). This has been done to include an easy possibility for changing the default value (from the HTML application).

Winning a set requires a minimum difference of two points between the two players. Therefore no set will ever end with 25-24. Here 26-24 could be the final score, as well as 29-27 or 27-29. It all depends which player will be the first to have a 2 point lead.

The HTML and CSS

Let's start off with some HTML. Most of the code below is pretty obvious, like the proper document type, setting the right character set or including some external style sheet. At the end of the body tag we include all our required scripts in the right order. This is where the magic will come into play.

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>Ultimate Volley</title>
	<link rel="stylesheet" href="Content/style.css" />
</head>
<body>
	<div id="beach"></div>
	<canvas id="canvas" width="1000" height="600">
		Your browser is too old. Please consider upgrading your browser.
	</canvas>
	<script src="Scripts/oop.js" charset="utf-8"></script>
	<script src="Scripts/constants.js" charset="utf-8"></script>
	<script src="Scripts/variables.js" charset="utf-8"></script>
	<script src="Scripts/enums.js" charset="utf-8"></script>
	<script src="Scripts/game.js" charset="utf-8"></script>
	<script>
		/* We'll discuss this later! */
	</script>
</body>
</html>

It should be noted that including all those scripts would not be required on a productive site. Here we would just pack all files into one and minimize it. Therefore just one request to the webserver is required and the transfer size has been reduced. So we only see two things on the page that are of interest:

  • A div-element with the ID beach
  • A canvas-element with a fallback hint

Let's have a look at the style sheet to get a feeling for the look of the page. First of all we will note, that the page uses a special font called Merge. Actually this font is not very special (usually we would include all kind of special / weird / cool fonts, because games live from typography as well as graphics in general), but it looks really nice and will give the game an unique look.

@font-face { /* Define a special font */
	font-family: 'Merge';
	src:url('fonts/merge_light.eot');
	src:local('☺'), /* To prevent old IEs from reading this */
		url('fonts/merge_light.woff') format('woff'),
		url('fonts/merge_light.otf') format('opentype'),
		url('fonts/merge_light.ttf') format('truetype'),
		url('fonts/merge_light.svg') format('svg');
	font-weight: normal;
	font-style: normal;
}
html, body {
	margin: 0;
	padding: 0;
	width: 100%;
	height: 100%; /* nice background tiles */
	background: url('background.png');
}
#canvas {
	display: block;
	position: absolute;
	top: 50%;
	left: 50%;
	margin-left: -500px; /* Center it */
	margin-top: -300px;
}
#beach {
	display: block;
	position: absolute;
	top: 50%;
	left: 50%;
	margin-left: -500px; /* Center above canvas */
	margin-top: -380px;
	height: 80px;
	width: 1000px;
	font: 36pt Merge;
	color: white;
	text-align: center;
}

We also choose an absolute positioning for our elements. The canvas element will be placed right in the middle of the screen, with the div element (that has the ID beach) sitting right above it. We also set some font size and text align statements and adjust the background of our webpage.

This alone is nothing special and really boring! So let's go over to the JavaScript side and let's have a look at all the magic happening.

The basic code design

The people who have read my article about the Mario5 game (it is available here, on the CodeProject), know already my obsession with classes in JavaScript. To not confuse any one: JavaScript is object-oriented, but object-oriented is not equal to class based. JavaScript as a (script) language is prototype based, i.e. there are no classes (even though there are some proposals for the new ECMAScript (yes, that is the official name for JavaScript) versions, to include a class keyword with all belongings).

But the prototype based programming can be used to give the programmer (us) the ability to think like we would actually have classes. This thinking actually works in two situations:

  1. We can use familiar (or reasonable) keywords, which give us the impression of working with classes
  2. We can directly use features like base functions, overrides and inheritance

Since the little helper that has been presented with the Mario5 article fulfills all requirements, we are going to use it again for this game.

A simple helper

The helper for OOP in JavaScript is placed in the file oop.js. It includes the following code:

(function(){
	var initializing = false, 
		fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
	// The base Class implementation (does nothing)
	this.Class = function(){ };
	// Create a new Class that inherits from this class
	Class.extend = function(prop) {
		var _super = this.prototype;
		// Instantiate a base class (but only create the instance, don't run the init constructor)
		initializing = true;
		var prototype = new this();
		initializing = false;

		// Copy the properties over onto the new prototype
		for (var name in prop) {
			// Check if we're overwriting an existing function
			prototype[name] = 
				typeof prop[name] == "function" && 
				typeof _super[name] == "function" && 
				fnTest.test(prop[name]) ?
				(function(name, fn) {
					return function() {
						var tmp = this._super;
						// Add a new ._super() method that is the same method
						// but on the super-class
						this._super = _super[name];
						// The method only need to be bound temporarily, so we
						// remove it when we're done executing
						var ret = fn.apply(this, arguments);        
						this._super = tmp;
						return ret;
					};
				})(name, prop[name]) : prop[name];
		}
		 
		// The dummy class constructor
		function Class() {
			// All construction is actually done in the init method
			if (!initializing && this.init)
				this.init.apply(this, arguments);
		}
		 
		// Populate our constructed prototype object
		Class.prototype = prototype;
		// Enforce the constructor to be what we expect
		Class.prototype.constructor = Class;
		// And make this class extensible
		Class.extend = arguments.callee;
		return Class;
	};
})();

Here we append an object called Class to the window element (providing, that this is pointing at that object; but since we are directly including it, the context will be the one of the current window). This object can then be used with the extend() function. So the following code snippet will create some class:

var SomeClass = Class.extend({
	init: function() {
		//The is the constructor of the class "SomeClass"
	},
        //Other methods to follow the same pattern

        /* Always call base function with this._super() within the corresponding method
});

We will make extensive use of this construct to built everything out of classes.

The class diagram

We spawn three inheritance trees directly from the Class object. Those three branches are:

  • A Resources class, where all resource managers will derive from
  • A VirtualObject class, that is the main starting point for all objects without a paint() method
  • A DrawObject class, which is the base for all classes with a paint() method

The first branch is rather uninteresting for showing. At the moment just one resource manager has been created - it is responsible for the images. In stage 2 of the project sound effects and music is going to be added - there one or two additional resource managers are required.

The second branch looks like the following diagram.

VirtualObject inheritance

Basically every object that does not implement the paint() method (and related properties like width, height etc.) derives from the VirtualObject class. This class provides any inherited class with basic string and number manipulating methods.

We will discuss the implementation of the Control class later on. We will also dive into a discussion about the Keyboard class implementation.

The last branch has the following diagrammatic expression:

DrawObject inheritance

Here we see that there are basically three important classes. The first one is the ViewPort. This one bundles all drawing objects and calls their paint() methods. There is also a special derivative of it called ReplayViewPort, which is used for displaying replays.

The next sub-branch is the Figure class. A Figure is any object, that can be moved in the game (either by interaction with another figure, or by controlling it with the keyboard). Currently (and probably forever), there are three types of figures: A Player, the Ball and the Net.

A Player is always controlled by an instance of the Control type. Therefore two other derivatives of Player have been created. Those two classes are controlled by AI / already made inputs.

The last sub-branch is the Field. Here we either have the real, BigField, which is basically the full ViewPort, and just a portion of it; a SubField. A Field has the responsibility of checking boundary conditions with its children. A Player instance would be in a SubField instance, while the Ball is always located in a BigField.

Finally everything is tied together in a Game instance. The relations here are as follows:

DrawObject inheritance

So basically everything is dependent on this instance. The players and observers are exceptions here, they are not created directly by the game, but just added. In theory there could be a number of players - but right now we are just sticking to 2. In stage 3 it could be possible that special 2 vs 2 and / or 2 vs 1 modes will be included.

The game algorithm

A Game object has many methods. The two most important ones are called play() and pause(). The first one starts the infinite game-loop and binds the specific control event handlers, while the second one stops the game-loop and unbinds the event handlers.

var Game = VirtualObject.extend({
	/* ... */
	play: function() {
		var me = this;

		if(!me.running) {
			me.running = true;

			for(var i = this.players.length; i--; )
				this.players[i].input.bind();

			me.loop = setInterval(function() {
				me.tick();
			}, LOGIC_STEP);	
		}
	},
	pause: function() {
		if(this.running) {
			this.running = false;

			for(var i = this.players.length; i--; )
				this.players[i].input.unbind();

			clearInterval(this.loop);
		}
	},
	/* ... */
});

However, those two functions would not do anything visible at all, if we would not specify what the infinite game loop should do. In our case we call the tick() function in every call.

This function first looks if the logic should be paused for some frames. This is the case after a player made a point. To not start directly (and maybe do something unexpected for the player), the logic should wait a fraction of a second.

A game in orlando

The second thing that is being executed by this function is the update of any players control. Therefore we tell all instances of Player, located in the players array, to invoke the steer() function.

After those mandatory updates we are also updating the data for the current replay recording. Then (finally!) we are performing our logic steps.

Every logic step consists of the following points:

  1. Perform the ball's logic (collision detection with the boundary, movement)
  2. Perform the net's logic (movement)
  3. Perform each player's movement (collision detection with the boundary, movement)
  4. Perform the ball's collision detection with each player
  5. Perform the ball's collision detection with the net

Those steps are being executed over and over again. Therefore we are performing an infinitesimal integration. We are doing this to prevent problems with the ball not hitting the player (if the speed is bigger than a player).

var Game = VirtualObject.extend({
	/* ... */
	tick: function() {
		if(!this.wait) {
			for(var i = this.players.length; i--; )
				this.players[i].steer();

			this.instantReplay.addData(this.players);

			for(var t = TIME_SLICES; t--; ) {
				this.ball.logic();
				this.net.logic();

				for(var i = this.players.length; i--; ) {
					this.players[i].logic();	
					this.ball.collision(this.players[i]);
				}

				this.ball.collision(this.net);
			}
		} else {
			this.wait--;
		}

		this.viewPort.paint(this.ball, this.players);
	},
	/* ... */
});

Finally we are painting the current scene by calling the attached ViewPort instance with the ball and the players to paint. The viewPort object is then doing the rest. Its paint() method looks like:

var ViewPort = DrawObject.extend({
	/* ... */
	paint: function() {
		context.clearRect(0, 0, width, height);
		context.drawImage(this.background, 0, 0);
		this.field.paint();
		this.net.paint();
		this.ball.paint();

		for(var i = this.players.length; i--; )
			this.players[i].paint();

		if(this.ball.getBottom() > height)
			this.paintCursor(this.ball.x);

		this.paintScore();

		for(var i = this.players.length; i--; )
			this.players[i].paintPulse();

		this.paintMessage();
	},
	/* ... */
});

Here we are just cleaning the current scene and drawing a new on it. So first we paint the background, then we paint the field in general, the net and finally the ball and the players. Afterwards we are painting the gauges.

Here we first start with an indicator if the ball is too high (above / outside the canvas element). Afterwards we are painting the current score for each player and also the players' pulses. We also have to paint any message like an information about a player winning the set.

Controlling the game

One of the most important aspects of any computer game is the input. If the programmer makes the input functions sloppy, or if the overall game design does not involve the adequate interactions with the game, the game itself will fail. Providing the right controls with the browser is basically quite easy. Looking much closer at some details we will realize, that a little bit of thought has still to be put into the design and implementation.

In this case our implementation starts with the base class Control. Without specifying anything about a specific binding to any input device (mouse, keyboard, touchscreen, ...) we are able to describe what the input device should actually do. In our case it is only possible to perform (or not perform) 3 actions:

  • Jumping, i.e. going up
  • Walking / Running to the left
  • Walking / Running to the right

We are providing all properties and methods for those 3 actions. Our overall design dictates the following behavior: Any input will be only written to a buffer, which will update the real values only if the update() is called. This method also saves the previous state, so that we can always compare the current values with the former values.

Why are we doing this? Well, the update() method will be called in a player's steer() method. This method is called in the beginning of any logic cycle, but not inside the time slice integration. This means: the user is only able to change the direction where his blobby is moving to all 40 ms, while the overall game resolution is much lower and independent of those 40 ms.

var Control = VirtualObject.extend({
	init: function() {
		this.reset();
		this._super();
	},
	update: function() {
		this.previousUp = this.up;
		this.previousLeft = this.left;
		this.previousRight = this.right;
		this.left = this.bufferLeft;
		this.up = this.bufferUp;
		this.right = this.bufferRight;
	},
	reset: function() {
		this.up = false;
		this.left = false;
		this.right = false;
		this.bufferUp = false;
		this.bufferLeft = false;
		this.bufferRight = false;
		this.previousUp = false;
		this.previousLeft = false;
		this.previousRight = false;
	},
	setUp: function(on) {
		this.bufferUp = on;
	},
	setLeft: function(on) {
		this.bufferLeft = on;
	},
	setRight: function(on) {
		this.bufferRight = on;
	},
	bind: function() { },
	unbind: function() { },
	cancelBubble: function(e) {
		 var evt = e || event;

		 if(evt.preventDefault)
		 	evt.preventDefault();
		 else
		 	evt.returnValue = false;

		 if (evt.stopPropagation)
		 	evt.stopPropagation();
		 else
		 	evt.cancelBubble = true;
	},
	copy: function() {
		if(this.previousUp === this.up && this.previousRight === this.right && this.previousLeft === this.left)
			return 0;

		return {
			l: this.left ? 1 : 0,
			u: this.up ? 1 : 0,
			r: this.right ? 1 : 0
		};
	}
});

Why are we saving the previous input data? Well, for this we can have a look at the copy() method. This one is used only for providing data to the replay. We will discuss the replay later on, but for now we can say that this will save a lot of memory (and probably disk space) and therefore gain performance. The reason for this is, that we can introduce a special notation if the keys did not change. Otherwise we will just provide the current state of the keys.

Another method mentioning here is the cancelBubble(). This one is a little helper to stop event propagation in all browsers. This one should make sure that keyboard inputs won't bubble up to the browser and have undefined behavior, such as reloading the page or opening a bookmark or performing a gesture (like going back to the previous page). All of those actions are a threat to a superb game-experience, which is why we have to eliminate them.

Let's have a look at a specific implementation of the Control class:

var Keyboard = Control.extend({
	init: function(codeArray) {
		var me = this;
		me._super();
		me.codes = {};
		me.codes[codeArray[0]] = me.setUp;
		me.codes[codeArray[1]] = me.setLeft;
		me.codes[codeArray[2]] = me.setRight;
		var handleEvent = false;
		this.downhandler = function(event) {	
			handleEvent = me.handler(event, true);
			return handleEvent;
		};
		this.uphandler = function(event) {	
			handleEvent = me.handler(event, false);
			return handleEvent;
		};
		this.presshandler = function(event) {	
			if (!handleEvent)
				me.cancelBubble(event);
			return handleEvent;
		};
		
	},	
	bind: function() {
		document.addEventListener('keydown', this.downhandler, false);
		document.addEventListener('keyup', this.uphandler, false);
		document.addEventListener('keypress', this.presshandler, false);
		//The last one is required to cancel bubble event in Opera!
	},
	unbind: function() {
		document.removeEventListener('keydown', this.downhandler, false);
		document.removeEventListener('keyup', this.uphandler, false);
		document.removeEventListener('keypress', this.presshandler, false);
	},
	handler: function(e, status) {
		if(this.codes[e.keyCode]) {
			(this.codes[e.keyCode]).apply(this, [status]);
			this.cancelBubble(e);
			return false;
		}

		return true;
	},
});

Of course we need to implement the bind() and unbind() methods. Usually we would only need to do this with the keydown and keyup event, but a bug (?) in Opera forces us to also bind the keypress event. If we would not do this, the event would still propagate.

In the constructor of the Keyboard class we are setting the keys of an object to the passed key codes. In the end we will just have to see if the currently entered key code is a key in the object and execute the function - which is the value of the object behind the key with the key code. The trick with the handleEvent closure variable is required to have the work-around working. Otherwise we would either always stop the propagation or never. This allows us to determine if we should stop it.

The physical side

On the physical side we have to take care of the whole energy business with elastic / inelastic collisions. Once we detect a collision the real trouble starts. Detecting a collision is really easy, since we just have to deal with circles (and our boundaries, but those can be solved very fast as well!). Detecting a collision is just a matter of adding the two x values (the first one is from the blobby; or in general from the object that is detecting if its hitting the ball - the second one is from the ball), squaring them and adding them to the square of the addition of the two y values.

This can then be compared with the square of the sum of the two radiuses. If the value is greater than the square of the sum of the two radiuses then we do not detect a collision, otherwise we have found a collision and have to put back the ball on the surface of the target. Now the tricky parts are starting!

Let's have a look at all the involved variables in a short sketch. The sketch shows a frame of the game before the actual collision is happening.

Involved physical variables

What we can see is that both (the ball and the target) have some velocity. Here we have some velocity vector, i.e. a part of the total velocity is going in x direction, the remaining part is going in y direction. We also see that the positions between ball and target have some kind of angle (called α) between each other. If α = 0 in a collision, then we have a purely horizontal collision. If α = Π/2 or 90°, then we have a purely vertical collision.

We also see that the ball has some spin property. The exact physics are somehow complicated and have not been ported 1 to 1. Therefore we will not focus on this. Let's just say the following two statements:

  1. The spin-physics part has been implemented to bring some action into the game
  2. The game would work fine without that part - but the ball would never start rotating

Therefore it is up to you to exclude the spin physics part or improve it. There are some known issues with the spin physics - but for personal reasons I do like the current implementation (maybe one bug that let's the ball climb up the wall has still to be fixed - but it's almost never happening) and it is in my opinion a (no - it's THE) fun part about the game.

The spin functions are included in the Ball class and look like this:

var Ball = Figure.extend({
	/* ... */
	changeSpin: function(vx, vy, dx, dy, sign) {
		var distance = dx * dx + dy * dy;
		var scalar = (dx * vx + dy * vy) / distance;
		var svx = vx + sign * dx * scalar;
		var svy = vy + sign * dy * scalar;
		this.omega += (svx * dy - svy * dx) / distance;
	},
	spin: function(vx, vy, dx, dy) {
		this.changeSpin(this.vx, this.vy, dx, dy, 1);
		this.changeSpin(vx, vy, dx, dy, -1);
	},
	/* ... */
});

If we alter the code to be like the following snippet (i.e. removing the changeSpin() function and removing the body of the spin() function), then we have successfully removed the spin physics from the game.

var Ball = Figure.extend({
	/* ... */
	// No more changeSpin 
	spin: function(vx, vy, dx, dy) {
		//No body!
	},
	/* ... */
});	

Where is the rest of the magic happening? Let's have a brief look at the Figure base class. There we have the three important functions:

var Figure = DrawObject.extend({
	/* ... */
	checkField: function() {
		if(this.y < this.hh) {
			this.y = this.hh;
			this.vy = 0;
		}

		if(this.getLeft() < this.container.x) {
			this.vx = 0;
			this.x = this.container.x + this.wh;
		} else if(this.getRight() > this.container.x + this.container.width) {
			this.vx = 0;
			this.x = this.container.x + this.container.width - this.wh;
		}
	},
	logic: function() {
		this.vy -= ACCELERATION;
		this.x += this.vx;
		this.y += this.vy;
		this.checkField();
	},
	hit: function(dx, dy, ball) {
		var distance = dx * dx + dy * dy;
		var angle = Math.atan2(dy, dx);
		var v = ball.getTotalVelocity();
		var ballVx = Math.cos(angle) * this.friction * v;
		var ballVy = Math.sin(angle) * this.friction * v;
		ballVx += BALL_RESISTANCE * ball.omega * dy / distance;
		ballVy -= BALL_RESISTANCE * ball.omega * dx / distance;
		ball.setVelocity(ballVx, ballVy);
	},
	/* ... */
});

We are not actually discussing where any of the last of those three methods is called. Well, we had a look at when the logic() method is called; inside the game loop, which is basically represented by the tick() method of the Game class.

The logic() method is changing the current position according to the laws of gravity and the players inputs (which are changing the velocity variables). Also it might be that the player is out of bounds. Therefore we also need to check the logic here, which means calling the checkField() function. Here we are only comparing some variables and deciding if we should reflect some velocity.

So where does the hit() method actually get called? We've already seen a call to the collision() method. This one is actually elementary. The Figure class already contains this method, but it is not yet implemented (in JavaScript that means it is there, but it has an empty body). Every child has (well obviously it has not to - but then nothing is happening) to implement its own collision() method.

This method basically looks if some collision (with the ball) is happening, with all the laws of the object, where it is implemented. If a collision is happening, the hit() function will be called with the arguments. The arguments are:

  • What is the horizontal distance
  • What is the vertical distance
  • Give me the ball!

Finally the hit function gets the properties of the current object and the ball and changes the specific properties of the ball.

A little gimmick: replays

Wouldn't it be nice if we could watch the best rallies over and over again? It's like watching the best goals, best touchdowns or best moves again. Therefore including the possibility for recording and playing replays is a must.

Saving a replay can be done with no effort, since we do not have any random numbers (else we would need to store them) in the game. For any rally we only need the following information:

  • Where is the ball in the beginning (which player did serve?)
  • Where are the players in the beginning?
  • What was the current score?
  • What was each player's current pulse?
  • What were the names of the player?
  • What were the colors of the players?

Additionally we just need to store any keyboard input. The first trial of a replay file looked therefore like this:

{"beach":"Mauritius","ballx":247.5,"bally":250,"data":[[{"left":true,"up":false,"right":false},{"left":false,"up":false,"right":false}],[{"left":true,"up":false,"right":false},{"left":false,"up":false,"right":false}],[{"left":true,"up":false,"right":false},{"left":false,"up":false,"right":false}],
/* many many more */,
"players":[{"color":"#0000FF","name":"Lefty","points":2,"sets":0},{"color":"#FF0000","name":"Righto","points":1,"sets":0}],"count":580}

In total (I saved you from reading that!) 47209 characters for only 580 frames! After a little bit of optimization a replay file looks like this:

"{"beach":"Mauritius","ballx":752.5,"bally":250,"data":[[0,{"l":0,"u":0,"r":1}],[0,0],[0,0],[0,0],[0,0],[0,0],[0,{"l":0,"u":0,"r":0}],
/* many many more */,
"players":[{"color":"#0000FF","name":"Florian","points":22,"sets":1},{"color":"#FF0000","name":"Christian","points":19,"sets":0}],"count":442,"contacts":14}"

Yeah, ok. In this file I got 4487 characters with those 442 contacts. So if we extrapolate and take the ratio, we'll see that the changes resulted in a file that contains only 1/7 to 1/8 of the original data. I also added a new property called contacts (to determine if its worth an instant replay). What else has been changed?

  • The properties of the keyboards have shorter names - left has become l, right has become r and up has become u
  • The true / false has been changed to 1 / 0 (still evaluates the same in JavaScript)
  • If the input has not been changed, a 0 is added instead of an object

Here we are abusing that objects (even if empty) evaluate to true, while 0 evaluates to false. The magic behind returning zero or not is already implemented in our Control class. Let's look now at the specific class that represents a Replay.

var Replay = VirtualObject.extend({
	init: function(beach, ball, players) {
		/* Initialize data for a new replay */
	},
	addData: function(players) {
		var inputs = [];
		this.contacts = 0;

		for(var i = 0; i < players.length; i++) {
			inputs.push(players[i].input.copy());
			this.contacts += players[i].totalContacts;
		}

		//Before starting to add data - look if the data is worth saving (no action = not worth)
		if(this.count === 0) {
			var containsData = false;

			for(var i = inputs.length; i--; ) {
				if(inputs[i]) {
					containsData = true;
					break;
				}
			}

			if(!containsData)
				return;
		}

		this.data.push(inputs);
		this.count++;
	},
	play: function(game, continuation) {
		game.pause();
		var frames = this.count;
		var index = 0;
		/* Create view for replays and fill with objects */
		var data = this.data;

		for(var i = 0; i < this.players.length; i++) {
			var bot = new ReplayBot(game, game.players[i].container);
			bot.setIdentity(this.players[i].name, this.players[i].color);
			replayBots.push(bot);
		}

		var iv = setInterval(function() {
			if(index === frames) {
				//Return to the game
				clearInterval(iv);

				if(continuation)
					continuation.apply(game);

				game.play();
				return;
			}

			/* Logic and paint methods being called */
		}, LOGIC_STEP / 2);
	}
});

The addData() method will be called from the Game instance with an array that contains all players. Then the current data from each player will be taken and a temporary array with those data points will be built. This array is then added with the exclusion of unchanged arrays, if no data has been added already. This excludes all actions before any of the player starts moving in the beginning of a new rally. This prevents saving data, which has no meaning.

The play() method starts the playing of the replay. Therefore it uses another ViewPort. It must perform its own painting, since the game loop (and therefore the painting) is paused, while watching a replay. An important part is the fact that the loop here could be also a recursive timer. This would allow the speed of the replay to be adjusted. Right now the speed of the replay is set to LOGIC_STEP / 2, i.e. double the speed of the real game. This value does not make any difference in the physics, since the real game evaluation is performed in the integration of the TIME_SLICES. The logic itself looks quite similar to the game.

So, which viewport does now actually perform the painting? Well, this is the derived version, which executes this task:

var ReplayViewPort = ViewPort.extend({
	init: function(players, container, net, ball) {
		var pseudo = {
			players: players,
			field: container,
			net: net,
			ball: ball
		};
		this._super(pseudo);
		this.setMessage(Messages.Replay);
	},
	setup: function() {},
	paintScore: function() {}
});

Basically it's the same as the usual ViewPort, but it does not paint scores and it has no setup() routine. Also it has a different constructor, which creates a new object that contains all the replay bots, the field, net and ball (all those things have been changed to something that is just used for the replay). This object is then passed to the base class's constructor. If we would use TypeScript, and we would (what we should) tell the constructor of the ViewPort to only accept game instances, then we would get a compiler error here. But luckily we are still in dynamic wonderland and this pseudo game helps us.

Replay

This is the final version - we just included the text Replay, such that everyone will realize instantly: HEY! THIS IS A REPLAY! One could also think of other things to be included; like a darkish color overlay or some funny comic graphics. But for the moment this is alright! I am also not displaying the score (as mentioned above) - but they are saved in the replay file, i.e. the score could be used somewhere else.

Constants to play around

The following is the code from the file constants.js. Here all the constants that are used by the game are defined. Go ahead and play around with those. Some of them should not be changed, like TWOPI or MIN_PLAYERS and MAX_PLAYERS. Others will result in funny movements or behaviors.

// Defines the width of the net
var NET_WIDTH = 10;
// Defines the height of the net
var NET_HEIGHT = 290;
// Converts grad to rad
var GRAD_TO_RAD = Math.PI / 180;
// Just saves one operation
var TWOPI = 2 * Math.PI;
// Time of 1 logic step in ms
var LOGIC_STEP = 40;
// Number of time slices per iteration
var TIME_SLICES = 40;
// Frames per second (inverse of LOGIC_STEP)
var FRAMES = 1000 / LOGIC_STEP;
// Sets the g factor
var ACCELERATION = 0.001875;
// The minimum amount of players
var MIN_PLAYERS = 2;
// The maximum amount of players
var MAX_PLAYERS = 4;
// The maximum amount of contacts per move
var MAX_CONTACTS = 3;
// The maximum (horizontal) speed of a player
var MAX_SPEED = 0.4;
// The maximum (vertical) speed of a player
var MAX_JUMP = 1.05;
// The (default) maximum number of points per set
var DEFAULT_MAX_POINTS = 25;
// The (default) maximum number of sets per match
var DEFAULT_MAX_SETS = 2;
// The time between two points in ms
var POINT_BREAK_TIME = 450;
// The start height of the ball in px
var BALL_START_HEIGHT = 250;
// Sets the acceleration of the ball through the player
var BALL_SPEEDUP = 0.4;
// Sets the strength of the reflection of the ball while serving
var BALL_LAUNCH = 1.5;
// Sets strength of the reflection of the ball
var BALL_REFLECTION = 0.8;
// Sets the air resistancy of the ball
var BALL_RESISTANCE = 1;
// Sets the drag coefficient of the ball
var BALL_DRAG = 0.005;
// Sets the increase per iteration of pulse
var PULSE_RECOVERY = 0.0004;
// Sets the decrease per iteration of pulse while running
var PULSE_RUN_DECREASE = 0.0005;
// Sets the decrease per iteration of pulse for jumping
var PULSE_JUMP_DECREASE = 0.17;
// Sets the size of the circles in the won-sets-display
var SETS_WON_RADIUS = 10;

If you want to change the rules of the game then just change DEFAULT_MAX_POINTS or DEFAULT_MAX_SETS. It is also fun to change MAX_CONTACTS or any of the physical constants.

Using the code

All the JavaScript files are required by the game. However, in none of those files the right commands to actually create an instance of Game and plug in players will be found. This code has to be created as well (do not worry - its already been done; but feel free to change it!). This is the code required for a simple game with two players, Lefty (on the left side) and Righto (on the right side), to work.

The keyboard can be set manually. For doing this you would need the specific key codes. The list of key codes can be found by using Google, by writing a few lines of code (either in a HTML file, directly in the browser or in any JavaScript console) or by consulting the following list:

Key Code Key Code Key Code
backspace 8 tab 9 enter 13
shift 16 ctrl 17 alt 18
pause/break 19 caps lock 20 escape 27
page up 33 page down 34 end 35
home 36 left arrow 37 up arrow 38
right arrow 39 down arrow 40 insert 45
delete 46 0 48 1 49
2 50 3 51 4 52
5 53 6 54 7 55
8 56 9 57 a 65
b 66 c 67 d 68
e 69 f 70 g 71
h 72 i 73 j 74
k 75 l 76 m 77
n 78 o 79 p 80
q 81 r 82 s 83
t 84 u 85 v 86
w 87 x 88 y 89
z 90 left window key 91 right window key 92
select key 93 numpad 0 96 numpad 1 97
numpad 2 98 numpad 3 99 numpad 4 100
numpad 5 101 numpad 6 102 numpad 7 103
numpad 8 104 numpad 9 105 multiply 106
add 107 subtract 109 decimal point 110
divide 111 f1 112 f2 113
f3 114 f4 115 f5 116
f6 117 f7 118 f8 119
f9 120 f10 121 f11 122
f12 123 num lock 144 scroll lock 145
semi-colon 186 equal sign 187 comma 188
dash 189 period 190 forward slash 191
grave accent 192 open bracket 219 back slash 220
close braket 221 single quote 222

The arguments have to be given in the following order: up, left, down. Therefore the default setting here is that the left player has a A-W-D keyboard (left-up-right), while the right player uses the arrow keys ←-↑-→ (left-up-right).

(function() {
	// Create new game
	var game = new Game();

	// Create the left boundary
	var fieldLeft = new SubField(0, 2);
	// Set up the keyboard for the left player
	var keyboardLeft = new Keyboard([87, 65, 68]);
	// Create and add left player
	var playerLeft = new Player(game, fieldLeft, keyboardLeft);
	playerLeft.setIdentity('Lefty', '#0000FF');
	game.addPlayer(playerLeft);

	// Create the right boundary
	var fieldRight = new SubField(1, 2);
	// Set up the keyboard for the right player
	var keyboardRight = new Keyboard([38, 37, 39]);
	// Create and add right player
	var playerRight = new Player(game, fieldRight, keyboardRight);
	playerRight.setIdentity('Righto', '#FF0000');
	game.addPlayer(playerRight);

	// Start game
	game.beginMatch();
})();

Let's discuss what you will find those JavaScript files.

  • oop.js contains the definition for the Class prototype and the OOP magic
  • constants.js contains the constants being used by the game - constants are always written in pure caps
  • variables.js contains the global variables used by the game - this is just the canvas and some properties
  • enums.js contains enumerations like what kind of beaches and what messages are available
  • game.js contains all classes and basically all the code

Therefore the game.js file is probably the most interesting one. I can only recommend playing around with some of the constants. It will result pretty funny effects!

Points of Interest

As I wrote this is "just" stage 1 of a three stage development process. However, I do hope that you do like the result. I already enjoyed the competition in a local multiplayer against some of my colleagues. With the (buggy, or let's say weird) spin physics, the pulse and the instant replay we had a lot of fun. It even happened once that the replay displayed a different outcome than the real game. That made me thinking: why? However, it was still a lot of fun, since my opponent was just laughing so much and kept talking about a "video proof". This video evidence thing was just hilarious!

A final version of the game will feature a multiplayer with similar capabilities as the SpaceShoot game had (Basic concept, Final implemention). However, it will most likely be written by using SignalR, since this also uses long-polling AJAX requests to handle old webbrowsers, or those with disabled websockets.

In the final version you can save the best replays locally, i.e. in the localStorage or per download. You can then either watch them with some guys online or alone. A chat lobby with a world wide gaming system and a ranking will be included. The only remaining question is:

Who will be the world champion?

History

  • v1.0.0 | Initial Release | 12.10.2012
  • v1.1.0 | Added section "Rules of the game" | 13.10.2012
  • v1.1.1 | Minor code fix | 13.10.2012
  • v1.1.2 | Some typos fixed | 14.10.2012
  • v1.1.3 | Added table of contents | 14.10.2012
  • v1.2.0 | Added YouTube video | 15.10.2012
  • v1.3.0 | Added section about the constants | 16.10.2012
  • v1.3.1 | Added entry in toc | 17.10.2012

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Florian Rappl
Chief Technology Officer
Germany Germany
Florian is from Regensburg, Germany. He started his programming career with Perl. After programming C/C++ for some years he discovered his favorite programming language C#. He did work at Siemens as a programmer until he decided to study Physics. During his studies he worked as an IT consultant for various companies.
 
Florian is also giving lectures in C#, HTML5 with CSS3 and JavaScript, and other topics. Having graduated from University with a Master's degree in theoretical physics he is currently busy doing his PhD in the field of High Performance Computing.
Follow on   Google+

Comments and Discussions

 
QuestionUncaught ReferenceError: Game is not defined PinmemberMember 1030791915-Oct-13 20:57 
AnswerRe: Uncaught ReferenceError: Game is not defined PinmvpFlorian Rappl15-Oct-13 22:33 
GeneralRe: Uncaught ReferenceError: Game is not defined PinmemberMember 1030791916-Oct-13 1:14 
GeneralRe: Uncaught ReferenceError: Game is not defined PinmvpFlorian Rappl16-Oct-13 6:39 
GeneralRe: Uncaught ReferenceError: Game is not defined PinmemberMember 1030791916-Oct-13 17:51 
GeneralRe: Uncaught ReferenceError: Game is not defined PinmemberMember 1030791916-Oct-13 17:53 
GeneralRe: Uncaught ReferenceError: Game is not defined PinmvpFlorian Rappl16-Oct-13 22:32 
QuestionMy vote of 5 PinmemberMajid Shahabfar13-Mar-13 22:15 
AnswerRe: My vote of 5 PinmvpFlorian Rappl14-Mar-13 0:36 
GeneralRe: My vote of 5 PinmemberMajid Shahabfar14-Mar-13 3:08 
GeneralRe: My vote of 5 PinmvpFlorian Rappl14-Mar-13 4:04 
GeneralRe: My vote of 5 PinmemberMajid Shahabfar14-Mar-13 10:18 
GeneralRe: My vote of 5 PinmvpFlorian Rappl14-Mar-13 10:34 
GeneralRe: My vote of 5 PinmemberMajid Shahabfar14-Mar-13 22:26 
SuggestionMy vote of 5 PinmemberMax_Power_Up26-Feb-13 4:50 
GeneralRe: My vote of 5 PinmvpFlorian Rappl26-Feb-13 6:10 
GeneralMy vote of 5 Pinmemberitaitai31-Dec-12 1:54 
GeneralRe: My vote of 5 PinmemberFlorian Rappl31-Dec-12 2:15 
GeneralMy vote of 5 Pinmembermanoj kumar choubey15-Nov-12 19:40 
GeneralRe: My vote of 5 PinmemberFlorian Rappl15-Nov-12 21:02 
GeneralMy vote of 5 PinmemberAmol_B18-Oct-12 0:00 
GeneralRe: My vote of 5 PinmemberFlorian Rappl18-Oct-12 3:30 
GeneralMy vote of 5 PinmemberMonjurul Habib16-Oct-12 10:31 
GeneralRe: My vote of 5 PinmemberFlorian Rappl16-Oct-12 10:59 
GeneralMy vote of 5 PinmemberAmbre Sachin16-Oct-12 3:29 
GeneralRe: My vote of 5 PinmemberFlorian Rappl16-Oct-12 6:49 
GeneralMy vote of 5 PinmemberRanjan.D15-Oct-12 6:52 
GeneralRe: My vote of 5 PinmemberFlorian Rappl15-Oct-12 7:22 
GeneralMy vote of 5 PinmemberMember 779801214-Oct-12 23:17 
GeneralRe: My vote of 5 PinmemberFlorian Rappl15-Oct-12 0:04 
GeneralMy vote of 5 Pinmemberfredatcodeproject14-Oct-12 7:26 
GeneralRe: My vote of 5 PinmemberFlorian Rappl14-Oct-12 9:34 
GeneralMy vote of 5 Pinmembergeekbond14-Oct-12 5:26 
GeneralRe: My vote of 5 PinmemberFlorian Rappl14-Oct-12 6:23 
Thanks! Rose | [Rose]
QuestionRe: My vote of 5 Pinmembergeekbond18-Oct-12 0:17 
AnswerRe: My vote of 5 PinmemberFlorian Rappl18-Oct-12 3:29 
GeneralRe: My vote of 5 Pinmembergeekbond18-Oct-12 3:43 
GeneralMy vote of 5 PinmemberJF201513-Oct-12 23:02 
GeneralRe: My vote of 5 PinmemberFlorian Rappl13-Oct-12 23:31 
GeneralMy vote of 5 Pinmemberswm170113-Oct-12 4:46 
GeneralRe: My vote of 5 PinmemberFlorian Rappl13-Oct-12 8:05 
QuestionNot too shabby Pinmembergstolarov12-Oct-12 17:56 
AnswerRe: Not too shabby PinmemberFlorian Rappl12-Oct-12 21:51 
GeneralMy vote of 5 PinmemberFlorian Rappl12-Oct-12 5:31 
GeneralRe: My vote of 5 PinmemberChristian Gradl12-Oct-12 10:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 18 Oct 2012
Article Copyright 2012 by Florian Rappl
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid