Introduction: Designing Multiplayer Games
or, "a crash course in scalable design that centers around multiplayer games"
by Jered Wierzbicki

Multiplayer games are a logical extension of single player games; for the most part, they make use of the same constructs, the same methods, and involve the same general concepts as any other games. The only difference is that the engine for a multiplayer game has to be able to handle more than one player at a time. If your engine is truly scalable, there shouldn't be a problem moving from single-player to multiplayer. On the other hand, if you make a lot of assumptions, then you're going to have trouble.

What does "scalable" mean in terms of a game engine? For our purposes, it means that the engine can be extended to function for many different cases with little trouble. Example of game-engine scalability: When you plot a pixel, do you assume that you're plotting at a certain resolution all the time? With that answer in mind, what if was to demand that you allow the player to be able to change the resolution during game play? If you answered, "yes, I assume that the screen has certain dimensions all the time," then it's going to require some better design thinking to get your putpixel working under different resolutions. On the other hand, a good design will intrinsicly allow this.

So take a look at your fundamental design assumptions in a single player game. How do you store information about the player, such as their position? Do you just slop it into globals? C++ doesn't make you immune to bad design, either--do you just slop the position of the player into private members of your game's "engine" class, or however you've got it worked out? When the right arrow is pressed, do you just add a velocity to some player_x variable?

It sounds trivial, but these issues all present problems when you want to make a single player game multiplayer. So here's a general solution: When you're designing the very foundation of your entire game logic, move all of your information about a player into a structure or a class, and use *that* to represent a player, not global variables. Treat a player like a polygon in your good-ol'-software-3D-engine-that-you-wrote-two-years-back-with-only-stone-tablets-in-Ancient-Greek-for-a-reference; you need to be able to represent a single player in a general fashion so that your engine can process any player.

For instance, let's consider a really simple side-scroller that must be designed to let two people play the same game on one machine. We might say that a player is:

#define KEYBOARD   0
#define JOYSTICK   1
#define MOUSE	 2
#define BRAIN	 3

typedef struct {
int x, y, xv, yv;
int input_device;
sprite *player_sprite;
} player;

[You want to consider that different players might be using different input devices; this is accounted for with the input_device member of the player structure and the constants that it can be set to.]

In our strictly-two-player one-screen hypothetical game, we might then define two "player" structs for both of our players as globals, like this:

player players[2];

Then in our main game loop, when we want to draw the players, we simply do this:

draw_sprite(player[0].player_sprite);
draw_sprite(player[1].player_sprite);

When we move the players, we simply do this:

move((player*)&player[0]);
move((player*)&player[1]);

Getting input from the players would be a bit more complicated, but still simple if we want to think in terms of making the game scalable. Remember that different players might be using different input devices; see above.

The only thing that's different between these input devices is how they get information to the game: The information that different input devices relay to a game is actually telling you the same thing. So just define a structure that describes what an input device is telling the game. This structure can then be reused for all input devices. Like this:

typedef struct {
    char move_left, move_right, move_up, move_down, fire;
} input;

Then, write seperate functions to process input from each kind of input device:

void keyboard(input *i) {
     ...
     if (key_down[KEY_LEFT])
        i->move_left = 1;
     if (key_up[KEY_LEFT])
        i->move_left = 0;
     if (key_down[ENTER])
        i->fire = 1;
     ...
}

void joystick(input *i) {
     ...
     if (button_down)
        i->fire = 1;
     if (button_up)
        i->fire = 0;
     ...
}

And then you can just use one master function to handle all of the input for each player:

void get_input(player *p) {
     input i;

     switch (p->input_device) {
            case KEYBOARD:
                 keyboard((input *)&i);
                 break;
            case JOYSTICK:
                 joystick((input *)&i);
                 break;
            ...
            default:
                 if (!known_device((input *)&i, p->input_device)) {
                 game_error(NO_INPUT_DEVICE);
                 return;
     }

     if(i->move_right)
       p->x += p->xv;
     if(i->move_left)
       p->x -= p->xv;
     ...
}

[the known_device() function would move through a list of specialty devices or something and do any special processing, returning 0 if the device doesn't exist at all or 1 if it did indeed process it and get input]

Then back in your main game loop, just do this:

get_input((player *)&players[0]);
get_input((player *)&players[1]);

Already, some of the benefits of scalable game engine design should be obvious to you. By simply generalizing a few fundamental assumptions that have been made, we've already figured out how to perform a few simple tasks involved in a multiplayer game, and we've also given ourselves the ability to change aspects of a player during real-time that come in handy even in a single player game.

For instance, let's say that your player's sprite in your little 2D game is a ship. Well, what if you want them to be able to choose from among a selection of ships? Simple, you just change the player_sprite member of their information structure. What if you want each ship to have different motion characteristics? Simple, you just add information that makes up the physics model for a given ship to the player's player structure, and set the fields appropriately when the player selects a given ship. What about if the player wants to switch from one input device to another in the middle of the game? Simple, just set the input_device field of their player structure to the input device that they want to use.

Stating the obvious: A large part of the complexity involved in moving your game to a multiplayer setting is simply a matter of resolving scalability issues in your game engine's fundamental design.

To extend this skin-and-bones-multiplayer-game-on-a-single-machine model that was just developed above, let's ask ourselves more questions, to eliminate more fundamental assumptions that we're making. First of all, it should become obvious that the model above applies only in simple, two player games, right? As is, it would seem so. But for the heck of it, let's pretend that we're developing for a console that allows up to four players at once.

#define MAX_PLAYERS  4

We'd like to be able to support four players, because the console offers paddles for that number of people, and we want our game to sell based on it's ability to utilize cool features. Okay, so all we really have to do is come up with a global variable,

int num_players;

>Then create MAX_PLAYERS player information structs for use in the game,

player players[MAX_PLAYERS];

and then process all the players in our main game loop instead of just two. This can be accomplished with simple for loops:

for (int index=0; index 

Because we were careful to design our functions which receive input and so forth scalably, we can seamlessly extend the game in a manner of no time whatsoever. All we have to do is write input device functions that work for PADDLE_1, PADDLE_2, and so forth.

Now what if we want to be able to support an arbitrary number of players? Let's say that there's this cool new extender device for our imaginary console machine, that allows one to hook up as many paddles as they want. We can simply extend our limit up to a certain level that really should never be reached, or we can use a linked list of players and make no assumptions. (read: be scalable)

For those of you that don't know what a linked list is, it's really simple. A linked list is a chain. Each link in a linked list contains some data or a pointer to some data, and a pointer to the next link. Each link on the chain is called a node. The first link on the chain is called the head. So it's like this:
typedef struct {
int data;
void *next;
} node;

node head;

So (node *)head->next = the next node on the linked list.

When you follow the next pointers of each node on the linked list until you reach a node who has no next pointer (node->next = NULL), and process the data on each node as you go, you are said to be walking, or transversing, the linked list.

This would allow us to extend the number of players or decrease the number of players at run time, by simply adding nodes to [malloc] or removing nodes from [free] the linked list. All you'd have to do is walk the linked list of players and process each one within the game's main loop. The number of players would be limited only by the amount of avaialble memory on the machine running the game.

In yet another scenario, what if someone comes along and adds a really cool new input device to the platform that you're writing this game for, and it's only a week before you ship? Well, assuming that you can write a driver for the thing or pull support code from somewhere, and do it fast, with the scalable design just given above, you can integrate it seamlessly.

Once again, it pays off to design your engine well. I didn't pull the situations above out of my hat--these things happen. Good design becomes exponentially more important with the complexity of your game, and may mean the difference between a few days work porting code, or a few days work adding features, and a weekend adding features. When your game becomes multiplayer, the durability of your engine is tested. So design it to be tough, design it to be scalable.

So hopefully, you've learned something about scalability as it relates to designing a multiplayer game. The key to extensibility is good design: Good design always, always, always pays off. Either take my word for it, or try it and take your own.

Good luck, happy coding, and finish that game.

Discuss this article in the forums


Date this article was posted to GameDev.net: 9/14/1999
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
General

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!