Multiplayer

Top  Previous  Next

This month we are going to start playing with Acknex's multiplayer capabilities; we will begin with very easy stuff and we'll end up having a fully playable multiplayer game.

Not sure you can learn multiplayer coding? Just take a look at the first example - I'll let you decide if it's complicated or not.

 

#include <acknex.h>

#include <default.c>

 

STRING* messages_str = "#50";

 

FONT* arial_font = "Arial#20";

 

TEXT* messages_txt = // displays the messages on the screen

{

       pos_x = 10;

       pos_y = 10;

       layer = 10;

       font(arial_font);

       string (messages_str);

       flags = visible;

}

 

function main()

{

       if (!connection) // can't find a server? (connection = 0)

       {

               str_cpy(messages_str, "Can't find any server. Please try again later.");

               wait (-4);

               sys_exit(NULL);

       }        

       if (connection == 2) // this instance of the demo runs as a client?

       {

               str_cpy(messages_str, "Running as a client");

       

       else // this instance of the demo runs as a server? (connection = 1 or connection = 3)

       {

               str_cpy(messages_str, "Running as a server");

       }

}

 

Believe it or not, this is the full code for a multiplayer project; it doesn't do too many useful things, but 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 - that's an exciting feature to have!

 

I have published all the multiplayer examples so that you can run them; locate the multiplayer1.cd folder, run server.bat, and then run client.bat (in this order). This will launch a server (a mean software application that sends commands to the clients) and a client (a docile software application that sends requests and then executes all the commands that come from the server). Let's take a look at the application windows before starting to explain what's happening with them.

 

aum74_workshop1

 

If you start the client first, it won't find the server and will pop up this error message:

 

aum74_workshop2

 

What's with this client-server tech talk, you ask? Ok, let's break it down a bit...

aum74_workshop4

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:

aum74_workshop5

 

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. I know it's a simplified explanation of the process, but it does the job.

 

Back to our workshop: all my examples are started using batch files, which are nothing more than regular text files that end with a .bat extension and contain some commands. Here's the content of server.bat:

 

multiplayer1.exe -sv -cl

 

As you see, server.bat runs our multiplayer1.exe file and adds the "-sv" and "-cl" parameters to it; "-sv" tells the engine to run as a server and "-cl" instructs it to run as a client. This means that server.bat creates a server and a client at the same time, allowing us to host the server and play the game as a client on a single computer. As you can guess, this must be a powerful computer - the most powerful you've got, because it does 2 jobs at the same time.

 

If we omit the "-cl" part, the computer will only run as a multiplayer server, just like this:

 

aum74_workshop3

 

The "-sv" only configuration will use all Acknex's resources for the server - that's how you are supposed to do the things when you are coding a MMORPG or a fast multiplayer action game, for example.

 

And now let's take a look at the client.bat file:

 

multiplayer1.exe -cl -ip localhost

 

This time the engine runs the game as a client only ("-cl") and scans the local computer, trying to detect the server that is supposed to run on the same computer. On the other hand, if your friend would have a game server with an IP address of 123.234.55.66, you would be able to connect to his computer over the Internet using a bat file that looks like this:

 

multiplayer1.exe -cl -ip 123.234.55.66

 

The A7 professional owners have access to a broader range of features: the possibility to run the engine as a server or as a client at runtime, the ability to create MMORPGs and so on; they have paid more money, so they deserve better things, after all. Don't worry though, 99.84% of the code from my workshops will work fine with A7 commercial as well ;)

 

Let's return to our first multiplayer example; I am defining a font, a string and the text that uses it - that's trivial. All we need to discuss is the content of function main( ):

 

function main()

{

       if (!connection) // can't find a server? (connection = 0)

       {

               str_cpy(messages_str, "Can't find any server. Please try again later.");

               wait (-4);

               sys_exit(NULL);

       }        

       if (connection == 2) // this instance of the demo runs as a client?

       {

               str_cpy(messages_str, "Running as a client");

       }

       else // this instance of the demo runs as a server? (connection = 1 or connection = 3)

       {

               str_cpy(messages_str, "Running as a server");

       }

}

 

The predefined variable named "connection" is set to 1 if the multiplayer application is run as a server, to 2 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, "connection" is set to zero.

 

Function main checks the value of "connection"; if it can't find a server, it displays an error message, waits for 4 seconds and then shuts down the engine. This could happen if you run the client before running the server.

 

If "connection" is set to 2, the server exists and the client was able to connect to it; if this is the case, the application will display the "Running as a client" message. Finally, if "connection" is set to 1 or 3 (server or client + server), we display the "Running as a server" message. In fact, that message will only be visible if "connection" is set to 3; a dedicated server doesn't output anything on the screen.

 

This first example illustrates a simple principle: the code for a multiplayer application has different sections for clients and server; some of the code for them can be identical, but there will be some branches that are executed or skipped by the server while others will be executed or skipped by the client.

 

The second example illustrates this much better, so let's check it out:

 

#include <acknex.h>

#include <default.c>

 

STRING* messages_str = "#50";

 

FONT* arial_font = "Arial#20";

 

TEXT* messages_txt = // displays the messages on the screen

{

       pos_x = 10;

       pos_y = 10;

       layer = 10;

       font(arial_font);

       string (messages_str);

       flags = visible;

}

 

function main()

{

       fps_max = 60; // limit the number of data packets that are sent each second to 60

       level_load ("multiplayer2.wmb");  

       wait (3);

       if (!connection) // can't find a server? (connection = 0)

       {

               str_cpy(messages_str, "Can't find any server. Please try again later.");

               wait (-4);

               sys_exit(NULL);

       }        

       if (connection == 2) // this will only run on the client

       {

               str_cpy(messages_str, "Running as a client.");

       }

       else // this will only run on the server

       {

               str_cpy(messages_str, "Running as a server.");

               str_cat(messages_str, "\nServer IP address: ");

               str_cat(messages_str, server_ip);

               str_cat(messages_str, "\nServer name: ");

               str_cat(messages_str, server_name);

       }

       // the code below will run on the client, as well as on the server

       str_cat(messages_str, "\nLevel name: ");

       str_cat(messages_str, level_name);

       str_cat(messages_str, "\nApplication name: ");

       str_cat(messages_str, app_name);

       str_cat(messages_str, "\nPlayer name: ");

       str_cat(messages_str, player_name);

       str_cat(messages_str, "\nSession name: ");

       str_cat(messages_str, session_name);

}

 

Once again, function main( ) is the main attraction of the show, so feel free to ignore the rest of the code. The first line of code limits the frame rate to 60 fps; this is a great way to limit the data that is sent over the network to 60 packets per second. A good method that allows us to set up a dedicated server is to limit its frame rate to 30 fps or so and create intelligent client code that works fine with only 30 data packets per second. The clients would run at their maximum frame rate, of course.

 

The second example contains lines of code that run only on the client, lines that run only on the server, as well as several lines (near the end) that run on the client, as well as on the server. Here's how the application windows look like:

 

aum74_workshop6

 

All the information is output by Acknex's predefined strings; server_ip gives server's IP address, server_name gives the name of the server, level_name contains the name of the current level, app_name contains the name of our multiplayer application, player_name contains the name of the client and session_name contains the name of the current multiplayer session. You can find more information about them in the manual.

 

As you see, this time we have run a server and two clients; one of them has a decent name (Joe) while the other has the cryptic Client1115995 name. The reason is simple: if we don't name our players using the "-pl" argument, the engine will do it for us. Here are the contents of the batch files for the two clients:

 

multiplayer2.exe -cl -ip localhost -pl Joe // this client will be known as Joe over the network

 

multiplayer2.exe -cl -ip localhost // the engine will pick up a name automatically for this player

 

The time has come for a "real" multiplayer chat application; let's examine its code right away:

 

#include <acknex.h>

#include <default.c>

 

var im_server = 0;

var im_client = 0;

 

STRING* messages_str = "#100";

STRING* input_str = "#100";

STRING* sent_str = "#100";

STRING* server_str = "#100";

 

FONT* arial_font = "Arial#20";

 

TEXT* messages_txt = // displays the info messages at the top of the screen

{

       pos_x = 10;

       pos_y = 10;

       layer = 10;

       font(arial_font);

       string (messages_str);

       flags = visible;

}

 

TEXT* input_txt = // used for the text that is typed in

{

       pos_x = 10;

       pos_y = 440;

       layer = 10;

       font(arial_font);

       string (input_str);

       flags = visible;

}

 

TEXT* sent_txt = // the string used by the text carries the actual message that is sent over the newtork

{

       pos_x = 10;

       pos_y = 40;

       layer = 10;

       font(arial_font);

       string (sent_str);

       flags = visible;

}

 

// server_txt has the same position with sent_txt because it works only on the server, where sent_txt doesn't do anything

TEXT* server_txt =

{

       pos_x = 10;

       pos_y = 40;

       layer = 10;

       font(arial_font);

       string (server_str);

       flags = visible;

}

 

function server_event()

{

       if (event_type == EVENT_JOIN)

       {

               str_cpy(messages_str, player_name);

               str_cat(messages_str, " is finally chatting with someone.");

       }

       if (event_type == EVENT_LEAVE)

       {

               str_cpy(messages_str, "Scared to death, a client has left the chat...");

       }

       if (event_type == EVENT_STRING)

       {

               str_cpy(server_str, "#100");

               send_string(sent_str);

               while (key_enter) {wait (1);}

               while (!key_enter) {wait (1);}

               str_cpy(sent_str, "#100");

       }

}                

 

void input_text()

{

       if(inkey_active) return;

       str_cpy(input_str, "#100");

       inkey(input_str);

       if(result == 13)

       {

               str_cpy(sent_str, player_name);

               str_cat(sent_str, ": ");

               str_cat(sent_str, input_str);

               send_string(sent_str);

               if (im_server)

                       str_cpy(server_str, sent_str);

               wait (1);

               str_cpy(sent_str, "#100");

       }

}

 

function main()

{

       fps_max = 60;

       vec_set(screen_color, vector(100, 10, 0));

       on_enter  = input_text;

       on_server = server_event;

       if (!connection)

       {

               str_cpy(messages_str, "Can't find any server. Please try again later.");

               wait (-4);

               sys_exit(NULL);

       }        

       if (connection == 2)

       {

               im_client = 1;

               str_cpy(messages_str, "Running as a client.");

       }

       else

       {

               im_server = 1;

               str_cpy(messages_str, "The server is anxiously waiting for a client to log in...");

       }

}

 

The code might look big and ugly, but trust me - it's one of the shortest (if not the shortest of all) lite-C chat applications you'll ever see. More than that, if we ignore the variable, string and text definitions, we end up having only 3 small functions. Let's discuss them one by one.

 

function main()

{

       fps_max = 60;

       vec_set(screen_color, vector(100, 10, 0));

       on_enter  = input_text;

       on_server = server_event;

       if (!connection)

       {

               str_cpy(messages_str, "Can't find any server. Please try again later.");

               wait (-4);

               sys_exit(NULL);

       }        

       if (connection == 2)

       {

               im_client = 1;

               str_cpy(messages_str, "Running as a client.");

       }

       else

       {

               im_server = 1;

               str_cpy(messages_str, "The server is anxiously waiting for a client to log in...");

       }

}

 

Function main sets a blue background color and makes sure that function input_text is run every time when we press the Enter key. The event named "on_server" is executed every time when the server receives a message from the client, be it a variable, a string, and so on. That line of code makes sure that when one of the clients sends data to the server, function server_event is run.

 

The rest of the code inside function main is identical with the one from the previous example; the only additions are the two variables named "im_client" and "im_server", which will help us detect later if a certain instance of the application is a client or a server. The principle is simple: im_client will be set to 1 on all the clients (and not on the server) and im_server will be set to 1 on the server (and not on the clients). We'll get to use im_server in the following function - input_text( ) - check it out!

 

void input_text()

{

       if(inkey_active) return;

       str_cpy(input_str, "#100");

       inkey(input_str);

       if(result == 13)

       {

               str_cpy(sent_str, player_name);

               str_cat(sent_str, ": ");

               str_cat(sent_str, input_str);

               send_string(sent_str);

               // the following lines output the string to the server as well (who sends it to all the clients, but not to itself)

               if (im_server)

                       str_cpy(server_str, sent_str); // this string powers server_txt, who is only effective on the server

               wait (1); // let's wait a bit

               str_cpy(sent_str, "#100"); // and now we can safely reset sent_str

       }

}

 

This function runs as soon as the player presses the Enter key; if inkey is already active, the code doesn't allow more than one instance of this function to run. We reset the input string by copying an empty string of 100 characters to it, and then we read user's input (the actual message) and store it inside the same input_str string.

 

If the user has pressed the Enter key ending the message (result = 13), we copy player's name to sent_str, we add ": " to the string, and then we add the actual message that was typed in by the player (the one that was stored inside input_str). If the player would be named Joe, sent_str would look like this:

 

Joe: Hi there!

 

The "send_string" line of code is our first "real" multiplayer line of code; it simply sends our sent_str string over the network. Function input_text could be run by the server or by one of the clients; if it is being run on the server (im_server = 1, which is the equivalent of the shorter im_server), we copy the string that was sent over the network to the server as well. You'll laugh, but the server sends the data to all the clients and ignores itself, so we have to tell it to keep a copy of the message for its own use.

 

Here's a picture that shows what happens when the player that runs the server types in a message:

aum74_workshop7

The server processes input_str, adding player's name and so on to it, and then sends the resulting sent_str to all the clients over the network. This means that each client updates its own sent_str string, displaying the message that was sent by the server on the screen.

 

And now let's see what happens when one of the clients types in a message:

aum74_workshop8

This time, sent_str is sent to the server; remember that the clients can't communicate directly. The server receives an event; its an EVENT_STRING event (look it up in the manual) because the client has sent a string. The code inside the following function will make the server send the message to all the clients, including itself, if we have started the server using the "-sv -cl" arguments, which we did. Let's take a look at the last function; it's server's event function and runs every time when the server receives a message from one of the clients.

 

function server_event()

{

       if (event_type == EVENT_JOIN)

       {

               str_cpy(messages_str, player_name);

               str_cat(messages_str, " is finally chatting with someone.");

       }

       if (event_type == EVENT_LEAVE)

       {

               str_cpy(messages_str, "Scared to death, a client has left the chat...");

       }

       if (event_type == EVENT_STRING)

       {

               str_cpy(server_str, "#100");

               send_string(sent_str);

               while (key_enter) {wait (1);}

               while (!key_enter) {wait (1);}

               str_cpy(sent_str, "#100");

       }

}                

 

If a client joins the chat, the content of messages_str is updated; it was looking like this when the server was running alone:

 

aum74_workshop9

 

And now, when at least a client has joined the chat, the server screen looks like this:

 

aum74_workshop10

 

MasterBlaster is the name of our server; I have set it inside the server.bat file.

 

       if (event_type == EVENT_LEAVE) // a client has left the chat?

       {

               str_cpy(messages_str, "Scared to death, a client has left the chat..."); // copy this text to messages_str

       }

 

If one of the clients decides to leave the chat, an EVENT_LEAVE is sent to the server, making it display a different message:

 

aum74_workshop11

 

And here's the final section of the server_event function:

 

       if (event_type == EVENT_STRING)

       {

               str_cpy(server_str, "#100");

               send_string(sent_str);

               while (key_enter) {wait (1);}

               while (!key_enter) {wait (1);}

               str_cpy(sent_str, "#100");

       }

 

If the server has received a string from one of the clients (it could only be sent_str in our example), it resets the string that displays the chat messages on the server, and then it sends the string to all the clients (including itself). We want to keep the string on the server until the player presses the Enter key, preparing to type in a new message; as soon as this happens, we reset sent_str, preparing it for a new, exciting journey along the wires or radio waves of our network. Here's how our chat application looks like:

 

aum74_workshop12

 

I know that this has been a very long workshop, but we have managed to cover quite a few multiplayer aspects. I'll see you all next month, when we will continue our journey with even more exciting multiplayer stuff!