Click here to Skip to main content
Click here to Skip to main content

Building Multiplayer Capability into Your Game in One Hour

, 7 Oct 2005
Rate this:
Please Sign up or sign in to vote.
Want to build multiplayer games but have no idea where to start or no time to develop solid networking code? Read on...

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.

// All the headers...(see actual 
// source code) <PACKETENUMERATIONS.H>

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 );  // 1st player (self), 
                                      // subsequent players are 
                                      // created on the fly
    }
    
    ~ClientConnection()
    {
        client->Disconnect(300);
        RakNetworkFactory::DestroyRakClientInterface(client);
    }

    // Updates all entities in own world, 
    // data coming in from external
    void updateOwnWorld(Player* incomingPlayer)
    {
        // Loop through all players including 
        // ownself and update accordingly
        for (int i=0; i<PLAYERS.SIZE(); 
          (incomingPlayer- diff="(int)" int { i++)>ID - players[i].ID);
            if (!diff)
            {
                // Copy all information
                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;
            }
        }
    }
    
    // Updates rest of the external world about own changes
    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);
        }
    }
    
    // Handle incoming packet
    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;
                    
            // Order is important (reference server code as well)
            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 you're not passing "uiversal data" 
            // (i.e. passing a single type of data 
            // which is universally shared) and the 
            // number of players are dynamically
            // changing, you'd need a manager to keep 
            // track of new and dropped out players.
            // I'm using a vector as a simple container 
            // and some manipulation for that.
            if (_isNewPlayer(inPlayer.ID))
            {
                std::cout << "New player " << 
                    inPlayer.ID << "just joined!" << std::endl;
                // Create new player and include in world
                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
            {
                // Look for the specific player and update accordingly
                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 )    // If similar, x-y=0;
            {
                count++;
            }
        }

        if (count==0)    // is new
        {
            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 )    // If similar, x-y=0;
            {
                // Update the information    
                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;    // Assuming no identical ID
            }
            else
            {
                count++;
            }
        }
        if (count == players.size())
        {
            return false;
        }
    }

    RakClientInterface * client;
};

int main(int argc, char** argv)
{
    // Change this to non-compile dependent
    ClientConnection myConnection("127.0.0.1", "10000");

    while(1) 
    {
        Sleep(100);
        // includes updating of changes from world
        myConnection.ListenForPackets();    
        std::cout << "Total number of players: " << 
             myConnection.getPlayerCount() << std::endl;
        
        if(kbhit())
        {
            char c=getchar();
            if (c==' ')
            {
                // players[0] is always ownself
                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--;

                // includes updating world of own changes
                myConnection.updateWorld(players[0]);    
            }
        }
        
        // Get an account of all world items
        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

  • 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

Share

About the Author

Gabriyel

Singapore Singapore
No Biography provided

Comments and Discussions

 
QuestionThe server sid PinmemberZhangjinsong10-Dec-07 20:59 
QuestionThe server side PinmemberZhangjinsong10-Dec-07 20:59 
NewsEndian safety PinmemberOvermindDL125-Apr-07 9:25 
GeneralRe: Endian safety PinmemberGabriyel12-Nov-07 4:51 
GeneralA few questions Pinmemberwaldermort17-Sep-06 10:44 
GeneralRe: A few questions [modified] PinmemberGabriyel18-Sep-06 4:21 
GeneralRe: A few questions Pinmemberwaldermort18-Sep-06 5:14 
GeneralRe: A few questions PinmemberGabriyel18-Sep-06 6:28 
GeneralDo not post bad code please,time is so invaluableners !! Pinmemberjiangwangjin3-Sep-06 4:48 
AnswerRe: Do not post bad code please,time is so invaluableners !! PinmemberGabriyel3-Sep-06 5:15 
GeneralRe: Do not post bad code please,time is so invaluableners !! Pinmembergwinter15-Sep-07 19:45 
GeneralSame hobby PinmemberXChronos15-Nov-05 19:13 
Generalbuild the raknet in the linux and window,failed Pinmemberwinart11-Oct-05 18:55 
GeneralRe: build the raknet in the linux and window,failed PinmemberGabriyel11-Oct-05 19:52 
GeneralRe: build the raknet in the linux and window,failed Pinmemberwinart11-Oct-05 23:03 
GeneralRe: build the raknet in the linux and window,failed PinmemberGabriyel11-Oct-05 23:24 
GeneralRe: build the raknet in the linux and window,failed Pinmemberwinart11-Oct-05 23:27 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 8 Oct 2005
Article Copyright 2005 by Gabriyel
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid