Click here to Skip to main content
15,881,588 members
Articles / Desktop Programming / MFC
Article

WinBattle

Rate me:
Please Sign up or sign in to vote.
4.46/5 (14 votes)
22 Dec 200320 min read 63.4K   2.8K   43   9
A multi-player game tutorial and reusable framework

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. It also resurrects a game from the early 90s that I (and a lot of other people) used to find great fun: XBattle.

Please note: The full text of the tutorial can be found in the source code zip file in the folder Notes (in HTML format). It contains a number of diagrams and detail which would make this web page far more complex than the site editors would like. This page contains only the main parts relating to the framework code.

Background and Acknowledgement

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.

Greg Lesher stopped working on XBattle back in 1996. However, it is still available for download for UNIX systems on the website (at least it was when I wrote this). However, no one has ever done a PC version and it seemed like a nice project for generating a reusable framework for multi-player games, so I wrote one.

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 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.

The Tutorial Game

It's quite simple. There is a playing board that represents a landscape. Sea is blue, flat land is green and darker shades of brown represents higher ground. Each player has bases (shown as circles) that generate troops which 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 which 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 in order to set the vector).

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

Image 1

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.

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 6 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.

The Explore option means 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 as 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:

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. 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, etc.). There is nothing related to the game itself here because the client and server code doesn't really care which game it's running (however, the full tutorial in WinBattle_src.zip does explain some of the trickier bits of the game).

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 which it just passes on to the server.

The clients and server communicate 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, to send some value from a client program to the server just requires the following line of code in the client:

server << some_value << "\n";

In order 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 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, etc. 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 class Control 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 program's messages. As long as the clients and servers both use the same number then everything should work just fine. The default for the WinBattle client and servers is set at 3333 as 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 file universal.h).

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 if there is a free player position in the game. The code is currently set to handle up to 6 players (max_players) but that is easy to change (increase max_players and create additional 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 (you'll find diagrams in the full tutorial showing the relationships of the various threads).

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:

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 is used to tell 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 and 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 which is connected to the hostname and port which 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.

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 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 commands below 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 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 who 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 then 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 then 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 that 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

OK, 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 that, in this simple example, it assumes 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".

Using The Socket Class

To use the socket class in your projects 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 as 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) then 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 (i.e. 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 then 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);

Summary

So, 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 should be a good base for implementing anything that fits into this model.

Things To Do

The project as it stands is the result of a few weekend's 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 they are looking for that DLL.

License

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 in the source code zip file.

History

  • December, 2003 - First Release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Systems Engineer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
General[Message Deleted] Pin
it.ragester2-Apr-09 21:56
it.ragester2-Apr-09 21:56 
Questionleft shift right shift Pin
jqt8-Feb-07 7:24
jqt8-Feb-07 7:24 
AnswerRe: left shift right shift Pin
Ken Reed21-Feb-07 8:15
Ken Reed21-Feb-07 8:15 
Generali can't play this game Pin
horse199711-Sep-06 20:36
horse199711-Sep-06 20:36 
GeneralRe: i can't play this game Pin
Ken Reed21-Feb-07 8:25
Ken Reed21-Feb-07 8:25 
GeneralRe: i can't play this game Pin
Ken Reed22-Feb-07 9:30
Ken Reed22-Feb-07 9:30 
GeneralRe: i can't play this game Pin
Ken Reed22-Feb-07 9:35
Ken Reed22-Feb-07 9:35 
GeneralQuite Good Tutorial Pin
Digvijay Chauhan28-Sep-04 1:10
Digvijay Chauhan28-Sep-04 1:10 
GeneralLooks good Pin
Badut22-Mar-04 19:26
Badut22-Mar-04 19:26 

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

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