Introduction
The interest in using HTML5 to produce awesome cross-platform games has been increased lately. Technologies like WebSockets, Canvas and others are becoming more and more stable as well as safer to use for webdevelopers. The rule is still that products should be built with technologies that will be available when the product ships - not when the product is being built. So I assume that the need for advanced game development using HTML5 in combination with CSS3 and JavaScript will increase even more over the next couple of years.
Background
I always wanted to write an awesome looking multiplayer game - but I did never have the time to achieve my goal at all areas. Either I had a really solid and fast multiplayer code, or I had a good game idea, or I had the right graphics / graphic methods. With HTML5, a world with new possibilities has come up. First of all, there are plenty of possibilities to write games. There are many kind of games possible. Some browsers have even implemented the (certainly not part of the upcoming HTML5 standard) WebGL technology. Some browsers like Google's Chrome have even the so called native client available. All those technologies make (accelerated) 3D in the browser possible.
At the moment, the most games are kind of 2D social games. They follow a simple principle and contain nice 2D graphics with an interface to Facebook / Google+ / Twitter or other kind of social networks. You'll mostly end up playing a single player that is somehow (mostly over AJAX) connected to the databases of the big sites providing you with updates on how the others do or different kinds of feedback and information. However, there is one technology that will change it all.
With the emergence of the HTML5 standard, more and more popular technologies will be standardized. One of those technologies will be the WebSocket technology. There has already been quite a lot of talk about node.js. The software has been written an order to let everyone write his on (WebSocket-) server code in JavaScript. The code will be compiled to machine code (over Google's open source V8 engine) and will handle all requests in an event based form. This certainly has an advantage if you want to program the server's code in a language similar to the client's one although it must be said that node is a different kind of technology (and I suppose you'll find that out by using it only once!).
Since I do love C# I wanted to write the server code with C#. The basis for the server code was an open source code called Fleck. It does call the necessary .NET-Framework classes and methods and supports most of the current protocol specifications. On the client side, I wanted to use the <canvas>
element. I do not want to present here a full game: the one I present here is playable (in multiplayer) and will certainly be quite a fun for a while. I build it to be extensible. It should be possible to include the features you want to. Everything that is in there was provided in order to make this also your game.
The Client Side with JavaScript
Before we go directly over to the JavaScript, we got to have a look at the HTML of the website. It is quite short and simple:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SpaceShoot SINGLEPLAYER</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="controls">
</div>
<canvas id="canvas" width=800 height=600>
Your browser does not support the HTML5 canvas element.
Please consider upgrading to a modern browser like
<a href="http://www.opera.com">Opera</a>.
</canvas>
<script src="base.js"></script>
<script src="objects.js"></script>
<script src="canvas.js"></script>
<script src="logic.js"></script>
<script src="sockets.js"></script>
<script src="events.js"></script>
</body>
</html>
It could be written a bit shorter. However, I do like the more strict XHTML style. Before I'll explain the JavaScript sources, I want to give you an insight about the CSS used to style the page. It is not much, however, we are having some possibilities here:
html, body
{
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas
{
width: 100%;
height: 100%;
background: url('bg.jpg');
background-size: 100% 100%;
}
As you can see, the first selector is kind of standard. In order to have the full page available, we tell the <html>
as well as the <body>
element to have neither a margin nor a padding set. Also we give those elements the full space that is available. In order to avoid any problems that can occur on some browsers, we set the overflow to be hidden, since we do not want to see any scrollbars or such. Now to the <canvas>
element. I tried out two possibilities:
- The canvas in the center of the browser window. Here we do have a problem if the browser window is below 800 x 600 pixels. Also on a big screen, we might have a problem spotting our spaceship or other interesting objects. This is also more far away from a classical game experience.
- This is the one that I picked: Here I set the size of the canvas to the full browser window. This has some advantages as well as one disadvantage. First of all, it fits to the classical game experience. Second it will adjust to big screens as well as small screens. However, it will also be more hungry for performance than the other approach.
I also set the background
and the background-size
rule in order to have the game's background been drawn by CSS. I am not quite sure if this saves performance compared to drawing the background image onto the canvas in each redraw cycle, but I assume it. In the end, it is nice to know that one can combine direct drawing on the canvas with CSS rules.
Enough of CSS and HTML - let's talk about the JavaScript! We have 6 subfiles here. I should note that I split up the code into several files in order to keep readability - if you want to deploy this online, then you should think about combining the files (in this order) as well as minimizing them. The files serve the following purposes:
- base.js: Here I declare the variables, constants and global helpers that are used throughout the whole code. In order to keep things simple, I keep everything globally and not in a separate namespace. Such a namespace (or a local scope) would be helpful if one considers using the code as a library or in combination with many libraries.
- objects.js: Here I do create special kind of objects (classes) that will be used frequently. Examples of those special objects are
particle
, asteroid
and ship
. The file only contains the constructor as well as the properties of these objects. - canvas.js: Here I create the function for drawing all objects. I also set the
draw()
method as a prototype
for the special objects. The big draw method will then call the draw()
method of each object. - logic.js: The file is quite the same as the canvas.js except instead of
draw()
methods I implement and call logic()
methods. Those ones are called in order to move objects, to execute the commands (direction, shoot, ...) and to evaluate if there have been collisions. Also objects return if they are still alive. If they are not, they will be moved or removed from the array that contains them. - sockets.js: This file contains the method to setup the WebSocket connection. It will also detect if there is a connection (else the
onconnect
event will not be called) or if the WebSocket
object can be used it all. If no connection is possible, then this file will not enable the multiplayer mode, but start a singleplayer game. - events.js: Here everything is wired up. This file sets the basic events and contains the loop that will execute the logic as well as the draw methods. It should be noted that usually it is a good practice to set the logic loop to a certain interval (thus guaranteeing that the logic will execute n times per second with the n controlled by you), while the
draw
method will execute as often as possible resulting in a number of frames per second that is the current maximum for the specific computer.
Instead of going through the code line by line, I will just write about some implementation details that are good to know. Since our canvas needs to have focus for getting keyboard input, we'll need to give focus to it. This can be done in the following way:
c.canvas.addEventListener('keydown', keyboard.press, false);
c.canvas.addEventListener('keyup', keyboard.release, false);
c.canvas.addEventListener('mouseover', handlefocus, false);
c.canvas.setAttribute('tabindex', '0');
c.canvas.focus();
Looks like a lot of code for just giving focus? But wait! There is more.... Actually the first two lines will just the required callbacks for the necessary keyboard events. Both callbacks are methods of the keyboard
object, which will call the manageKeyboard
function. The difference is that one calls the manageKeyboard
method with a status of true
(key pressed), while the other one is calling with a status of false
(key released).
The next lines do the following: The set that hovering the mouse over the canvas (i.e. the browser viewport) will give focus to the canvas element. So the user can give focus to the adressbar, but will return the focus to the game if he returns the mouse cursor over the game. In order to state clearly that the canvas is the first (and in this case only) element that should get focus on the webpage, we give it the tabindex
of 0 (lowest). Then we also call the focus()
method.
For drawing high quality games, you need to have high quality graphics. Those graphics need to be available at runtime so that they can be drawn on the screen. In HTML, this is an easy task to fulfill since we have the amazing <img>
element. In JavaScript, we can generate those objects without attaching them to a node. This means that the browser will load the image, but not render, i.e. display or draw, the image. For this example, I did require some images to be ready at runtime - so I had to make sure that those resources were available. The following code illustrates how to make sure that those resources are loaded:
explosionImg = document.createElement('img');
asteroidImg = document.createElement('img');
implosionImg = document.createElement('img');
shiposionImg = document.createElement('img');
explosionImg.src = 'explosion.png';
asteroidImg.src = 'asteroid.png';
implosionImg.src = 'implosion.png';
shiposionImg.src = 'shiposion.png';
var count = 4;
shiposionImg.onload = implosionImg.onload = explosionImg.onload =
asteroidImg.onload = function() {
--count;
if(count === 0)
loop = setInterval(gameLoop, 40);
};
If more resources are about to come, it would be easier to create another object that manages the resources by itself. A (simple yet powerful) approach would be to just use an array like the following:
var temp = ['explosion.png', 'asteroid.png', ];
for(var i in temp) {
var count = 0;
var t = document.createElement('img');
t.src = temp[i];
t.onload = function() {
count++;
if(count === temp.length) { }
};
temp[i] = t;
}
For this simple example, I thought my code might be more clear. So what am I prefetching here exactly? One image is for the asteroids, and three others are actually spritesheets. Spritesheets can be used to animate things since they contain a lot of (similar) images. In this case, every spritesheet has the dimension of 256 x 256 pixels, with 16 pictures in there. So each picture has 64 x 64 pixels. In order to draw the spritesheets, we need a kind of special object with a special draw method. I did create the following object:
var explosion = function(x, y, size, img) {
this.x = x;
this.y = y;
this.size = size;
this.frame = 15;
this.img = img || explosionImg;
};
The class's name is explosion
. It's constructor accepts coordinates (of the center - all coordinates in the game are center coordinates) as well as an individual size and a reference to the (spritesheet) image we are going to draw. The last parameter is optional. If the img
parameter is not set, it will have the value undefined
(resulting in false
) and set explosionImg
as the image to use. Also there is a property named frame
initialized with a value of 15. This sets the initial number of frames for the given spritesheet to 16. We will use this value in order to set the current index for the frame to display and the lifetime of the object. The object's logic is really simple and looks like this:
explosion.prototype.logic = function() {
return this.frame--;
};
So here we just decrement the framecount. The return
statement will tell the logic loop if the object is still considered alive. Here we see that a current value of 0 (decrementing to -1) will result in false
. This means that 16 frames will be displayed, the 17th frame will not be displayed with the object being disposed instead. The drawing method has the following implementation:
explosion.prototype.draw = function() {
var pos = (15 - this.frame) * 64;
var s2 = this.size / 2;
c.save();
c.translate(this.x, this.y);
c.drawImage(this.img, pos / 256, pos % 256, 64, 64,
-s2, -s2, this.size, this.size);
c.restore();
};
We set a (one-dimensional) position and use this in order to determine the position in the two-dimensional spritesheet. Here we can also see why I decided to use only center coordinates: I am performing coordinate transformations to the center of the object. This does not help so much for these objects, but they will be very helpful for the spaceship and the asteroids. With those objects rotations are required - rotations which are performed against the center of the object. Coordinate transformations help a lot for performing such tasks. If one did never see the syntax for the drawImage
method of the canvas 2D drawing context: It takes 9 parameters, stating which image to draw, with the source images x, y, width and height coordinates. The other parameters are the destination x, y, width and height coordinates. These coordinates are picked after the translation is applied. Here we change the size of the image to draw from 64 x 64 pixels to a different size x size statement.
The singleplayer
Now we look a bit more into the details of the SpaceShoot
game. I did start out with a working singleplayer, just in order to have a working code before I start coding something more sophisticated. For instance, I did use the following variables:
var c = document.getElementById('canvas').getContext('2d'),
particles = [],
asteroids = [],
explosions = [],
ships = [],
deadVessels = [],
,
multiPlayer = false;
Additionally, I also used some constants - to make the game easier to maintain. In order to work with IE9, I had to change const
with var
for the final version. However, to be more semantic, I will still present it with the const
keyword here:
const MAX_AMMO = 50,
INIT_COOLDOWN = 10,
MAX_LIFE = 100,
MAX_PARTICLE_TIME = 200,
ASTEROID_LIFE = 10,
MAX_SPEED = 6,
DAMAGE_FACTOR = 15,
ACCELERATE = 0.1;
All in all, we are ready to make our game work with implementing some logic and drawing methods as well as one big loop that forges everything together:
var gameLoop = function() {
if(gameRunning) {
logic();
items();
draw();
++ticks;
}
};
In order to give a good insight into what is actually happening in the big loop, we can have a look at the logic()
method that is being called at the beginning. Here, we have the following code:
var logic = function() {
var i;
for(i = explosions.length; i--; )
if(!explosions[i].logic())
explosions.splice(i, 1);
for(i = ships.length; i--; )
ships[i].logic();
for(i = particles.length; i--; )
if(!particles[i].logic())
particles.splice(i, 1);
for(i = asteroids.length; i--; )
if(!asteroids[i].logic())
asteroids.splice(i, 1);
for(i = ships.length; i--; )
if(ships[i].life <= 0) {
explosions.push(new explosion(ships[i].x, ships[i].y, 24, shiposionImg));
deadVessels.push(ships[i]);
ships.splice(i, 1);
}
};
With exception of the ship, everyone does execute its logic followed by returning if the object should be disposed. The ship is an exception in this due to some benefits from this approach. Here I am giving the ship the opportunity to make a move before the asteroid and other important objects get a chance (since they could also harm the ship). So I split the logic of the ship from the test if the ship is now dead to give the player a bit of an edge. As for the for
loops, I use a faster version than the more convenient for(i = 0; i < x.length; i++) {...}
. I do not want to waste any performance within the loop!
What can be noted is that once the player's own ship dies (this one has id === 1
), the player will not be able to control the game any more. Here a summary screen or something equivalent should be presented. I will have to write something nice looking before I built that in. However, in the current code, there is already some statistics being recorded like the total number of asteroids that have been hit as well as the total number of asteroids that have been destroyed. All those ship specific statistics are part of the ships' base object.
The Server Side with C#
In order to implement the server side, I used C# 4.0 in combination with an open-source solution that is called Fleck. Fleck can be integrated into any project using the NuGet package manager - just search for the term "Fleck". The advantages of using this library is that everything from Secure WebSockets to different versions of the WebSocket protocol have been implemented already. So we do not have to focus on tedious tasks like writing the basic server code and testing it - we can directly go into the section where we want to write specific server code that is directly applied to our game.
It should be noted that there are many problems arising when trying to program such a game with a multiplayer mode. What about latency? What kind of messages are sent and received? Is logic done on the client as well or only on the server? My approach is kind of simple for this example project. However, my goal is to present this approach and tell you about its advantages and disadvantages as well as how to get rid of those.
Every client will send keyboard input shortly before it gets evaluated to the server. The server then distributes the input to the other clients. So everyone will see the same actions. This has the advantage that the game remains nearly unchanged to the single player except that messages do have to be sent before logic is evaluated and except messages can be received which result in additional logic being done. However, there are several severe disadvantages - namely:
- If one client lags, he will receive the logic too late and not be able to see the actions of the opponents.
- Even worse, if messages from this client are not distributed in time n messages from one client will not be sent to the other clients. So the other clients will probably just see one message - the last one being sent. Instead of taking some turns and making some shots, only the last action will be seen resulting in a game that is out of sync.
In order to prevent the syncing problem, every spaceship is sending its current life status and its current position to the server. This data is then distributed to the other clients. This again has some disadvantages:
- One could cheat by altering the own spaceship's life and ammo properties.
- Particles that have been shot are not included, i.e., here the keyboard actions are really crucial.
Most of these disadvantages can be tackled with the following approach:
- The game's important logic is being executed on the server only in multiplayer.
- The game's not so important logic (like moving an asteroid) is also done on the client.
- The clients are only responsible for drawing and sending keyboard commands.
- The server collects one round of keyboard commands - it will not send the keyboard commands unless everyone has send the keyboard data.
- The clients loop is still send to 40 ms but does only send the keyboard information (in multiplayer).
- The rest is done if all the keyboard commands are received. So everyone will stay in sync for sure.
This approach has several advantages. First, if a new player joins, the server can give all required objects including coordinates to the client. This is possible since the server does execute logic by itself - knowing all the positions. Also the server just executes one logic step after it received all keyboard commands. So instead of relying on total time (which is sometimes good but mostly bad), it would rely on synchronized time.
This approach is more advanced and results in somehow better (also node.js would probably then be a superior choice since we could reuse objects we've written for our singleplayer logic). So in order to stay close to a simple solution, I stay with the version that has some drawbacks. BUT I promise to hand in a full working amazing multiplayer experience quite soon.
Here is the basic server construct:
class Program
{
const int GAMEPLAYERS = 2;
static void Main(string[] args)
{
var id = 1000;
var ticks = 0;
var allSockets = new Hashtable();
var server = new WebSocketServer("ws://localhost:8081");
var timer = new Timer(40);
server.Start(socket =>
{
socket.OnOpen = () =>
{
};
socket.OnClose = () =>
{
};
socket.OnMessage = message =>
{
};
});
timer.Elapsed += new ElapsedEventHandler((sender, e) =>
{
});
var input = Console.ReadLine();
timer.Close();
timer.Dispose();
server.Dispose();
}
}
This code does not look too complicated. Actually, I found it very easy and really powerful. After all, everything you have here is the basis for an awesome multiplayer game. Since I forgot to mention another disadvantage of this approach, I have to do it now. As you can see, I set a fixed number of players per game. In order to keep every client in sync with this very simple approach, I resist on starting the multiplayer game right away (when somebody connects). Instead the server waits until the fixed number of player has been reached and informs everyone that the game has now started.
To not let everyone start in the middle of the game area, I generate a random pair of numbers that correspond to x and y coordinates of the own vessel. In a more advanced server where a player could join any minute (thus corresponding to a server which also does compute some logic), it would be necessary to give the joining player a huge set of data which contains all required information about any object that is including in the gameworld.
The data is transmitted as strings (which is one of the problems of the WebSocket technology). In order to transmit meaningful data, I picked the JSON format. This one provides me with less overhead than XML and has some really nicely implemented parsers in C#. In order to stay simple, I used Microsoft's own HttpFormater
extension - it comes with objects that are enclosed in the namespace System.Json
.
The following code snippet shows what happens when a client sends some keyboard data to the server:
socket.OnMessage = message =>
{
var json = new JsonObject();
json["logic"] = JsonObject.Parse(message);
json["logic"]["ship"]["id"] = (int)allSockets[socket];
var back = json.ToString();
foreach (IWebSocketConnection s in allSockets.Keys)
if(s != socket)
s.Send(back);
};
Here I do use the JSON library. I do create a new JsonObject
and place the contents of the incoming JSON object, which was placed in a string
that can be accessed by the message
variable, in a property called logic
. Since the client does not know which ID its own ship has (own ships have the ID 1) we have to alter the ship's ID before transmitting it to the other clients. This is done by json["logic"]["ship"]["id"] = (int)allSockets[socket];
. Here we access logic.ship.id
and change the ID by the one that has been stored in the Hashtable
. Afterwards, we create one string
out of the JSON object and transmit it to every client except the one who sent the message.
I should note that collecting keyboard input and sending it once has also a scaling advantage. Here we will usually receive n messages per round (by n clients) and will have to send each one to n-1 clients. This will result to n2-n messages per round that have to be send. Since we receive n messages, the total amount of messages will be n2. This is simply too much if we want to scale this to 100 or 1000 clients (even though the gamefield is too small right now to support more than 20 players). If we consider collecting all keyboard messages and re-sending them, then we have just n outgoing messages. This results in 2n total messages or just 2/n of the original load. In the case of two, we see that this does not have any beneficial effects for our program - so another reason to stick with the simple example.
The Multiplayer
I did already write a lot about the basic handling of multiplayer events and such. Now I want to go a little bit more into the details. Let's first consider the Websocket
setup. I've written the following method:
var socketSetup = function() {
if(typeof(WebSocket) !== 'undefined') {
socket = new WebSocket('ws://localhost:8081');
socket.onopen = function() {
multiPlayer = true;
document.title = 'SpaceShoot MULTIPLAYER';
};
socket.onclose = function() {
};
socket.onmessage = function(e) {
var obj = JSON.parse(e.data);
if(obj.asteroid) {
var a = obj.asteroid;
asteroids.push(new asteroid(a.size, a.life, a.angle,
a.rotation, a.x, a.y, a.vx, a.vy));
} else if(obj.added) {
for(var i = obj.added.length; i--; )
ships.push(new ship(obj.added[i], { }, 1));
} else if(obj.removed) {
for(var i = ships.length; i--; )
if(ships[i].id === obj.removed) {
ships.slice(i, 1);
break;
}
} else if(obj.logic) {
for(var i = ships.length; i--; )
if(ships[i].id === obj.logic.ship.id) {
break;
}
}
else if(obj.status) {
ships[0].x = obj.status.x;
ships[0].y = obj.status.y;
gameRunning = true;
}
};
} else
gameRunning = true;
};
That looks like a lot of code, but there is not so much behind the scenes. First of all, it is checked if the type of the WebSocket
object is known. If the type is not known, then there is only one possibility: The browser does not support the WebSocket
object! This can either be the case for old browsers or for browsers which have decided to wait until the specification is fully standardized. On such systems, we will directly go into singleplayer mode.
Otherwise we create the socket (i.e., the connection) and tell the browser what to do when we have a running connection. In this case, the onopen
event is really important. It is worth noting that WebSocket
connection will remain open until they are closed by the browser. This is the case if you change the website, close the browser or tell the socket
object explicitly to close. From the other two events, the onclose
one is not so interesting. This one will be called if the connection is closed - independent of the origin (could be closed from the server as well). The onmessage
event is more important. Here, we will do everything in order to distinguish between the different kinds of messages that the server can send us. Usually one determines a special kind of property like "type
" that contains a special meaningful string
that helps differentiating between the types of messages. The selection is done by a big switch-case
block.
Here I did something different (again). Since I just have a limited amount of data, I select by using that undefined properties have always a null
value which casts to false
. That distinguishes between the addition of a ship, the exit of a competitor, the addition of an asteroid and logic by other ships. Also the status that was mentioned before is included here.
Using the Code
In order to use the code, you do not have to do a lot of things. However, I need to distinguish between the singleplayer mode and the multiplayer mode. The singleplayer mode should run in your browser (if it is not too old!). I tested the game in the following browsers (singleplayer mode):
- Microsoft Internet Explorer 9+
- Opera 11.5+
- Google Chrome 16+
- Apple Safari 5+
- Mozilla Firefox 7+
Since the singleplayer mode is trivial to play around with - let's take a look at the multiplayer mode. First of all, you need to know that in order to run on your webserver
, you probably need to set certain firewall settings. Even if you set them, you should take into consideration that the client side probably sits also behind a firewall (from the local router, your computer, ...). So in order to guarantee a connection, you need to choose your port as well as your testing methods carefully. Otherwise you can do the following:
- Open the provided Visual Studio 2010 solution.
- Change the line with
new WebSocketServer("ws://localhost:8081");
the port you want to pick (instead of 8081). - Open the file sockets.js.
- Alter the line with
new WebSocket('ws://localhost:8081');
by replacing both, the port you want to use (instead of 8081) and the IP address or DNS name of the computer (e.g. replace localhost with 192.168.0.1 or example.com). - Host the HTML/CSS and JavaScript files either on a local server or on the internet.
- If you execute the server program in a local network, then serving the HTML code over the internet does not make much sense - however, it should still work if you execute the page locally.
The last two points are quite non-trivial. Why do you still need to host the page on a server (e.g. localhost) instead of hosting the website in the filesystem? The problem is security. Most browsers do have a security setting preventing websites to execute things when being hosted on the local filesystem. Google Chrome does not WebSocket
requests from locally hosted websites. The same is true for most XHR requests and other features (like WebWorkers
). Some browsers offer options in order to adjust those settings. Since I don't know your browser, I just assume you have a browser that is capable of WebSockets
and give you a set of instructions that should work for sure!
About performance: If the game does not run smooth on your computer, you should consider resizing the browser window or taking a different CSS setting. In the CSS file provided, I included another setting. You just have to change the first line with the second line:
<canvas id="canvas" class="stretched" width=800 height=600><!--
<canvas id="canvas" class="centered" width=800 height=600>
<!--
The full singleplayer demo can be viewed live at http://html5.florian-rappl.de/SpaceShoot/.
Points of Interest
I think the code contains a number of things that are quite important or good to know. I also think that the project still leaves room for innovation and improvement. As I stated several times: Security is an issue. Right now, anyone with decent JavaScript skills can break this code and cheat without limits. This is certainly one of the more advanced topics - how to secure those applications.
I really wanted to do something with WebSockets
. This was certainly one of the incentives to write this article. The other one was to write a non-trivial game that is still simple just by using the canvas technology. I was really curious about the performance of canvas on several devices (My PC, MacBook Air, WindowsPhone 7 and iPod Touch (3G)). The canvas is still in the early stages - I am impressed by the performance on the big machines but disappointed with the performance on the small ones.
Acknowledgements
Thanks to Vangos Pterneas for his great article about Kinect & HTML5 using WebSockets and Canvas. That article did contain the information about the Fleck project, which I used for this project. The article can be found here.
Also I do have to say thanks to Jason Staten for creating Fleck. The project is hosted on GitHub and can be found here. I prefer NuGet to include the additional functionality to my projects.
Further Articles
Since the game does not contain final statistics, audio and gauges besides the two status bars, there is a lot of space for improvement. Currently I plan on writing more articles on writing code for providing informative gauges and implementing audio in a nice and unobtrusive way. I will keep this article updated with major improvements on the game and links to other articles that will be related in some way.
History
- v1.0.0 | Initial Release | 14.01.2012