Background
I'm building my own 3D game as a hobby. It would have been treated as a museum article if there was no built-in multiplayer capability. Given my software development background (over 7 years experience), I could spend some time trying to write the networking code myself. However, as minute as many would perceive the networking module to be, in comparison to the other components in a game, I must say that is a common misconception. If you were to ever think about going online, the networking library that you plan to use has to be robust and dependable, not just able to send data across. For games, the first kill-joy would be network errors that send that entire program crashing.
Getting started
If you hit Google, you'd probably find a bunch of free networking libraries out there. Though the very fundamental networking functions are the same, such as the connect type (TCP, UDP, IPX), encryption, compression etc., there are usually more requirements when you want to use it to support multiple players in a game. Perhaps the most important question to most indie developers (I'm one of them) is the cost. To cut the story short, I've done a pretty extensive search and the following are the more prevalent networking libraries out there for game developers:
Seriously I'm not going to do a review of each of the networking toolkits here because that's not the purpose of this article and I'm sure there are relative strengths and weaknesses among them. But after running through each of their source trees and example projects, I did find Raknet to be the most comprehensive and easy to adopt library.
Planning your network game
It is important to have a clear understanding of the abstraction of the network layer from your game. Game worlds or states have to be managed properly and mapped to the updates from the rest of the connected players. Some newbies would probably have heard of the term client-server architecture but don't have a clear understanding of how the information flows from the client to the server and vice versa when it comes to setting up network connection, authentication, message sending and disconnection etc. Things can get complicated when network libraries impose requirements on developers to take care of issues such as endian-safeness and serialization/retrieval of their streams etc. I'm just going to dive straight into a sample code that I believe most people can put to use easily.
In the client-server model, you expect to start a server (as the host) and then have the clients connected to it. Forget about how the connections are done in the background and how many clients can be connected because that is taken care of by the networking library (of course, you may need to set a couple of parameters in the process). What happens is that the server keeps track of the clients connected to it and updates the rest whenever a client sends an update to the server. So as the name suggests, the server acts as the central point to "serve" the updates to all the clients. There are data that can be exclusively managed by the server (which clients cannot alter) and pushed to the clients and conversely so for the clients too. But in this article, we will look at how clients update one another via the server.
Using the code
You first need to download Raknet and build the source tree. You can unzip the source code provided with this article and then add them to the Raknet .NET solution. They should fit in and build without errors. I'm introducing BitStreams in this article and by using streams you are basically packing your data into packets of char
(bits/bytes) and then sending them off. On the receiving end, it is unpacked and reinterpreted into their original formats for processing.
const unsigned char PACKET_ID_LINE = 100;
class ClientConnection
{
public:
ClientConnection(char * serverIP, char * portString)
: client(NULL)
{
client = RakNetworkFactory::GetRakClientInterface();
client->Connect(serverIP, atoi(portString), 0, 0, 0);
Player myself;
players.push_back( myself ); }
~ClientConnection()
{
client->Disconnect(300);
RakNetworkFactory::DestroyRakClientInterface(client);
}
void updateOwnWorld(Player* incomingPlayer)
{
for (int i=0; i<PLAYERS.SIZE();
(incomingPlayer- diff="(int)" int { i++)>ID - players[i].ID);
if (!diff)
{
players[i].position[0] = incomingPlayer->position[0];
players[i].position[1] = incomingPlayer->position[1];
players[i].position[2] = incomingPlayer->position[2];
players[i].orientation[0] =
incomingPlayer->orientation[0];
players[i].orientation[1] =
incomingPlayer->orientation[1];
players[i].orientation[2] =
incomingPlayer->orientation[2];
players[i].speed = incomingPlayer->speed;
players[i].missiles = incomingPlayer->missiles;
players[i].health = incomingPlayer->health;
}
}
}
void updateWorld(Player p)
{
RakNet::BitStream dataStream;
dataStream.Write(PACKET_ID_LINE);
dataStream.Write((float)(p.position[0]));
dataStream.Write((float)(p.position[1]));
dataStream.Write((float)(p.position[2]));
dataStream.Write((float)(p.orientation[0]));
dataStream.Write((float)(p.orientation[1]));
dataStream.Write((float)(p.orientation[2]));
dataStream.Write((int)(p.missiles));
dataStream.Write((int)(p.speed));
dataStream.Write((int)(p.health));
client->Send(&dataStream, HIGH_PRIORITY,
RELIABLE_ORDERED, 0);
}
void ListenForPackets()
{
Packet * p = client->Receive();
if(p != NULL)
{
HandlePacket(p);
client->DeallocatePacket(p);
}
}
void HandlePacket(Packet * p)
{
RakNet::BitStream dataStream((const char*)p->data,
p->length, false);
unsigned char packetID;
dataStream.Read(packetID);
switch(packetID)
{
case PACKET_ID_LINE:
Player inPlayer;
dataStream.Read((inPlayer.ID));
dataStream.Read((inPlayer.position[0]));
dataStream.Read((inPlayer.position[1]));
dataStream.Read((inPlayer.position[2]));
dataStream.Read((inPlayer.orientation[0]));
dataStream.Read((inPlayer.orientation[1]));
dataStream.Read((inPlayer.orientation[2]));
dataStream.Read((inPlayer.missiles));
dataStream.Read((inPlayer.speed));
dataStream.Read((inPlayer.health));
if (_isNewPlayer(inPlayer.ID))
{
std::cout << "New player " <<
inPlayer.ID << "just joined!" << std::endl;
Player newplayer;
newplayer.ID = inPlayer.ID;
newplayer.position[0] = inPlayer.position[0];
newplayer.position[1] = inPlayer.position[1];
newplayer.position[2] = inPlayer.position[2];
newplayer.orientation[0] = inPlayer.orientation[0];
newplayer.orientation[1] = inPlayer.orientation[1];
newplayer.orientation[2] = inPlayer.orientation[2];
newplayer.missiles = inPlayer.missiles;
newplayer.speed = inPlayer.speed;
newplayer.health = inPlayer.health;
players.push_back( newplayer );
}
else
{
if (!updateOwnWorld(inPlayer))
{
std::cout << "Error updating Player " <<
inPlayer.ID << "'s information!" << std::endl;
getchar();
}
}
break;
}
}
int getPlayerCount() { return players.size(); }
private:
bool _isNewPlayer(double playerID)
{
int count=0;
for (int i=0; i<players.size(); i++)
{
int diff = playerID-players[i].ID;
if ( !diff ) {
count++;
}
}
if (count==0) {
return true;
}
return false;
}
bool updateOwnWorld(Player p)
{
int count = 0;
for (int i=0; i<players.size(); i++)
{
int diff = p.ID-players[i].ID;
if ( !diff ) {
players[i].position[0] = p.position[0];
players[i].position[1] = p.position[1];
players[i].position[2] = p.position[2];
players[i].orientation[0] = p.orientation[0];
players[i].orientation[1] = p.orientation[1];
players[i].orientation[2] = p.orientation[2];
players[i].missiles = p.missiles;
players[i].speed = p.speed;
players[i].health = p.health;
return true; }
else
{
count++;
}
}
if (count == players.size())
{
return false;
}
}
RakClientInterface * client;
};
int main(int argc, char** argv)
{
ClientConnection myConnection("127.0.0.1", "10000");
while(1)
{
Sleep(100);
myConnection.ListenForPackets();
std::cout << "Total number of players: " <<
myConnection.getPlayerCount() << std::endl;
if(kbhit())
{
char c=getchar();
if (c==' ')
{
players[0].position[0]-=0.1f;
players[0].position[1]-=0.2f;
players[0].position[2]-=0.3f;
players[0].orientation[0]+=0.1f;
players[0].orientation[1]+=0.1f;
players[0].orientation[2]+=0.1f;
players[0].health++;
players[0].missiles++;
players[0].speed--;
myConnection.updateWorld(players[0]);
}
}
for (int j=0; j<players.size(); j++)
{
printf("Player[%u] position[0]: %f\n",
players[j].ID, players[j].position[0]);
printf("Player[%u] position[1]: %f\n",
players[j].ID, players[j].position[1]);
printf("Player[%u] position[2]: %f\n",
players[j].ID, players[j].position[2]);
printf("Player[%u] orientation[0]: %f\n",
players[j].ID, players[j].orientation[0]);
printf("Player[%u] orientation[1]: %f\n",
players[j].ID, players[j].orientation[1]);
printf("Player[%u] orientation[2]: %f\n",
players[j].ID, players[j].orientation[2]);
printf("Player[%u] missiles: %d\n",
players[j].ID, players[j].missiles);
printf("Player[%u] speed: %d\n",
players[j].ID, players[j].speed);
printf("Player[%u] health: %d\n",
players[j].ID, players[j].health);
}
}
return 0;
}
I have slotted in a number of getchar()
s to let the programmer keep things in sync as he/she tries to debug the networking errors. Although things crawl a little but these creatures make things really clear about how the client-server-client exchanges data. Once you are familiar with how things work and have greater confidence, you can remove them and get your game running with the networking code in full swing.
Points of interest
I find using Raknet's Distributed Object Model easy but it is not endian-safe and that means when you run your program on different machines (e.g. OSX, Solaris), you may get problems. I ran into an example from Dave Andrews using Raknet with Irrlicht (a game engine) and the credit goes to him for shedding light on multiplayer networking.
The code provided here only goes to show a simple player management. In 3D games (such as the one I'm developing), you'll have to tie the management of world objects with the information from the network layer. And from there comes questions on information extrapolation, compression and all other means of ensuring network bandwidth is maximized and the world objects are synchronized.
History