Introduction
This is the third article in the SpaceShoot series. The first one (which can be found here)
discussed the principle of the game with a very easy and not very robust implementation. The second one (which can be found
here) showed one possible way of integrating a fun single-player using JavaScript with
APIs provided by the upcoming HTML5 standard. In this article we will discuss the final implementation of the multiplayer server in C#
using Fleck build by Jason Staten.
We will have a look at the basic concepts behind the server's structure, the object oriented design of the match and the ability to give the server commands over the WebSocket protocol.
We will also have a short look at the modified JavaScript and the lessons from the whole project. There is still the possibility of a fourth article in this series,
where the topic would be a mobile implementation of SpaceShoot. That one would be most concerned about the changed UI and possible performance tweaks.
However, a release date for that article is at the moment hard to predict, since the right ideas as well as the need for the mobile implementation are quite limited.
The game can be found (and played) at html5.florian-rappl.de/SpaceShootMulti/.
Video and Background
We made a short video of one of our battles. Even though it is a little bit over 12 minutes long only a short look should be sufficient in order to grasp the basic
concept of the game itself. The video is hosted on YouTube:
The so-called HTML5 standard is generating more and more attention. This is incredible considering that the official standard will not enter the final status in the next years.
This is also quite amazing due to the fact that some of words or techniques often mentioned in the context of HTML5 are not included in the official W3C standard.
Regardless of those two valid points against any HTML5 article, we will discuss the option of creating a game server in C# that builds the basis for a game that will
be viewed in a web browser with a JSON compatible scripting engine. Therefore we will be on the edge of current technology.
It is possible to include other systems as well. The web browser is just an obvious pick, since it provides platform independence and a very rich API. It is also
a system that includes a lot of debugging tools. Those tools are not as excellent as the ones provided by Visual Studio, however, they are sufficient in order to know
where the problems occur and to get an idea about of to fix them. One of the problems in a seperated client/server application is always the debugging of the whole application.
Since client and server are separated usually two debugging procedures have to be done. One way around this mess is the usage of test driven design for at least the server application.
There are some JavaScript framework that help building up test driven design in JavaScript. Those frameworks are (in my opinion) quite well written, but too limited due from
the JavaScript side or the current JavaScript IDEs. This article will not focus on test driven design.
We will focus on the techniques used with C# as a language and the .NET-Framework. We will investigate some of the most interesting methods and classes and explain the basic
construct of client-server-interaction for this game. We will also have a look at the basic server construct including an advanced command pattern to execute methods over the
command line or other ways (web pages, GUIs, ...). Finally we will analyze the changes in the JavaScript files for the SpaceShoot website.
Browser compatibility
The game has been tested with the latest versions of the major browser vendors. Excluding Microsoft's IE 9 every major browser is capable of the WebSocket technology.
However, Mozilla's Firefox and the Opera web browser do require further steps in order to activate this technology.
Information for activating WebSockets in the latest version of Opera can be found
at techdows.com/2010/12/enable-websockets-in-opera-11.html. For (some versions of)
Mozilla Firefox, a similar documentation can be found like the one available
at techdows.com/2010/12/turn-on-websockets-in-firefox-4.html.
Technologies
This project did already contain quite a lot of different technologies. Right now it is just the time to mention the technologies used when designing the server:
- Fleck for driving the WebSocket with C# and the .NET-Framework.
- JsonValue by Microsoft for giving us a simple JSON API.
- A lot of reflection (commands, properties, collision, ...)
- Some .NET 4.0 features like the
Complex
class - A little bit of test driven design to implement some methods correctly without headaches and provide some tests for future changes
During the development some problems with Fleck did come up. Therefore I lookup up the source code available at GitHub. Recognizing no mistake there
I tried an update with Nuget, realizing I was working with an old version. This update was therefore essential, because it alleviated me from most of my troubles.
Interfaces
A lot of interfaces have been used in order to distinguish between different abilities of the objects available in the game. There are five central interfaces:
IExplodable
requires that the inheriting object implements an explosion (sprite) number.IOwner
marks the inheriting object as ownable including a property that stores the owner.IVelocity
flags the object as moveable and gives the object properties that contain the velocity in x
and y
direction.IParticleUpgrade
for creating identifiable particle upgrades and implementing a method to perform the upgrade.IShipUpgrade
is used by atomic ship upgrades for being identifiable and implementing methods for loading and unloading upgrades.
Three out of the six core interfaces deal with upgrades. Let's have a look at the infrastructure of those upgrade-lists:
We see that every upgrade list inherits directly from the abstract upgrade list class Upgrades<T>
. This class gives the freshly created concrete
upgrades class a specialized list and some useful methods like a virtual Logic()
method as well as Add()
and Clear()
. The generic
type T
that has to be set by inheriting from Upgrades<T>
has a constraint to be of type IUpgrade
. This interface is the parent
of both, IShipUpgrade
and IParticleUpgrade
, and will be the parent of other upgrade interfaces to come.
Next to the distinguishing and extending interfaces are those for characterizing possible collision partners. One of the problems of the JavaScript code was to extend
the logic()
method of every new object in order to cover possible collisions. With C# and the possibility to manage this with reflection and globally (by inheritance)
we are much more productive. All in all every object just has to inherit the right interfaces to mark itself collidable with certain other types of objects:
public class Ship : GameObject, IAsteroidCollision,
{
}
public class Asteroid : GameObject, IShipCollision,
{
}
So here we are not only marking the ship as collidable with an asteroid, but we have to mark the asteroid as collidable with a ship as well.
This is necessary since we are not performing n2 - n collision detections with our n objects, but just (n2 - n) / 2 collision
detections, i. e. every collision is checked only once. By inheriting from the corresponding interface (like IAsteroidCollision
) the class has to implemented
one method: OnCollision()
. The method name is the same for all collision interfaces, however, the argument is different and related to the kind of object
the class is colliding with. By implementing IAsteroidCollision
the method's signature would be void OnCollision(Asteroid asteroid)
.
The method to check for possible collisions is coded in the Match
class. Here we have the Next()
method to perform all logic steps.
It contains the following code snippet:
Parallel.For(0, N, i =>
{
for (int j = i + 1; j < N; j++)
if (Objects[i].CheckCollision(Objects[j]))
PerformCollision(Objects[i], Objects[j]);
});
We do use the parallel-for loop brought into gameplay with the .NET-Framework 4. The usage is quite straightforward. All in all we are able to easily transform
any for-loop into parallel-for if we have no dependencies at all. Otherwise it gets a little bit more complicated. The magic is then performed by using a lambda expression.
The CheckCollision()
method is available in the abstract GameObject
class. Some special objects like the bomb override this method,
just to wrap it including a more specialized condition for checking collisions.
The PerformCollision()
method is being called once a collision seems possible. The possibility is calculated using just coordinates and sizes
of the two corresponding objects. Now we need to know if a collision is also possible from the types of objects:
public bool PerformCollision(GameObject source, GameObject target)
{
var t_source = source.GetType();
var t_target = target.GetType();
var coll_source = t_source.GetMethod("OnCollision", new Type[]{ t_target });
if (coll_source == null)
return false;
coll_source.Invoke(source, new object[] { target });
return true;
}
So here we use reflection to check if the source object contains a method called OnCollision()
which accepts the specific target as parameter.
If so then we invoke this method using the target.
Another area where reflection is really useful is the generation of upgrades. We use a static method Generate()
in the UpgradePack
class.
The method does not know any upgrades it can generate. However, it knows that any upgrade has to implement the interface IUpgrade
.
Knowing this we can generate a random number that represents a specific class to use. Then an instance of that class is created.
public static UpgradePack Generate(Match game)
{
var ran = game.Random;
var up = new UpgradePack();
up.X = ran.Next(0, game.Width);
up.Y = ran.Next(0, game.Height);
var types = Assembly.GetExecutingAssembly().GetTypes().Where(
m => !m.IsInterface && m.GetInterfaces().Contains(typeof(IUpgrade))).ToList();
var idx = ran.Next(0, types.Count);
var o = types[idx].GetConstructor(System.Type.EmptyTypes).Invoke(null) as IUpgrade;
up.Upgrade = o;
up.Type = o.ToString();
return up;
}
The advantage of using reflection here is the reduced amount of code maintenance. Without reflection we would need to register any acceptable class
in the UpgradePack
's instance(s) or singleton instance. Therefore we would need to write code on multiple places instead of just adding
or removing classes with the appropriate inheritances.
Classes
Different classes have been written in order to construct a nicely object oriented approach. Every game object inherits from the abstract base class
GameObject
, which already contains quite a lot of useful implementations. Every class has to implement its own Logic()
method.
The class diagram looks like the following:
We can recognize some of the (known) objects from the game. With Particle
and Bomb
we have the two types of weapons.
The ships are represented by the Ship
class. Asteroids have their own class. Every upgradepack has to inherit from the abstract class
Pack
. This is used by reflection to dynamically register and generate such packages. The packs can only collide with ships.
Therefore the interfaces shown in the diagram do have a crucial role.
It is also interesting to note that not only real objects inherit directly from the abstract class GameObject
, but also another type
of object: GameInfoObject
. Classes that inherit directly from this layer are used only for information. Here we have the InfoText
(for displaying texts) and Explosion
(for displaying explosions). There is one big difference to the other game objects: While normal game objects
perform the logic steps on the server, the game info objects perform their logic on the client.
This means that game info objects are only created on the server. They are removed after they've been sent out to the clients. The clients will do the logic
steps for those informative (but non-interacting) objects. This trick helps us reducing the amount of work-load on the server, while still being able to produce
a game that includes texts, explosions and other objects. We will now have a deeper look at on of the game objects shown above:
So let's take the class to be used for particles. We see that all corresponding constants are now (compared to the JavaScript version) in the correct place.
We have numerous properties and important methods. The most important method is of course the Logic()
method, which is called at every logic step.
There are actually to methods to create a new instance of this class. One possibility is quite standard over the constructor. Another (most often more useful)
possibility is provided with a static Create()
method. The last one accepts some parameters and performs a lot of important tasks.
The ArtificalIntelligence()
method is only used when the particle is in the boomerang mode. This is the case with the help
of a special upgrade. Instead of a linear route another more focused and specialized path is followed. In order to determine
this path the ArtificalIntelligence()
method is required.
Next to the (required due to interface implementation or due to the logic) properties we have the very important ToJson()
method. This one must
be implemented in any class which inherits from GameObject
. While the classic ToString()
method can be implemented in any object (giving a proper
string representation of the instance back), this one has to be (giving back a proper JSON representation). We will call this method in any included object right before
sending out the whole game state to every client.
Commands
Every command has to implement the interface shown above. This guarantees invocation as well as other required methods. With the implementation
of the shown methods it can be determined if a command can be executed. It is also possible to undo previously executed commands. It is worth noticing
that the commands are completely independent of the console. They do not write any output to the console, i.e. they can be used with a variety of possible
views, e.g. webpages, forms or the already mentioned console.
All commands are brought together in the singleton class Commands
. In order to talk to the current instance we just have
to call Commands.Instance
. The private constructor of the Commands
class looks like the following:
private Commands()
{
undoList = new Stack<ICommand>();
undoCallings = new List<string>();
var icmd = typeof(ICommand);
var cmds = Assembly.GetExecutingAssembly().GetTypes().Where(m => icmd.IsAssignableFrom(m) && !m.IsInterface).ToList();
commands = new ICommand[cmds.Count];
for(int i = 0; i < cmds.Count; i++)
commands[i] = cmds[i].GetConstructor(System.Type.EmptyTypes).Invoke(null) as ICommand;
}
In order to invoke any command (from the command line, or any other text-based interface) we can use the Invoke()
method of the Commands
class.
Let's have a look at the concrete implementation of this method:
public bool Invoke(string command)
{
string _origcmd = command;
var cs = command.Trim().Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries);
command = cs.Length > 0 ? cs[0] : string.Empty;
var args = new string[0];
if (cs.Length > 1)
args = cs.Skip(1).ToArray();
foreach (var cmd in List)
{
if (CheckForCall(cmd, command))
{
if (cmd.Invoke(command, args))
{
if (cmd.CanUndo)
{
undoList.Push(cmd);
undoCallings.Add(_origcmd);
}
last = cmd;
return true;
}
return false;
}
}
return false;
}
So what is actually going on here? First any text command is normalized so that additional (non-required) spacings are removed. Afterwards the first element
from the list of items or an empty string is taken as the actual command. The rest is then saved as optional arguments. Finally we iterate through our list with
commands and look for a calling match. If we found one we invoke the corresponding command. The command is then evaluated for being undo-able and saved as last
called command. We return true to show the calling method that the command has been successfully executed. Otherwise we return false to signal a failure in either
the command itself or the arguments.
How is it possible to use the Commands
class from the web? By connecting to the running server via the WebSockets protocol we can
not only play games with the server, but also post commands to it. The commands will be redirected to the Commands.Instance
object and invoke
the Invoke()
method. To achieve this the following code snippet is used (located in the MatchCollection
class):
public void UpdatePlayer(IWebSocketConnection socket, string message)
{
var j = JsonObject.Parse(message);
if (j.ContainsKey("cmd"))
{
switch (ParseString(j["cmd"]))
{
case "console":
if (socket.ConnectionInfo.ClientIpAddress.Equals(IPAddress.Loopback.ToString()))
{
if (Commands.Instance.Invoke(ParseString(j["msg"])))
socket.Send(ReportConsole(Commands.Instance.Last.FlushOutput()));
else
socket.Send(ReportConsole("Command not found or wrong arguments."));
}
else
socket.Send(ReportConsole("Not enough privileges for sending commands."));
break;
}
}
}
So the only thing one has to be sensitive about is the question who should be able to send commands to the server. Right now only locally connected players
(we can also call them clients) can send those commands. This seems like a big restriction. However, this gives us the possibility to write a web page which performs
the communication (locally) and shows us the results. In order to ensure security such a website should have a proper login with a secure authentication process.
Implementing AI
The idea of including some nice bots is usually not a bad one. In this case we will follow an interesting approach, which is a solid basis for further upgrades. The bots from this server will work over the Keyboard
class. Every Ship
has an instance of this class in its portfolio. The difference between a bot and a human is given over the IsBot
property of the Ship
class. In case of a bot the Logic()
method of the current Ship
will call the Automatic()
method of the corresponding Keyboard
instance. This method looks like the following:
public void Automatic()
{
Reset();
if (Ship.Respawn)
{
Shoot = true;
return;
}
switch (Ship.BotLevel)
{
case 1:
MonteCarlo();
break;
case 2:
MonteCarlo();
ForceBot();
break;
case 3:
case 4:
GoodBots();
DeployBomb();
break;
case 5:
UltraBot();
DeployBomb();
break;
}
}
The first thing we notice is that any previously set command (or keyboard key) will be unset and that bots will always directly respawn (not waiting for a better moment or having a coffee break). We also see that some methods are being used in order to distinguish between different kinds of bots. This is kind of interesting and needs to explained a little bit further.
In a usual game a lot of different players attend. We have some really bad ones, some really good ones, and a lot of average players. Of those average players the majority is somewhat OK, while a few of them are quite good. Others might be mediocre. So all in all we have a distribution as shown in the next figure.
In order to set a fixed level for every bot the level has to be determined at creation. Since the .NET random generator does only give us uniformly distributed pseudo random numbers we have to use an external library or write our own method. In this case we can do the last one, since it is not that much of a problem and we can control any overhead. There are actually more possible solutions in order to create a normal pseudo random number generator from a uniform one.
One of the most used solutions is the coordinate transformation, where two uniform random numbers will be created in order to create (actually two and we could and should store the second one for further usage, but this has been omitted until now) a normal random number. In order for this to work we use that exp(x2)exp(y2)=exp(x2+y2) and substitute x2+y2 by r2. The full code looks like this:
double GaussianRandom()
{
var u = 0.0;
var v = 0.0;
var r = 0.0;
do
{
u = 2.0 * Game.Random.NextDouble() - 1.0;
v = 2.0 * Game.Random.NextDouble() - 1.0;
r = u * u + v * v;
}
while (r == 0 || r > 1);
var c = Math.Sqrt(-2 * Math.Log(r) / r);
return u * c;
}
There are faster implementations out there. However, this is one of the easiest to implement and since it is not used that often (for every bot just once), we can easily afford the overhead.
The concept
The basic concept of having a secure server to prevent cheating while making the code readable for anyone is displayed below:
The solution to the security (cheating) issue comes with a price tag: We have to do all the hard work on the server. Otherwise we could never
know if everyone still sees the same game. Another point is that this construction gives us some robustness regarding synchronization and latency. Right now we are doing the following:
- If a player joins the game (or hosts the game) he gets the full information of the game, i.e. every object with all information. The client then constructs
the game based on this information.
- If a player presses or releases a key the updated keyboard information will be send to the server.
- Every 40ms a logic step is executed on the server. This logic step uses the current keyboard information from every user.
- After the logic step is completed the changed game state will be sent to every client. The clients have to remove objects that will be removed from the
next round on, as well as add new objects. New objects can be either new players (ships), asteroids, (upgrade-) packages etc.
- If a player leaves the game his ship is being marked to be removed. It will then be removed after the next logic step.
This means that we do not care if a player sent us current keyboard information or not. We do also not care if a player received the current status.
In general every player is responsible for his own connection. If the connection is not good (fast) enough he has to suffer from it -- not the other players.
This also means that most of the work is done on the server. Only a few tasks can be done on the client. One of those tasks is the fading of information messages.
The fading is not controlled by the server. Here the server is just responsible for creating the information texts. After the text has been sent to the client
it is immediately removed from the server's memory. The client is then responsible for not only drawing it - but also for it's logic steps.
Those logic steps are synchronized with the message receiving from the server.
The reason for this is simply that the decay of the message should be bound to the game or the drawing of the game. The drawing as well as the game
itself is bound to the game's logic, which takes place on the server. Therefore we can say that we just replaced the client's logic loop with a server's logic send-loop.
If we bind the client's (small) logic routines to the network receive method, we indirectly bind those routines to the original logic. To say it short: It just looks more consistent.
Some JavaScript modifications
In order to support the modified server completely a different structure was required for the client's JavaScript code.
Next to obvious methods like the ones that wire up the menu (host, join, ...) we have to include different methods for starting the game. The singleplayer
is focused on a game loop which is then called all 40ms. Now this loop is sitting on our server. We only receive the message sent by the server in an interval
of about 40ms. Right after joining (or hosting) a multiplayer game we receive a big package from the server containing the game's current state. This message
is used in order to initialize the game:
game.startMulti = function(data) {
game.reset();
game.multiplayer = true;
document.title = 'Multiplayer - ' + DOC_TITLE;
for(var i = 0, n = data.ships.length; i < n; i++)
ships.push(new ship(data.ships[i]));
myship = ships[ships.length - 1];
for(var i = 0, n = data.asteroids.length; i < n; i++)
asteroids.push(new asteroid(data.asteroids[i]));
for(var i = 0, n = data.particles.length; i < n; i++)
particles.push(new particle(data.particles[i]));
for(var i = 0, n = data.packs.length; i < n; i++)
packs.push(new pack(data.packs[i]));
for(var i = 0, n = data.bombs.length; i < n; i++)
bombs.push(new bomb(data.bombs[i]));
game.running = true;
loop = true;
};
The variable loop
is just set to be true
. This is just to prevent any errors. Before this variable was set to the pointer of the interval
that has been executed with the GameLoop
. In cases of the game being stopped, the loop
was set to null
(equivalent to false
)
right after the interval was cleared. We see that only initialization is done in this method. There is no drawing method whatsoever being invoked.
Along with the method to initialize the multiplayer game we require another one that is being called on receiving a data package. This one looks like this:
network.receive = function(e) {
var obj = JSON.parse(e.data);
switch(obj.cmd) {
case 'next':
game.continueMulti(obj);
break;
case 'chat':
chat.append(obj);
break;
case 'info':
infoTexts.push(new infoText(obj.info));
break;
case 'list':
network.setList(obj.list);
break;
case 'current':
game.startMulti(obj);
break;
case 'error':
network.error(obj.error);
break;
case 'console':
console.log(obj.msg);
break;
}
};
So we basically just decide what kind of message is being sent by the server and execute the appropriate method. We have already seen the corresponding method
in the case of current. Now we are interested in the method that is mostly called, i.e. when we receive a package that is of type next:
game.continueMulti = function(data) {
if(!loop || !game.multiplayer)
return;
network.gauge.value = network.measureTime();
var length, i;
length = explosions.length;
for(i = length; i--; )
if(!explosions[i].logic())
explosions.splice(i, 1);
for(i = data.explosions.length; i--;)
explosions.push(new explosion(data.explosions[i]));
length = ships.length;
for(i = length; i--;) {
if(!data.ships[i] || data.ships[i].remove)
ships.splice(i, 1);
else
ships[i].update(data.ships[i]);
}
for(var j = length, n = data.ships.length; j < n; j++)
ships.push(new ship(data.ships[j]));
gauge.speed.value = myship.boost;
chat.logic();
statistic();
draw();
network.gauge.draw();
};
Here most of the work is to iterate over all the arrays, either updating, deleting or adding entries. After the incoming data was analyzed we have to do
a little bit of logic on the client as well as perform some drawing. The last thing is quite important - after all it is the only location where drawing
is performed in the case of a multiplayer game. The only question is now how interactivity is achieved. The answer lies in a small modification
of the manageKeyboard()
method as displayed here:
var manageKeyboard = function(key, status) {
if(chat.shown)
return false;
if(game.multiplayer)
keyboard.send();
return true;
};
The keyboard.send()
method is one of the required network methods. It is just a wrapper for the network.send()
method including
the keyboard
object and the right identifier (cmd
). The code snippet should be enough in order to fully understand what is going on:
keyboard.send = function() {
network.send({
keyboard : keyboard,
cmd : 'keys',
});
};
network.send = function(obj) {
network.socket.send(JSON.stringify(obj));
};
The Server class
The class which creates the actual binding of our program with Fleck (which gives us a nice access layer to the WebSocket technology by using TCP/IP) is called SpaceShootServer
. Here we create our instance of the MatchCollection
class to give us a lobby and a lot of possibilities. We also save the startup time. The server is then controlled by the methods Start()
, Stop()
and Restart()
. The code has the following layout:
public class SpaceShootServer
{
#region Members
MatchCollection matches;
WebSocketServer server;
DateTime startTime;
#endregion
#region Properties
public DateTime StartTime
{
get { return startTime; }
}
public MatchCollection Matches
{
get { return matches; }
}
public bool Running
{
get { return server != null; }
}
#endregion
#region ctor
public SpaceShootServer()
{
Construct();
}
#endregion
#region Methods
private void Construct()
{
matches = new MatchCollection();
server = new WebSocketServer("ws://localhost:8081");
}
public void Start()
{
startTime = DateTime.Now;
server.Start(socket =>
{
socket.OnOpen = () =>
{
matches.AddPlayer(socket);
};
socket.OnClose = () =>
{
matches.RemovePlayer(socket);
};
socket.OnMessage = message =>
{
matches.UpdatePlayer(socket, message);
};
});
}
public void Stop()
{
server.Dispose();
server = null;
}
public void Restart()
{
matches = null;
server.Dispose();
GC.Collect();
Construct();
Start();
}
#endregion
}
All those public methods are controlled by the commands as presented above. It is possible to restart the server or stop the execution at all. The Start()
method has been invoked by our program in the beginning. The most important part in the SpaceShootServer
class is certainly the event binding. Here we use some lambda expressions in order to set the properties appropriately.
Another important issue when using the Fleck library as the responsible layer for the Websocket access is the restriction to strings as valid arguments for sending data. It is true that the specification just allows strings (which are serialized to bytes for transport anyway). However, we are only interested in a certain kind of strings: strings build out of JSON objects. Therefore we implement the following extension method:
public static void Send(this IWebSocketConnection socket, JsonObject msg)
{
socket.Send(msg.ToString());
}
Using this extension method we can invoke the Send()
method with JsonObject
s as with strings. The advantage lies in the readability. It is now much cleaner without the unnecessary ToString()
call.
Using the code
The server should be easily adjustable for other games and projects. With the basic commands already implemented and an existing MatchCollection
the server can be used to create a solid basis for a variety of different games. This section discusses the parts of the code which should be changed, the ones which
could be changed and the ones which should not be changed by taking this code as a basis for some WebSocket games.
The following image shows a screenshot of the server's command line interface. The advanced command pattern (including the already built-in commands) should
be part of any project building up on this one.
Basically the folders Structures
, Interfaces
, and Game/Upgrades
are only SpaceShoot related. So they could be removed for
other projects. The Game
folder contains some files which might be good as a starting point. MatchHelpers
, MatchCollection
,
and Match
do contain some SpaceShoot related code, however, the related part can be easily cut out of the more generic rest. Also GameObject
and GameInfoObject
are more or less generic. Some methods or properties might be close related to SpaceShoot, but they can be modified or removed easily.
The other classes of the folder Game
are fully related to SpaceShoot and could be removed.
The files in the folder Server
are as generic as the code in the Program
class with the Main()
entry function. Changes here should not be applied.
Some of the commands being found in the Commands folder will have to be modified regarding the changes in the MathCollection
class.
If the class is fully removed then a lot more changes are about to come. There could have been ways around it involving more interfaces and dependency injection,
however, it is obvious that some things can be over-engineered. In this case the server is somehow specialized to the game, i.e. it would be kind of waste to build
generic interfaces and such when we actually want to specialize the commands to this specific kind of server (SpaceShoot). It is therefore obvious that
the same (specialized commands for a specialized server) applies for future projects. Therefore the commands rely on the MatchCollection
(and other stuff
like Match
and Ship
) by intention.
The test project is not part of test driven design, but part of just including some fixed points for testing. The included list of methods is neither complete
nor very clever. The list grew and shrunk due to demand. Therefore the test should probably be the first thing that is being removed or ignored from this project,
since the tests are really focused on problems regarding SpaceShoot.
A quick way to find a good spawning position for a player has been included in the Match
class. We see the following code:
public Point StartingPosition()
{
var pt = new Point { X = Width / 2.0, Y = Height / 2.0 };
var dist = 0.0;
var monitor = new Object();
if (Objects.Count > 0)
{
Parallel.For(0, 10, i =>
{
var x = (Random.NextDouble() * 0.6 + 0.2) * Width;
var y = (Random.NextDouble() * 0.6 + 0.2) * Height;
var min = double.MaxValue;
for (int j = 0; j < Objects.Count; j++)
{
if (Objects[j].Remove)
continue;
var dx = x - Objects[j].X;
var dy = y - Objects[j].Y;
min = Math.Min(Math.Sqrt(dx * dx + dy * dy) - 10.0 - Objects[j].Size / 2.0, min);
}
lock (monitor)
{
if (min > dist)
{
dist = min;
pt.X = x;
pt.Y = y;
}
}
});
}
return pt;
}
So what is happening here? We are generating 10 possible starting positions and checking which of them maximizes the minimum distance to all other objects.
It will rarely happen that the obtained solution is the optimum solution, however, it will usually happen that the obtained position will be a good solution.
And we did get this with just a few calculations (depending on the number of objects). Overall we had to generate 20 random numbers and perform at maximum
10 * N measurements (where N is the number of objects in the game). Compare this to obtaining the best solution which can be done by checking
every pixel in that rectangle. This could involve W * H * N measurement (where W and H are the width and height of the rectangle).
In practice we were quite amazed with the performance this way. We had no measureable performance impact but obtained solutions that were close to perfect at a first glance.
Points of Interest
Already quite a few cheats have been found. One of the earliest was that someone could pick black as primary and secondary colors, nearly cloaking him.
This was not the main problem with the color choice. The biggest problem was that particles have also the color of the player, resulting in cloaked particles.
This was way too strong, so it had to be prevented on the server side. Any color choice is evaluated there and adjusted to a color which is bright enough for the any opponent.
By using some of the real useful .NET-Framework 4, as well as C# 4 properties and methods we can boost our productivity. A good example for this is the new
Complex
structure from the System.Numerics
namespace. In order to use it we have to include the corresponding assembly reference.
It gives us a very easy possibility to calculate angles between two or more points in two dimensions. All we need to do is represent the points in a complex
number where X would be the real part and Y would be the imaginary part - depending on the coordinate system we pick. Every complex point has a certain absolute
value (named Magnitude
by the developer team) and a certain angle (named Phase
). This is because it is possible to represent any complex
number z = x + iy as z = r * exp(i * f), with r = (x2 + y2)1/2.
Also the usage of parallel-for loops and other features of the task library as well as LINQ and lambda expressions did help us a lot. By including the parallel
task library we made our server scalable. By using LINQ we did produce a more readable code - write less, do more. The same can be said for lambda expressions,
which are already essential for Fleck.
The game can be found (and played) at html5.florian-rappl.de/SpaceShootMulti/.
Avoiding dark colors
For picking a suitable primary and secondary color a color-picker control has been included with the free JSColor script. The drawback is that players can also pick quite dark colors, which make them hard to distinguish. The main problem with this is that not the ship itself, but the particles (shots) from such a ship cannot be seen or is really hard to detect.
The only way to safely prevent abuse of darkish colors is to introduce such a routine on the server. Every color has to be set in the Colors
class. This class contains properties for the primary and the secondary color. By setting one of these properties a routine is called with the passed value. The called method is this:
public string CheckDarkness(string value, string[] colors)
{
var rgb = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (rgb.Length != 3)
return colors[0];
var r = 0;
var g = 0;
var b = 0;
if (!int.TryParse(rgb[0], out r) || !int.TryParse(rgb[1], out g) || !int.TryParse(rgb[2], out b))
return colors[0];
var h = new HSV(r, g, b);
h.Value = Math.Max(h.Value, 0.5);
return h.ToString();
}
We only get (comma separated RGB) strings for colors. So the first task is to see if the passed string is really a color. If not then we return a predefined color from an array, which is either primaryColors
or secondaryColors
. If everything seems ok then we create a new HSV
class instance. This is a nested class. It has the following code:
class HSV
{
public double Hue { get; set; }
public double Saturation { get; set; }
public double Value { get; set; }
public HSV(Color rgb)
{
int max = Math.Max(rgb.R, Math.Max(rgb.G, rgb.B));
int min = Math.Min(rgb.R, Math.Min(rgb.G, rgb.B));
Hue = rgb.GetHue();
Saturation = (max == 0) ? 0 : 1.0 - (1.0 * min / max);
Value = max / 255.0;
}
public HSV(int r, int g, int b) : this(Color.FromArgb(r, g, b))
{ }
public Color ToRgb()
{
var hi = Convert.ToInt32(Math.Floor(Hue / 60)) % 6;
var f = Hue / 60 - Math.Floor(Hue / 60);
var value = Value * 255;
int v = Convert.ToInt32(value);
int p = Convert.ToInt32(value * (1 - Saturation));
int q = Convert.ToInt32(value * (1 - f * Saturation));
int t = Convert.ToInt32(value * (1 - (1 - f) * Saturation));
if (hi == 0)
return Color.FromArgb(255, v, t, p);
else if (hi == 1)
return Color.FromArgb(255, q, v, p);
else if (hi == 2)
return Color.FromArgb(255, p, v, t);
else if (hi == 3)
return Color.FromArgb(255, p, q, v);
else if (hi == 4)
return Color.FromArgb(255, t, p, v);
return Color.FromArgb(255, v, p, q);
}
public override string ToString()
{
var c = ToRgb();
return string.Format("{0},{1},{2}", c.R, c.G, c.B);
}
}
Nothing to exciting here. It is basically a HSV (Hue, Saturation, Value) class which has two possible ways of being constructed. Either by a System.Drawing.Color
structure or with three values (red, green, blue). We need this class, since it allows us to get the value of Value
. This represents the lightness. We want the value to be 0.5 or higher. Therefore we had the following call before: h.Value = Math.Max(h.Value, 0.5)
.
Next we just use some wicked code to transform HSV back to RGB. Usually we will not call this method, since we did also override the ToString()
method. This one will now just give us a comma separated string containing red, green and blue - a RGB string that can be read from any CSS parser. With this code no one is able to abuse the usage of darkish colors in order to get an unfair advantage in a match.
What does the whole project teach us?
This is a really interesting question I asked myself after finishing the server program. The big message is: Start with the server before you write
any line of HTML, CSS, or JavaScript.
This sounds odd, however, I think my JavaScript design would have been significantly better with a really good and solid server already up and running.
Writing the code like this I had to adjust the server first to the JavaScript needs (going from OOP to prototype / procedural programming is for me much easier than
the other way), which was kind of messy and showed me that my JavaScript code could have been written a lot better by abusing the prototype pattern much more than I did.
Also the extensibility of the program would have been much better, because I was already aware of the connection to the server. Therefore I would have designed
the JavaScript to embrace the server and not the other way around. I think that my awareness of these problems is somehow due to using a different language for
the server than the client. A lot of the described problems could have been avoided by using node.js. Nevertheless I think using C# for the server gives
you not only some coding fun, but also a kind of different perspective to the problem. Also you will get the best debugging tools available, which will help you
a lot in building a robust and scalable server.
History
- v1.0.0 | Initial release | 19.03.2012.
- v1.0.1 | Minor update with some fixes | 21.03.2012.
- v1.1.0 | Update (Including AI) | 23.03.2012.
- v1.2.0 | Update (Including ColorCheck) | 24.03.2012.
- v1.3.0 | Update (Including Server class, Fleck extensions) | 27.03.2012.