Multiplayer Communications (Networking)

Introduction

This was one of the most enjoyable features that I've implemented in Bapple 2. The game uses a sort of hybrid client/server and peer-to-peer communications system. Currently there are two main types of communication - streams and events. A stream is data that flows on a regular basis between the server and its clients. The only stream in the game right now is what I call the player state update packet stream. Whenever a player's state changes (i.e. location, health, or other critical stats), one of these optimized packets is sent to the server, which distributes the packet to all of the other clients. Anyway, the other type of communication is the event. Most bits of communications are events. For example, when a player fires a projectile (bullet), an event packet is sent out to the server with a packet type of PACKET_PROJ. This defines where the projectile came into existence, what direction it should travel in, etc.

But I'm getting ahead of myself. Let's first talk about the underlying communications protocols / libraries.

WSOCK, then Libnet ... and TCP vs. UDP

The very first step in writing the comm code was to find a comm library for DJGPP. After a good bit of searching, I came across Dan Hedlund's bare-bones WSOCK library. There was no documentation and only one out of two example programs worked. But after playing with the library for a few hours I came up with a working finger client using simple TCP/IP. After having spent this time, using the library was much easier. Using the source to WSOCK and some Winsock references as guides, I then created a TCP-based client/server test program. I worked this into Bapple 2 and pretty soon one person could connect to another. The whole multiplayer code originated as two-player only, which simplified matters greatly in the beginning (it was a purely peer-to-peer communications model).

After getting the connection code working, it was fairly easy to send and receive data packets. So the first real multiplayer communications code was born - each player regularly sent out and listened for player state update packets. To test all of this stuff I borrowed my boss's laptop (thanks Larry!) and had some help from people on IRC in #djgpp (thanks dwi!). Now, I came up against a problem that I did not understand - the packets seemed to be getting jammed up and then let through in bursts. I would see the other player freeze, then move rather quickly, then freeze again. After trying many things, I determined that my only solution was to use UDP rather than TCP. What caused such a choice? Well, for one thing, it was widely known that Quake used UDP for maximum throughput and minimum latency without regard to reliability. I needed to do pretty much the same thing with my game.

In case you're wondering about TCP and UDP, I'll explain briefly what these are (in the context of WSOCK/Winsock). TCP (transport control protocol) is a popular transport protocol used on the Internet that provides a basic guarantee of reliability to applications that use it. For example, if a packet gets lost between you and your destination, it is up to TCP to negotiate the retransmission of that packet. I'm no expert on TCP, IP, or UDP, but I think this is how it works. This kind of thing is good if you're doing an FTP download or are browsing web pages. Now, with such a system in place, TCP has a fair amount of overhead - mainly in terms of latency (delay). If one packet gets screwed up or lost in transit, TCP could be sitting there for a while trying to get that packet through when the game would rather forget about the lost packet and move right along.

That's where UDP comes in! UDP (user datagram protocol) is an unreliable protocol, which means that you can blast packets across the network without getting stuck if there are problems. When you're using UDP, you basically send out a packet to a machine and port # (207.180.64.226:4000 for example) and hope that the packet gets there ok. If it's something important that can't afford to be lost (say, for transmitting maps to new clients), then the application can do its own error checking and handling. If you were really cool, you might consider using both TCP and UDP simultaneously in your application. I decided against this for the sake of simplicity though. One other thing that's good about UDP is that it's very simple. It is a stateless protocol, really - there is no connection procedure needed to transfer data between two systems! To send data, you specify a machine's network address and a port number. To receive data, you listen on a certain port number. When data is received, you will be given the network address of the sender.

So, needless to say, I dumped TCP and converted everything to use UDP. This solved the packet jamming problems I was having. To this day, I have never had any trouble with the so-called "unreliability" of UDP. To my knowledge, it has been 100% reliable while used by Bapple 2. Then again, it's not like this game has seen a whole lot of playtesting over the Internet.

Update: I don't use the WSOCK library anymore, thankfully - now I use the cool Libnet library by George Foot and Chat Cattlett. It's friendlier, it has examples, it has docs, and it has a MAJOR bug fixed: WSOCK had some inline asm that'd set one certain byte of code in your code segment to 0!!! You wouldn't believe how much pain this caused me! And it was far enough into the code segment to not be noticible until your program reached a certain size. And even then, it wouldn't screw up the actual program execution all that often..

What Exactly is a Packet?

Before I continue, you should know what I mean by a packet. WSOCK provides two basic network I/O functions... Send() and Recv(). One of the parameters for each function is a pointer to a data buffer where the packet data is or will be stored. Usually when I talk about packets, I mean the stuff that is in these data buffers. The data that the application passes over the network and receives on the other end. Now, my packet scheme is pretty simple. The first byte of the packet is a packet type code. In my comm.h file, I have all of the possible packet types listed with #define statements. The beginning of this section looks like this:

/////// PACKET TYPES

// Player state update
#define PACKET_PLSTATE 0

// INITIALIZATION PACKETS
/////////////////////////

// Initial connection negotiation
#define PACKET_CREQ 1
#define PACKET_CACK 2

// Final connection negotiation
#define PACKET_IREQ 3
#define PACKET_IACK 4

I like this method because I can always tell exactly what kind of packet something is by that first byte (which is the easiest to access). After the first byte, the rest of the packet can be anything. I often have structs set up for certain types of packets, so that I can throw stuff into a struct (where the 1st byte of the struct is the packet type) and then point the Send() and Recv() functions right at that struct. Some example packet structs are:

// Player state update packet (also used for other things like projectiles)
typedef struct {
    signed char type;        // 1 byte
    char x, y;               // 2 bytes
    signed char xoff, yoff;  // 2 bytes
    fixed speed;             // 8 bytes
    unsigned char direction; // 1 byte
    short health;            // 2 bytes
    unsigned char id;        // 1 byte
} PACKET_T;                  // 17 bytes total

// Hurt request packet
typedef struct {
    signed char type;
    unsigned char victim;
    unsigned char victor;
    short health;
} PACKET_HURT_T;

Establishing the Initial Connection

This part is really quite easy. The basic idea is to have the game server listen for incoming packets of a certain type. When one of these is received, the server decides whether or not to admit the new player requesting a connection, and sends back an acknowledgement packet. The client then sends various requests for certain pieces of info, like the game map, info on the other clients in the game, the units in the game, etc. The server sends back all of this information as it is requested.

To be continued...