WinBattle'�A Multi-Player Game Tutorial and Reusable Framework


This article was contributed by Ken Reed.

Environment: Networking, games

Introduction

This is WinBattle. It's mainly a tutorial in writing a multi-player network game but it also provides re-useable client/server framework code that can form the basis of subsequent projects. Oh, and it resurrects a game from the early 90s that I (and a lot of other people) used to find great fun: XBattle.

The Tutorial Game

It's quite simple. There is a playing board that represents a landscape. The sea is blue, flat land is green, and darker shades of brown represent higher ground. Each player has bases (shown as circles) that generate troops that are shown as coloured circles. The more troops present in a hexagonal cell, the bigger the circle. Troops can be made to move across the board by clicking on them to set movement vectors that show up as small lines. Click near the side of the hexagon in the direction you want to send your troops (you must have troops in the hexagonal cell to set the vector).

A few minutes into a two-player game might look something like this:

The idea, of course, is to attack your opponent's troops, capture their bases, and wipe them off the board.

To attack, just point your movement vectors at a cell occupied by the enemy. However, you'll find that you need to attack from more than one side to successfully occupy a cell.

Origin and Acknowledgments

Back in the early 90s, real-time multi-player games were pretty much unheard of. Computers were expensive and the only place you would find them connected to a network was at work and they would usually be running UNIX (or some manufacturer's proprietary operating system). Games were available, but they were almost exclusively single player.

Then, Steve Lehar released XBattle on the comp.sources.x newsgroup. This was a two-player battle simulation that was picked up by Greg Lesher and developed (over a number of years) into a multi-player, real-time battle simulation game that was quite unlike anything seen before. And great fun to play.

You can find an excellent description of the game here: The XBattle Home Page.

Greg Lesher stopped working on XBattle back in 1996. However, it is still available for download for UNIX systems on the Web site (at least it was when I wrote this).

Anyway, one wet weekend I was feeling nostalgic and reminiscing about the old games. XBattle came to mind and a quick trip to Google delivered me to the XBattle home page. After browsing the site, I thought it would be fun to play it again but I no longer have a UNIX workstation. So, off I went in search of a Windows version, thinking somebody must have done a PC version over the years. I didn't find one. There was a hint of a PC version on the XBattle home page (a program called Energy Battle), but that has long since disappeared. So, as I was looking for a hobby project at the time, I thought I'd port XBattle to Windows, thinking "How hard could it be?"

The answer is very. XBattle was written for the UNIX X-Window system and is so tied into the way that works. Getting the original code working under Windows would have taken quite a while. Also, the original code is written in C when I prefer C++. So, I wrote a Windows version from scratch.

Although I didn't use any of Greg's original code, I wanted to carry on the spirit of XBattle and release my version as free software. An e-mail exchange with Greg verified he had no problem with this as long as I changed the name slightly to indicate that it is not an "official" follow-on from XBattle. Hence, WinBattle.

The demo project is, of course, a very fun game. However, I wanted to release it as free software so that budding programmers have a (relatively) simple example of a way to build multi-player games. Just getting programs to talk to each other has always been hard work and WinBattle includes an easy to use mechanism that can be reused in other projects.

Trying Out the Demo

You'll find client and server programs available for download (WinBattle_demo.zip). Run the server on a PC and start up to six clients on networked PCs with the command:

winbattle server computer name

Where server computer name is the name of the computer where the server is running.

Or, create a shortcut that does the same thing. (By the way, the default server computer name is localhost, so if you want to try it out on just one PC, just start the server and one or two clients on that machine to play.) Press the start button on the server and away you go.

The server has a small number of options that are available in the dialog that looks like this:

The Explore option means that your troops have to scout out the terrain before you can see what's there. Turn it off and all players see the whole board from the start.

The Attrition option will cause troops to slowly waste away. This prevents the build up of totally massive amounts of troops.

The Group Bases option does just that. A player's bases will be positioned close together when this option is set and randomly scattered when it isn't.

The Disrupt option will cause enemy movement vectors to be cancelled when attacked. This allows a small number of troops to wreck supply lines that have to be constantly repaired.

The Hide Enemy Vectors option means you can only see an enemy's movement vectors if there are troops present in the cell. (This provides a stealthier start to games because tracks back to your bases aren't obviously visible if you scout with a small number of troops.)

Finally, you can set the number of bases each player is given.

On the client side the commands implemented are the following:

Mouse:

  • Left mouse button—Set a movement vector adding to any already set
  • Right mouse button—Set a movement vector cancelling any others
  • Shift + left mouse—March troops automatically across the board
  • Click in centre of cell—Cancel all movement vectors

Keyboard:

  • a—Attack: position cursor over cell to attack
  • f—Fill-in sea: set one movement vector to point to the sea cell to fill in
  • s—Scuttle a base: position cursor over base to scuttle

And, that's basically it. If you would like a considerably more detailed description of how to play, I recommend you visit the XBattle Home Page and read the first few pages of the tutorial. As far as the basic operation is concerned, XBattle and WinBattle are pretty identical.

Project Contents

The source code zip file expands to produce the following folders:

  • Client—Contains the client-side specific code
  • Common—Contains utility functions used by the client and server
  • Game—All of the code that is specific to WinBattle is held here
  • Notes—Holds the full tutorial (in HTML format)
  • Server—Contains the server-side specific code

The project was built with Visual Studio version 6 although it has also been tried with Visual Studio .NET. Should you want to compile the source code, you only need to compile and build the client and server components. They will automatically pull in what they need from the common and game folders.

Tutorial Introduction

This tutorial section talks about the WinBattle client/server programs in general (how they start up, communicate,and so forth). There is nothing related to the game itself here because the client and server code doesn't really care which game it's running.

What this means is that if you want to write a new game, all you have to do is write the game code. You can re-use the client and server code pretty much as is. Of course, it isn't quite that simple, but more on this later.

Note: Although you can read this tutorial in isolation, to get the most from it you really should take a peek at the code in the client and server folders to get a feel for what's going on. Pretty much everything else can be ignored.

Anyway, as should be apparent by now, there are two programs used to run the game: a server and a client. The server actually runs the game, updating the board in real time and sending the current state to all the clients. The client does very little. It displays the current state of play and accepts commands from the player; it just passes those commands on to the server.

The clients and server communicate by using sockets. Sockets can be tricky to get right, so all of the detail is hidden in a Socket class. As far as using it is concerned, it's just like reading and writing from a file. For example, sending some value from a client program to the server just requires the following line of code in the client:

server << some_value << "\n";

To read this value in the server, again, just requires one line of code:

client >> some_value;

You can find more details on using the Socket class later on in the tutorial. For now, we'll concentrate on how the client and server programs interact.

Start Up

The server main program can be found in server.cpp. All this does is pop up a dialog box that allows a number of options to be set such, as the number of bases, whether bases are grouped together, and so on. All of the dialog handling can be found in main_dialog.cpp. It's pretty much standard Windows programming, although some of the details of the Windows API calls are tucked away in the Control class for readability.

As part of the dialog initialisation, the server starts two threads: game_handler and update_handler. The first thread handles commands going back and forth between the clients and the server and the second thread sends game updates to the clients once the game is actually started.

Actually establishing the network communication is probably the hardest part, so let's begin by looking at the game_handler thread. The core of its operation is the following code:

Socket server(server_port);

while (true) {
   server.listen();

   for (int i = 0; i < max_players; i++) {
       if (! clients[i].in_use) {
          clients[i].connection = server.get_connection();
          clients[i].in_use     = true;

          DWORD thread_id (0);

          CreateThread(0, 0, client_handler,
                       reinterpret_cast<LPVOID>(i), 0, &thread_id);
          break;
       }
   }
}

The code creates a socket telling it the port number to use. Port numbers are just a unique number so that many programs can use sockets without worrying about getting other programs' messages. As long as the clients and servers both use the same number, everything should work just fine. The default for the WinBattle client and servers is set at 3333 because there isn't much chance of that number being in use on most PCs. However, if it is and you get a "port in use" error when you start the server, you can change it to any free port (you'll find that the port number is defined in the common folder in the universal.h file).

You can find out which ports are in use on your PC by issuing the following command at a DOS prompt (the port numbers are the ones after the ":" in the command output):

netstat -a

The thread then calls listen. This function will simply wait until some client program says it wants to communicate with the server on port 3333 (this is done from the client with a call to connect in client.cpp). Once a client has connected, the listen function returns and the code runs through a table of player details to see whether there is a free player position in the game. The code is currently set to handle up to six players (max_players), but that is easy to change (increase max_players and create colours for the new players in the Board class).

Once a free slot has been found, the details of the client connecting are stashed away and a new thread (client_handler) is started to look after the new player. The game handler then loops back to the listen call where it will wait for more clients to connect. So, to take an example of two clients connected, we would have the following server threads running:

The client_handler thread creates a socket (called client) and initialises it with the details saved in the game thread so that it can talk to the newly joined client.

Socket   client;
client.set_connection(clients[player].connection);

Then, the client handler sits in a loop that performs the following tasks:

while (true) {
   string command;
   client >> command;

   for (int i = 0; i < num_commands; i++) {
      if (command == command_list[i].command) {
         command_list[i].function(client, player);
      }
   }
}

This just reads a command text string from the client connected to this thread, looks it up in a table of commands and, if it is found, calls the appropriate function to deal with that command. From the client end, the code to actually send some command is trivial, for example:

server << "some-command\n";

The very first thing that a client does when starting up is to start a thread to receive updates from the server update handler (the code for doing this is almost identical to that shown above). Once that thread is running, the client sends the following command to the server:

connect computer_name port

This command tells the client handler which computer name the client is running on and which port the server should use to talk to the client. (We could have just hard-coded a value as we did for the server, but it's handy to run multiple clients on the same PC for testing; this lets us do this without messing about in the code.) The server code for handling this command looks like this:

int    port;
string hostname;

client >> hostname >> port;
client << player << "\n";

Socket * socket = (new Socket());

socket->connect(hostname.c_str(), port);

clients[player].socket = socket;

The first part of the code reads in the hostname and port and sends the player number back to the client. This tells the client which player he is in the game and is used to select the colour of his troops as the board is updated (amongst other things). The client_handler thread then creates a socket that is connected to the hostname and port that the client has just asked us to use. This will be used later to send the client the game updates and other commands. The socket we have just created is then stashed away in a table where we keep the details of the connected clients.

So, once a client has fully started, we have the following communication relationships between the threads of the client and server:

Game Start

When the start button is pressed on the server dialog, the following code is executed:

board.initialise(exploring, hidden);

for (int i = 0; i < max_players; i++) {
   if (clients[i].in_use) {
      board.setup_player(i, number_of_bases, group_bases);
   }
}

for (int k = 0; k < max_players; k++) {
   if (clients[k].in_use) {
      Socket * socket = clients[k].socket;
      (*socket) << "new-board\n";
        board.transmit(socket);
   }
}

game_running = true;

First, we ask the board to initialise itself, passing over some of the options from the dialog (this is when the terrain is generated). Then, for each player, we ask the board to set up that player (this is where bases are created).

Finally, we ask the board to copy itself to each of the clients over the socket connection we have established with the client. This deviates from our preferred streaming approach and uses the socket transmit function. I did start out streaming the board into and out of the socket, but it was just too slow.

It's important to point out that the server has one copy of the board and every client has its own that is partly synchronised to the server master. This allows every client to have their own unique view on the board relating to what they can see (this feature is used to implement the Explore mode, for example).

On the client side, the code that responds to the new-board command is very predictable:

board.receive(inbound);

Client Commands

When the game is running, the client can send the following commands to the server in response to mouse clicks and key presses (x and y are the Windows co-ordinates where the mouse is positioned):

mouse x y
key value x y

When a key is pressed, the command key is followed by the ASCII code of the character pressed.

Why do we do it this way? We could, of course, build in knowledge in the client that a left mouse click means to add a vector and send a command to that effect to the server. However, this is building knowledge of the game into the client, the server and the game classes themselves. This means that there could be a lot of work should we want to re-use the client or server code in a new project where keys and mouse buttons imply different commands.

The design aim is to make the client (and the server) as generic as we can. All they need to know is that they are playing a multi-user game. They don't need to know they are playing WinBattle or any other game. Consequently, the client and server are dumb. If somebody clicks a mouse button, they just blindly pass it on to the game classes that know what to do with it. In an ideal world, if you want to write a totally different multi-player game, all you would have to do is change the game classes. You wouldn't have to touch the core client or server code at all.

However, back in the real world, this isn't completely practical (although it is possible with some work). For example, the server dialog needs to know about the various game options so that they can be displayed and set. So, the pragmatic design goal is to keep the dependencies as small as we can and if you look at the code in the client and server directories you'll see that, although they know they have a game board, they know nothing else about it. For example, if we suddenly decided to make the board cells tiled octagons instead of hexagons, there would be no changes needed to either the client or the server code.

Game Updates

As the game progresses, the server update_handler thread periodically sends the updated state of the board to the clients. The code is as follows:

vector<Client>::iterator k;

for (k = clients.begin(); k != clients.end(); k++) {
   if (k->in_use) {
      Socket * socket = k->socket;

      (*socket) << "update-board\n";
      board.send_updates(socket);
   }
}

board.end_of_updates();

This loops for each possible client. If a particular client is connected, we warn it that an update is about to follow with the update-board command and then send across the updates. Once all updates have been sent, we let the board know that (with the call to end_of_updates so that it can prepare the next set).

The client code to deal with this is the rather boring:

board.get_updates(&inbound);
Note: Neither the client nor the server has any idea what these updates are. They both just know that periodic updates are being exchanged.

Shut Down

Okay, we've got all these threads up and running. How do we handle clients and servers being terminated? Quite simply, if either stops, it's polite enough to send a message to the other to that effect. For example, if the server is killed while clients are still running, it runs through its list of connected clients and sends each one a message so that the client/server handler thread can exit gracefully:

vector<Client>::iterator i;

for (i = clients.begin(); i != clients.end(); i++) {
   if (i->in_use) {
      *(i->socket) << "shut-down\n";
   }
}

Similarly, if a client terminates, it sends a message to the server so that the server/client handler thread can close down and free up a slot for another client:

server << "disconnect\n";

The code that runs in the server when this command is received is as follows:

Socket * socket = (clients[player].socket);

(*socket) << "shut-down\n";

client.close();
socket->close();

free(socket);
clients[player].in_use = false;

ExitThread(0);

As the command to shut down comes from the main thread of the client, we send a message to the client's other thread to ask it to shut down, too. We close the sockets to release their resources and flag that we now have a free slot in our list of clients.

Note: In this simple example, it's assumed that a server is started before any clients are and that if a server is shut down, clients are, too. It wouldn't be hard for clients to carry on working when the server is restarted, but I'll leave that as "an exercise for the reader".

Summary

So far, we've described an almost generic client-server environment that can run any game of a certain class (it has a board, players, and real-time updates). The framework described so far should be a good base for implementing anything that fits into this model and the next section shows some of the additional techniques that were used to implement WinBattle.

Board Class

All of the code that knows about the game can be found in the game folder. It is just two classes: Board and Cell (although these classes are implemented using a number of separate files for readability). The Board class implements the game itself and the Cell class implements the individual cells on the board. So, ignoring the functions for copying the board from the server to the client which we've already covered, the Board class has the following additional functions we can call:

  • draw—Draws the whole game board.
  • get_size—Returns the window size of the board.
  • initialise—Prepares the board for a new game (generates terrain and so forth).
  • key—Deals with a key press.
  • mouse—Deals with a mouse button press.
  • setup_player—Sets up an individual player (creates bases and such).
  • update—Update the game board.

The initialise, setup_player, and draw functions are called at the very start of the game (we only need to draw the whole board once). The key, mouse, and update functions are called as the game is played to respond to player commands and update the board to reflect the results of those commands.

The Board functions are largely self-explanatory although there are a few things that are not obvious. For example, given the coordinates of a mouse click in the game window, just how do you find out exactly which side of which hexagonal cell it points to? This, and some other tricky bits, is explained in the sections that follow.

Grid Layout

The board is a two-dimensional array of hexagonal cells. The board cells are numbered as shown below:

Identifying a Cell

To identify the tile indices from the window coordinates, we mentally chop up the board into a grid so that we will know exactly what we will find at the target point. In the same way that alternate columns are offset by half a cell on the board, we also offset our alternate imaginary grid columns as shown below (in red).

If the x coordinate specifies an odd numbered grid column, all we need to do is shift the y coordinate up half a cell before computing cell indices. We then can treat each cell the same way by first assuming that the x and y coordinates pick the cell that dominates the area and then adjusting the indices if the actual point is outside that cell. Here's what the code looks like to identify which square on our imaginary grid we are dealing with (ignoring checking for coordinates that are off the board). Note that we are starting with Windows coordinates in x and y and we want to end up with two array indices: i and j.

int i (x / (cell_size - (cell_size / 4)));
int j (y / cell_size);

if ((i & 1) != 0) {
   j = (y - half_cell_size) / cell_size;
}

The triangular portion of the hexagon that goes over our imaginary grid line is 1/4 the size of the cell (so i is actually the x coordinate divided by 3/4 of the cell size. Next, we need to start adjusting for those tricky triangular portions:

If the coordinates hit one of the two corner areas, we will need to decrement the x index to identify the correct cell. If the top corner is selected and the column number is even, we need to decrement the y index. If the bottom corner is selected and the column number is odd, we need to increment the y index. The technique to deal with the triangular area is to split the hexagon through the middle and then perform a boundary check based on the gradient of the sides. The gradient of a hexagon side is actually a constant equal to the following:

(1/4  cell size) / (1/2 cell size)

Now that we have made an initial stab at the cell indices, we can retrieve the coordinates of the cell that dominates the square on our imaginary grid (ox and oy). This allows us to adjust for the real values we want as described above. The code is as follows:

if (oy < half_cell_size) {
   boundary = quarter_cell_size - (int) (gradient * oy);
   if (ox < boundary) {
      if ((i & 1) == 0) j--;
      i--;
   }
} else {
   boundary = (int) (gradient * (oy - half_cell_size));
   if (ox < boundary) {
      if ((i & 1) != 0) j++; 
      i--;
   }
}

We now have the indices of the cell that corresponds to a particular Windows coordinate. This vital function is packaged in the private utility function get_cell.

Drawing Movement Vectors

Movement vectors are drawn from the centre of a cell to the middle of a side using the player's colour (or half way if the cell is empty). If troops are present in the cell, a black segment is drawn to the edge of the troop circle. As we know the length of the vector (or can calculate it) and the angle from the horizontal, we just need to convert from the polar coordinates into their Cartesian equivalents. However, as shown in the diagram below, the x and y co-ordinates are quite easy to compute directly as constants and are provided by table lookup (the x offset for a vector that isn't straight up or down is 3/8 the cell size with a y offset of 1/2 the cell size).

Identifying a Side

If we want to identify which hexagon side is near a mouse click, we have the opposite problem to the previous example. We have Cartesian coordinates and we want to convert them to polar ones so that we know the angle. Once we have that, the side can be identified by simply calculating angle/60. The formula for calculating the angle is fundamentally:

angle = tan-1 (y/x)

However, this is not the whole story because there are boundary conditions to worry about (for example, when x is zero, y must not be divided by it). Fortunately, all of the problems are hidden away in the atan2 library function so the code we need to write is quite simple:

double radians (atan2(y, x));
double angle   (radians * (180.0 / pi));

if (angle < 0) angle += 360;

return static_cast<int> (angle / 60.0);

Using the Socket Class

This project uses a slightly updated version of a Socket class that I posted on CodeGuru in late 2003. To use the socket class, you will need to include socket.h in your code and incorporate socket.cpp as a module. You will also need to include the socket library ws2_32.lib in your link parameters. Now, what are all those things in the socket include file?

#include "exception.h"

C++ lets you handle errors with exceptions. Some people like them; some people don't. I do, so if I detect an error, I throw an exception (the exception class that I use is included in the project). I recommend you use exceptions because they are a great help in debugging. Always put any code using the socket class inside a try block (note the caveat about streams below, though) and catch and report any errors. For example:

try {
   my_routine_that_uses_sockets();
}
catch (Exception & e) {
   MessageBox(0, e.get_error().c_str(), "Ooops!",
              MB_SETFOREGROUND);
}

Now, there is one thing to watch out for. If there is an error while you are streaming (using the << or >> operators), the standard library will catch the thrown exception and quietly set the stream state to bad or fail (depending on the error). If you stream your data, you must use the standard library error test functions fail() and bad() and not rely on an exception being caught.

void   close          ();
void   connect        (const char * const host, const int port);
void   listen         ();

These functions have been covered in the tutorial above. There's really not much more to say apart from the fact that if you give a port number of zero when creating a socket, it will automatically allocate a free port number (you can see this being used in the client-side code). If you want to find out what number has been allocated, use the following call:

int    get_number     ();

This is used by the client program to get its socket number so that it can tell the server what it is.

int    bytes_read     (bool reset_count = false);
int    bytes_sent     (bool reset_count = false);

You can read (and reset) the number of bytes sent and received over a socket. I used this when I wanted to display a progress bar while sending files over a socket in another project.

void   getline        (std::string & s);

The Microsoft string getline function doesn't work with sockets (it can block forever). Consequently, this is a replacement (which also doesn't include the eol character in the result ... just a personal preference).

void   read_binary    (void * buffer, int buffer_size);
void   write_binary   (void * buffer, int buffer_size);

Streaming is great because it is just so easy to read (and modern PCs are so fast that it is rare that you notice the overhead). However, if you are sending lots of data very often (especially floating point numbers), it can be very CPU intensive (in other words, slow) converting everything to text and back. So, if you need performance, use these binary functions. Define a structure containing the elements you want to send and call write_binary at the sender and read_binary at the receiver. You won't get data transfer between networked PCs much faster than that.

void   set_trace      (const char * filename);

If things are not working as expected and you can't figure out what's going wrong, turn on tracing. Pass a file name to this function and everything streamed over the sockets will be logged to that file (another good reason for streaming). Look at it to see exactly what was sent and what was received by each program (saves bags of debug time).

void   set_connection (void * handle);
void * get_connection ();

These functions let you transfer details from a socket established by a listen to a new one (so that the original socket can go back to listening). It was demonstrated in the game_handler thread description.

private:
   Socket (const Socket & Socket);         // No copying allowed

No copying of Sockets is allowed because I haven't written a copy constructor ... yet (and I'm not sure I want to). Why? The Socket class contains a buffer where it builds up text to send. Taking a copy (passing the socket as a parameter to a function) and adding to the buffer (inside the function) and then reverting to the original buffer (returning from the function call) is just too error prone. I just don't want to think about it. Does this mean you can't pass a socket as a function parameter? No, it doesn't; just pass it by reference rather than by copy. For example:

void my_function(Socket & socket);

Things To Do

The project as it stands is the result of a few weekends' tinkering. Consequently, there are loads of features in the original game that aren't present in this version. If you would like to extend the game, please do.

Bugs

I've tried it out on Windows XP, 2000, and NT4. It should work on Windows 95 and later, but I haven't been able to test that. There are probably plenty of bugs in the code, so feel free to fix any that you come across. I'd also appreciate an e-mail letting me know of any that you find.

One problem I have seen is that sometimes, when I compile it on my XP machine, it seems to introduce a dependency on gdiplus.dll. I don't know why (yet), but if the programs bomb out on start up, it's a good chance it's looking for that DLL.

Licence

WinBattle is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License (GPL) version 2 as published by the Free Software Foundation. You will find a copy of the GPL and the original XBattle license in the project notes folder.

Contact

After having two e-mail accounts rendered totally unusable by spam, I don't publish my e-mail address on Web pages or in newsgroups. However, you are welcome to e-mail me should you want. My address can be found in the file contact.txt in the notes folder.

Downloads

Download source code - 79 Kb
Download demo project - 159 Kb



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: October 29, 2014 It's well understood how critical version control is for code. However, its importance to DevOps isn't always recognized. The 2014 DevOps Survey of Practice shows that one of the key predictors of DevOps success is putting all production environment artifacts into version control. In this webcast, Gene Kim discusses these survey findings and shares woeful tales of artifact management gone wrong! Gene also shares examples of how high-performing DevOps …

  • On-demand Event Event Date: December 18, 2014 The Internet of Things (IoT) incorporates physical devices into business processes using predictive analytics. While it relies heavily on existing Internet technologies, it differs by including physical devices, specialized protocols, physical analytics, and a unique partner network. To capture the real business value of IoT, the industry must move beyond customized projects to general patterns and platforms. Check out this webcast and join industry experts as …

Most Popular Programming Stories

More for Developers

RSS Feeds