Previous: Collisions. AI. State machines

Multiplayer and online games

This workshop deals with creating multiplayer games, and is based on the multiplayer series by George Pirvu in the Gamestudio magazine AUM, starting with issue 75; it also covers a bandwidth optimization method suggested on the user forum. While the AUM series was about LAN games, this workshop will also give you a whiff of serious online gaming with bandwidth and lag considerations. This is important when you want to connect many players to your game, or play with people from the other side of the globe, or write the next generation MMORPG.

Let's start with an extremely simple game and then convert it to multiplayer. The game to start with is in script25.c:

////////////////////////////////////////////////////////////////////////////
// extremely simple single player game
////////////////////////////////////////////////////////////////////////////
 
action player_move() // control the player
{    
  var walk_percentage = 0;
  while (1) 
  {
    my.pan += (key_a - key_d)*5*time_step;   // rotate the entity using [A],[D]
    var distance = (key_w - key_s)*5*time_step;
    c_move(me, vector(distance,0,0), NULL, GLIDE); // move it using [W],[S]
    walk_percentage += distance;
    ent_animate(me, "walk", walk_percentage, ANM_CYCLE); // animate the entity
    wait (1);
  }
}

function main() 
{
  level_load("multiplayer6.wmb");
  vec_set(camera.x, vector(-600, 0, 100)); // set a proper camera position
  ent_create("redguard.mdl", vector(100, 50, 40), player_move); // then create the red guard!
}

Run the script, and use the WSAD keys to move and rotate the player. Level loading and player actions should be familiar to you now, so I only give a brief overview. The main function loads the level, sets a camera position, and creates a player entity that gets the action player_move. This action rotates the player with the [A] and [D] keys, and moves him forward and backward with the [W] and [S] keys. Again we're using the walk distance - the input value to c_move - as a percent value for the ent_animate function. This way the entity animates accordingly to its walking speed.

Well, I have to admit: A red player walking around in a dull grey room is not a very exciting game. The guy looks lonely, let's give him some company! For the following you'll need Gamestudio A7.7 Commercial edition or above, which supports multiplayer games. Let's now open script25_1.c:

////////////////////////////////////////////////////////////////////////////
// simple lite-C LAN game - naive approach
////////////////////////////////////////////////////////////////////////////
 
action player_move() // control the player on the client, move it on the server
{    
  var walk_percentage = 0;
  while (1) 
  {
    if (my.client_id == dplay_id) { // the following code runs on the player's client only
      my.skill1 = key_w - key_s; // forward/backward
      send_skill(my.skill1,SEND_UNRELIABLE|SEND_RATE); // send movement request to the server
      my.skill2 = key_a - key_d; // left/right rotation
      send_skill(my.skill2,SEND_UNRELIABLE|SEND_RATE); // send rotation request to the server
    }

    if (connection & CONNECT_SERVER) { // the following code runs on the server only
      my.pan += my.skill2*5*time_step;   // rotate the entity using its skill2
      var distance = my.skill1*5*time_step;
      c_move(me, vector(distance,0,0), NULL, GLIDE); // move it using its skill1
      walk_percentage += distance;
      ent_animate(me,"walk",walk_percentage,ANM_CYCLE); // animate the entity
    }
    wait (1);
  }
}

function main() 
{
  if (!connection) 
    error("Start first the server, then the clients!");
  else 
    while (dplay_status < 2) wait(1); // wait until the session is opened or joined

  dplay_localfunction = 2; // run actions both on server and client
  level_load ("multiplayer6.wmb");
  vec_set(camera.x, vector (-600, 0, 100)); // set a proper camera position

  if (connection & CONNECT_SERVER)  // this instance of the game runs on the server
    ent_create ("redguard.mdl", vector (100, 50, 40), player_move); // then create the red guard!
  else // otherwise, it runs on a connected client
    ent_create ("blueguard.mdl", vector (-100,-50, 40),player_move); // create the blue guard
}

Believe it or not, this is the full code for a working multiplayer game! It will help us learn quite a few things about Gamestudio's multiplayer capabilities. The Acknex engine gives us the ability to run the server and the clients on the same computer, allowing us to create a full multiplayer game and test it without needing two or more computers.

This time it's not sufficient to click the black triangle for starting; you need to make up your mind if you want to start it as server or as client. For starting in server mode, you have to pass the command line option -sv -cl to the engine. The easiest way to enter command line options is SED Options / Preferences:

Start the server first: check both Client and Server, then hit Ok and start the script by clicking the black triangle . Wait until the engine window opens. Then go to Options / Preferences again and uncheck Server. Now start the script again, but this time only Client is checked. If you have an older SED without those check boxes, just enter first -sv -cl, then only -cl manually in the Options field.

If you did everything right, you'll now have two engine windows on your PC that look like this:

Use again the WSAD keys to move and rotate the players; the keys work in the active window. When you've clicked into the server window, you can move the red player. In the client window you can move the blue player.

Now before examining the script, you'll probably have one important question: What the heck is this client-server tech talk? An overview can be found in the manual under Multiplayer, but here's the short version: A server is an obsequious software application that serves requests from the clients, a client is a mean software application that demands services from the server. Ok, let's break it down a bit...

Take a good look at the picture above; the client is making a request to the server, asking for something. The server receives the request, processes the information and sends back the result to the client. That's pretty much it when it comes to client-server applications. Let's examine one more picture:

As you can see, the clients can't communicate directly with each other; this isn't a peer-to-peer connection. Player1 can't send any data to Player2 directly; all the clients have to use the server whenever they want to exchange information. Let's imagine that all the players are running a multiplayer shooter game; if Player1 wants to fire a bullet at Player3, it sends its bullet's coordinates to the Server, which checks if they match Player3's model coordinates. If Player1's bullet can collide with Player3, the Server subtracts from Player3's health value. It's a simplified explanation of the process, but it does its job.

For avoiding that we need a third machine for a two player game, the engine is able to start a server that is a client at the same time. That's why we've given the -sv -cl command line options that start server and client together in a single window.

Now have a look at the start of our script:

if (!connection)
  error("Start first the server, then the clients!");
else
  while (dplay_status < 2) wait(1);

The predefined variable named connection is set to 1 (defined as CONNECT_SERVER) if the multiplayer application is run as a server, to 2 (CONNECT_CLIENT) if the application is run as a client, and to 3 if the application is run as a server and as a client at the same time (-sv -cl). If no server is found or the game is in single player mode, "connection" is zero. This would happen here if you run the client before running the server. This gives us an error message.

If connection is not zero, the server was started or the client was able to connect to it. In this case we wait until the variable dplay_status is 2 or above. dplay_status is a sort of countdown variable, but it counts up. 2 means that the server has accepted the client to the session.

dplay_localfunction = 2;

Setting dplay_localfunction at 2 means that all entity actions will run on the clients as well. If we had not set this variable, entity actions would only run on the server.

Afterwards the level is loaded, the camera is positioned, and depending on wether we're running on the server or a remote client, we're creating a red or a blue player:

if (connection & CONNECT_SERVER)
   ent_create ("redguard.mdl", vector (100, 50, 40), player_move);
else
   ent_create ("blueguard.mdl", vector (-100,-50, 40),player_move);

Note the "connection & CONNECT_SERVER" condition. It is true when the engine is running as a stand alone server (connection is 1) and also true when it's running as a server and client at the same time (connection is 3). If we had written "connection == CONNECT_SERVER", the condition only were true when connection is 1. The '&' is the usual method to compare a single flag regardless if other flags are set or not.

The main secret of this multiplayer game is the player_move function:

action player_move()
{
  var walk_percentage = 0;
  while (1)
  {
      if (my.client_id == dplay_id) {
        my.skill1 = key_w - key_s;
        send_skill(my.skill1,SEND_UNRELIABLE|SEND_RATE);
        my.skill2 = key_a - key_d;
        send_skill(my.skill2,SEND_UNRELIABLE|SEND_RATE);
     }
    ...

The player_move function runs on all PCs in the network; we've determined that with dplay_localfunction = 2. So we must decide within the function what to do dependent on whether the action is running on the server, on a client, or on the very client that has created the player. In that last case the client_id of the player (my.client_id) is the same as the id of the client (dplay_id). If so, the client can send keystrokes to control the player, which happens in the following lines. But it can't move the player directly - all player movement in a multiplayer game must happen on the server. Only this guarantees that collision detection, hits etc. are consistent for all connected clients.

Rule 1: Movement is normally performed on the server. 

This means that you can let the clients move a NPC (flying bird, etc) that has a visual effect only; however, all entities relevant for collisions and for the game outcome - players, bullets etc. - must be moved on the server. Thus, rather than moving its player itself, the client sends move and rotate requests to the server. That's done through the player's skill1 and skill2. The send_skill engine function will send a skill (or more) to the server. For saving bandwidth, we're sending in unreliable mode (SEND_UNRELIABLE) and only 32 times per second (SEND_RATE). After that, skill1 and skill2 on the player entity on the server will take the same values as on the player client.

Rule 2: Information is normally exchanged between clients and server by updating entities, skills and variables.

For this we use send_ functions, except for the positions and other visual parameters of entities. They are automatically updated by the server to all clients. Therefore we don't need any send functions for keeping the level on all clients up to date, which is automatically handled by the engine.

We were only using skill1 and skill2 for movement and rotation in this example; however, you could use other skills for jumping, strafing, shooting etc as in a "real" game. Now let's see what the server does with this information:

if (connection & CONNECT_SERVER) {
  my.pan += my.skill2*5*time_step;
  
var distance = my.skill1*5*time_step;
  var distance = c_move(me,vector(distance,0,0), NULL, GLIDE);
  walk_percentage += distance;
  ent_animate(me,"walk",walk_percentage,ANM_CYCLE);
}

The code checks if it's running on the server, then rotates, moves, and animates the player according to the skill1 and skill2 values received from the player's client.

That's all we need for creating a multiplayer game! Can we now start writing the next generation MMORPG? For answering this question, start the server and client as before, but press [F11] to invoke the statistics panel. Now begin to start more clients. Notice that a new blue player appears for any client you start. Make sure to walk the player away from the start position before starting a new client - otherwise, it would become entangled with the next client's player and can't move.

Observe the bps display. It's the number of bytes received per second, while rel and unr display the number of bytes sent in reliable and unreliable mode. The more clients connect, the higher the bps value gets on clients and servers. Both the incoming and the outgoing data traffic increases with every new client. We can easily imagine that eventually it will hit a limit, and then the game will start to play laggy and jerky. What can we do about this?

Creating a serious online game

We've written a LAN game that's not really suited for running online. Now what is the difference between a LAN game and an online game? Certainly, our script above would also play over the internet when you give the server's IP address with the -ip command line option when starting the client. However, playing it with many players would not be much fun. The reason is bandwidth and latency. Bandwidth determines the maximum number of bytes per second supported by the network connection. Latency depends on the travel time of a data packet over the network. Bandwidth restricts the data exchanged between server and clients, and can - with a bad script - slow down or even stall the game when the limit is exceeded. Latency adds a considerable delay - lag - between pressing a key and seeing a reaction on the screen. Despite otherwise rumors, the engine has no influence whatsoever on Internet bandwidth and data packet travel speed.

But your script has influence on their effects. Online games use many tricks to reduce the data transferred, and to give the impression of a fast reaction. Have a look at script25_2.c. We've marked bandwidth and lag improving modifications in red:

////////////////////////////////////////////////////////////////////////////
// simple lite-C online game - serious approach
////////////////////////////////////////////////////////////////////////////

function on_client_event() // terminate client when server disconnects
{ 
   if (event_type == EVENT_LEAVE) sys_exit("Disconnected!"); 
}

function player_remove() // remove player when client disconnects
{ 
   if (event_type == EVENT_DISCONNECT) ent_remove(me); 
}

action player_move() // control the player on its client, move it on the server, animate it on all clients
{ 
  my.event = player_remove;
  my.emask |= ENABLE_DISCONNECT;
  my.smask |= NOSEND_FRAME; // don't send animation
   
  var walk_percentage = 0;
  var skill1_old = 0, skill2_old = 0;
while (1) { if (my.client_id == dplay_id) // player created by this client? { if (key_w-key_s != my.skill1) { // forward/backward key state changed?
my.skill1 = key_w-key_s; send_skill(my.skill1,0); // send the key state in reliable mode } if (key_a-key_d != my.skill2) { // rotation key changed?
my.skill2 = key_a-key_d; send_skill(my.skill2,0); // send rotation state in reliable mode } } if (connection & CONNECT_SERVER) // running on the server? { if (my.skill1 != skill1_old) { // if movement changed
send_skill(my.skill1,SEND_ALL); // send movement to all clients
skill1_old = my.skill1;
}
if (my.skill2 != skill2_old) { // if rotation changed
send_skill(my.skill2,SEND_ALL); // send rotation to all clients
skill2_old = my.skill2;
}
}
my.pan += my.skill2*5*time_step; // rotate the entity using its skill2 var distance = my.skill1*5*time_step; c_move(me, vector(distance,0,0), NULL, GLIDE); // move it using its skill1 walk_percentage += distance; ent_animate(me,"walk",walk_percentage,ANM_CYCLE); // animate the entity wait (1); } } function main() { if (!connection) { // not started with -cl / -sv -cl? if (!session_connect(app_name,"")) // no client found on the localhost? session_open(app_name); // start as server } do { wait(1); } while (dplay_status < 2); // wait until the session is opened or joined dplay_entrate = 4; // 16 ticks/4 = 4 updates per second dplay_smooth = 0; // dead reckoning not needed dplay_localfunction = 2; level_load ("multiplayer6.wmb"); vec_set(camera.x, vector (-600, 0, 100)); // set a proper camera position if (connection & CONNECT_SERVER) { // this instance of the game runs on the server video_window(0,0,0,"Server"); ent_create ("redguard.mdl",vector(100,50,40),player_move); // then create the red guard! } else { // otherwise, it runs on a connected client video_window(0,0,0,player_name); random_seed(0); // allow random player positions ent_create ("blueguard.mdl",vector(-100+random(200),-50+random(100),40),player_move); // create the blue guard } }

The above script exposes some possibilities to improve a LAN game for online playing. The main idea is that all clients move and rotate their entities directly, and the server only forwards the movement and rotation skills. Entity updates are now less frequent, and only used for keeping them in sync by adjusting their angles and positions when they deviate. This method has four advantages:

The first main() lines are only for the convenience of Pro Edition owners:

if (!connection) {
  if (!session_connect(app_name,""))
    session_open(app_name);
}

This looks for a session on the local host, and if none is found, starts one as server. So this saves us from always entering the -sv, -cl command line options. But if you don't have a Pro Edition, you can safely ignore this method and use the SED command line preferences as before.

dplay_entrate = 4;
dplay_smooth = 0

There are other improvements in the main function. We're reducing the entity update rate - dplay_entrate - to 4 updates per second which is sufficient because entity updates are now only needed for synchronization. For the same reason, we can and need to switch off the dead reckoning system by setting dplay_smooth at 0. Otherwise it would disturb the entities' own movement. The client name (or just "Server") is now displayed in the title bar through the video_window function. And the blue player is now placed at a random position in the level so that we don't have to walk it away all the time.

Note also that we're using a do...while loop now for waiting until the session is joined. We're doing that for making sure that at least one wait() is executed. We need this for the video_window calls that only work when the engine window is open, which happens after the first frame.

Now let's have a look into two new event functions:

function on_client_event()
{
  
if (event_type == EVENT_LEAVE) sys_exit("Disconnected!");
}

This is a client event. If the client does not receive anything anymore from the server, this event is triggered. We suppose the server was shut down, or the Internet broke down or something, and do a clean exit.

function player_remove()
{
  
if (event_type == EVENT_DISCONNECT) ent_remove(me);
}

And this is a similar event for the player entity: When its creator client does not send anymore, EVENT_DISCONNECT is triggered and the player is removed from the server. If we would not react on this event, players of disconnected clients would continue to linger around on the server until eternity. We need to set up this event at the beginning of the player function:

action player_move()
{
  my.event = player_remove;
  my.emask |= ENABLE_DISCONNECT;
  my.smask |= NOSEND_FRAME;
  ...

We've also set the NOSEND_FRAME flag to prevent that the entity animation is sent from the server to the clients. We're doing that for saving bandwidth. Animation data is about 6 bytes per update. When 100 players are connected, NOSEND_FRAME alone saves - at 4 updates per second - more than 200.000 bps outgoing server traffic!

However, when the server does not send animation updates anymore, the clients have to calculate the animation themselves. We'll come to that soon. Before, look at the movement controlling part:

if (my.client_id == dplay_id) {
  if (key_w-key_s != my.skill1) {
    my.skill1 = key_w-key_s;
    send_skill(my.skill1,0);
  }
}

We're now comparing the key state with its previous state stored in skill1, and only send the key state when it has changed. Because every sent value matters now, we must send it in reliable mode and without SEND_RATE. Still, sending only the key changes takes away a lot of traffic from the clients to the servers. We're also using skill2 for sending the forward/backward movement. By the way, here is occasion for improvement when we use more keys: a skill has 32 bits, so we could easily compress the 2 keys in 2 bits of a single skill, and had even space for 30 more.

if (connection & CONNECT_SERVER)
{
  if (my.skill1 != skill1_old) {
    send_skill(my.skill1,SEND_ALL);
    skill1_old = my.skill1;
  }
  if (my.skill2 != skill2_old) {
    send_skill(my.skill2,SEND_ALL);
    skill2_old = my.skill2;
  }
}

Not only the player client, but also all other clients have to be informed about the rotation and movement of the player. Therefore, the server now sends the received movement and rotation skills to all connected PCs. For saving bandwidth, the skills are again only sent when they were changed.

my.pan += my.skill2*5*time_step;
var distance = my.skill1*5*time_step;
c_move(me, vector(distance,0,0), NULL, GLIDE);
...

Now what's that? We've told above that all movement happens on the server - but now rotate and move the player directly on the client! This is for fighting lag. In a first person shooter, the camera moves with the player. If we would only send a rotation or movement request to the server and wait for the angle and position update, the camera would need up to a third of a second - depending on the internet lag - to react on the key we've pressed. This is inacceptable, thus we're moving the player directly on its client for getting an immediate reaction.

With the above code we use far less bandwidth than the previous version, and have no rotation lag anymore. Do we now have the perfect code for an MMOG? Let's calculate the traffic, under the assumption that 1000 players from all over the world are connected to our server, and that about 300 of them are moving along at the same time. A compressed position data update has a size of about 8 bytes. This results in an average outgoing server traffic of 300 positions x 8 bytes x 1000 clients x 4 updates = 9.600.000 bytes per second. Your Internet connection at home probably won't do, but a T3 line with a bandwidth of 45 Mbps should handle this without much problem. When you need even more concurrent players, there are a lot more tricks to reduce the traffic - for instance dividing the level in zones, and only sending entity updates to clients that are in the same zone as the updated entity. For large worlds and even more players, you can split your game into different levels that run on several servers with separate Internet connections. You can do all this with Gamestudio. But the more players you want to serve, the more hard thinking is required to optimize the traffic.

Hosting an online game

How can we set up our home PC so that people can play our online game over the internet? There are two problems. First, your PC is probably not directly connected to the Internet, but sits behind a router and a firewall. Second, the internet IP address of your PC will probably change all the time, dependent on your service provider. The solution to the first problem is Port Forwarding, to the second a DynDNS service. Here's a brief step by step instruction to set up an online game server.

========================================================================================

You can now call yourself an experienced game programmer - you know (well, at least in theory) how to do a MMOG, and have reached the last paragraph of the last workshop! The last workshop? Well, not really. There's more to learn. And there are more workshops. You can find them all in the Acknex User Magazine (AUM) on the Gamestudio website. The workshop series is continued in AUM 67, where we'll learn everything about lite-C pure and legacy mode...