Multiplayer

Top  Previous  Next

This month we are going to start exploring real-time multiplayer action. No more turn-based games, chat applications and so on (even though I'll miss my lovely puppy ;)

If you think that real-time multiplayer is complicated you'll have quite a surprise; just take a look at this full real-time application:

 

#include <acknex.h> 

#include <default.c>

 

function move_players()

       while (1) 

       {

               my.x += my.skill1;

               my.y += my.skill2;

               ent_sendnow(my);

               wait (1);

       }

}

 

function main() 

{

       fps_max = 60;

       level_load ("multiplayer5.wmb");

       camera.z = 1000;

       camera.tilt = -90;

       if (!connection)

               sys_exit(NULL);

       if (connection == 2)

       {

               my = ent_create ("red.mdl", vector (100, 50, 40), move_players);

               while (1) 

               {

                       my.skill1 = 8 * (key_w - key_s) * time_step;

                       my.skill2 = 6 * (key_a - key_d) * time_step;

                       send_skill (my.skill1, SEND_VEC);

                       wait (1);

               }

       }

       if (connection == 3)

       

               my = ent_create ("blue.mdl", vector (-100, -50, 40), move_players);

               while (1) 

               {

                       my.skill1 = 8 * (key_w - key_s) * time_step;

                       my.skill2 = 6 * (key_a - key_d) * time_step;

                       wait (1);

               }

       }

}

 

Yes, that's all of the code for one of the shortest real-time multiplayer applications ever. Let's run server.bat, and then client.bat from inside the multiplayer5.cd folder.

 

aum76_workshop1

 

You can move each sphere around using the WSAD keys and you will see its movement in the other application window as well. 

I have used a top down camera, looking down in a simple, Wed-based level that consists of a single hollowed cube:

 

aum76_workshop2

 

Let's examine the code now:

 

function main() 

{

       fps_max = 60;

       level_load ("multiplayer5.wmb");

       camera.z = 1000;

       camera.tilt = -90;

       if (!connection)

               sys_exit(NULL);

       if (connection == 2)

       {

               my = ent_create ("red.mdl", vector (100, 50, 40), move_players);

               while (1) 

               {

                       my.skill1 = 8 * (key_w - key_s) * time_step;

                       my.skill2 = 6 * (key_a - key_d) * time_step;

                       send_skill (my.skill1, SEND_VEC);

                       wait (1);

               }

       }

       if (connection == 3)

       

               my = ent_create ("blue.mdl", vector (-100, -50, 40), move_players);

               while (1) 

               {

                       my.skill1 = 8 * (key_w - key_s) * time_step;

                       my.skill2 = 6 * (key_a - key_d) * time_step;

                       wait (1);

               }

       }

}

 

We limit the data packets that are sent over the network to 60 fps, and then we load the level and set the camera position and angle. 

If the predefined variable named connection is zero, the engine shuts down; this will happen, for example, if you run client.bat before running server.bat.

 

If connection is set to 2, this instance of the game runs as a client; we create the red sphere at x = 100, y = 50, z = 40 and we attach it the function named move_players( ).

We are going to move the ball using its own skill1 and skill2 values, so the WSAD keys set positive or negative values for them. 

Play with "8" if you want to modify the movement speed on the x axis and with "6" to modify the movement speed on the y axis.

 

The last line of code inside the first while (1) loop has a special role: it sends the values of red sphere's entity skill1... skill3 to the server.

 

send_skill (my.skill1, SEND_VEC);

 

The "send_skill" instruction will send a skill (or more) to the server if it is run on the client; "SEND_VEC" tells the instruction to send the two following skills as well.

We are only using skill1 and skill2 in this example; however, you could (and you should!) use skill3 for player's gravity, jumping, etc in a "real" game.

 

If connection is set to 3, this instance of the game runs on the server (who is also a client in our example). We create the blue server sphere and we control it using 

the same move_players ( ) function and the WSAD keys. This time we don't have to use send_skill because all the movement is done on the server.

 

Important rule: the movement has to be controlled by the server. Don't allow the clients to move the important (player, etc) entities by themselves.

 

This means that you should allow a npc (flying bird, etc) to be moved by the client code; however, you shouldn't allow the player movement to be controlled by the client code.

 

Let's examine the second function, the one that takes care of the actual movement of the spheres:

 

function move_players()

       while (1) 

       {

               my.x += my.skill1;

               my.y += my.skill2;

               ent_sendnow(my);

               wait (1);

       }

}

 

This function runs on the client, as well as on the server; it changes the x and y positions of the spheres depending on their skill1 and skill2 values.

The "ent_sendnow" instruction sends the updated entity position every frame; if we don't use it, the engine will send the position of the sphere less often over the network,

depending on the "dplay_entrate" value, and so on.

 

Let's review this real-time multiplayer example one more time: 

1) The client and the server use WSAD to set the skill1 and skill2 values for the spheres.

2) The sphere entity on the server is moved directly by the server, while the client needs a "send_skill" instruction in order to send skill1 and skill2 to the server.

3) Finally, "ent_sendnow" makes sure that the position of the sphere is updated each frame; is we wouldn't use it, the movement would be much less fluid.

 

So here you have it; world's shortest, fully functional real-time multiplayer example. As you can probably guess, the code for a "real" multiplayer example with 

collision detection, animations and so on would be much more complicated... or not! Just take a look at the code for our second real-time multiplayer example:

 

#include <acknex.h> 

#include <default.c>

 

function move_players()

       var walk_percentage;

       var stand_percentage;

       while (1) 

       {

               c_move(my, vector(my.skill1, 0, 0), nullvector, GLIDE);

               my.pan += my.skill2;

               if (my.skill1)

               {

                       walk_percentage += 1.5 * my.skill1 * sign(my.skill1);

                       ent_animate(my, "walk", walk_percentage, ANM_CYCLE);

                       stand_percentage = 0;

               }

               else

               {

                       stand_percentage += 1.2 * time_step;

                       ent_animate(my, "stand", stand_percentage, ANM_CYCLE);

                       walk_percentage = 0;

               }

               ent_sendnow(my);

               wait (1);

       }

}

 

function main() 

{

       fps_max = 60;

       level_load ("multiplayer6.wmb");

       vec_set(camera.x, vector (-600, 0, 100));

       if (!connection)

               sys_exit(NULL);

       if (connection == 2)

       {

               my = ent_create ("redguard.mdl", vector (100, 50, 40), move_players);

               while (1) 

               {

                       my.skill1 = 5 * (key_w - key_s) * time_step;

                       my.skill2 = 4 * (key_a - key_d) * time_step;

                       send_skill (my.skill1, SEND_VEC);

                       wait (1);

               }

       }

       if (connection == 3)

       

               my = ent_create ("blueguard.mdl", vector (-100, -50, 40), move_players);

               while (1) 

               {

                       my.skill1 = 5 * (key_w - key_s) * time_step;

                       my.skill2 = 4 * (key_a - key_d) * time_step;

                       wait (1);

               }

       }

}

 

See? It's not complicated at all! In fact, if you take a good look at it, you will see that function main is almost identical: we have a different camera position and slightly

different movement speeds, but apart from that nothing has changed.

 

Let's run server.bat, and then the client.bat from inside the multiplayer6.cd folder; we should see something that looks like this:

 

aum76_workshop3

 

Use the WSAD keys to move the players; this time, they will not pass through each other and they will play their animations.

All these goodies are brought to you by CrashEM! (your favorite car insurance company) and the improved move_players ( ) function; let's examine its content right away:

 

function move_players()

       var walk_percentage;

       var stand_percentage;

       while (1) 

       {

               c_move(my, vector(my.skill1, 0, 0), nullvector, GLIDE);

               my.pan += my.skill2;

               if (my.skill1)

               {

                       walk_percentage += 1.5 * my.skill1 * sign(my.skill1);

                       ent_animate(my, "walk", walk_percentage, ANM_CYCLE);

                       stand_percentage = 0;

               }

               else

               {

                       stand_percentage += 1.2 * time_step;

                       ent_animate(my, "stand", stand_percentage, ANM_CYCLE);

                       walk_percentage = 0;

               }

               ent_sendnow(my); // send the updated entity position regardless of the "dplay_entrate" value, etc

               wait (1);

       }

}

 

We are using entity's skill1 to move it and its skill2 to rotate it by changing its pan. If skill1 isn't zero (the player is moving), we increase the "walk_percentage" value.

Don't be bothered by the "sign(my.skill1)" part of the code; its role is to provide positive values for "walk_percentage" at all times, animating the player whenever it moves.

The last line of code inside the "if" branch resets stand_percentage; we do this because we want our "stand" animation to start with its first frame each and every time.

 

The "else" branch increases "stand_percentage", who's value doesn't depend on player's input. We animate the player and we reset walk_percentage, making sure that

the player starts with its first "walk" animation frame each and every time.

 

Finally, the "ent_sendnow" instruction sends the updated entity position every frame, helping us achieve that smooth movement on the server, as well as on and the client.

As you can see, the client uses its own animation code; there's no need to take care of clients' animations on the server and send animation data to the clients.

 

It's been an exciting workshop for me; I hope that you have liked it as well. Next month we will continue to improve our multiplayer knowledge, so stay tuned!