Multiplayer

Top  Previous  Next

This month we are going to teach our players how to fire bullets at each other. But first, we need to solve a serious problem: how can we get access to the player pointers?

 

In a regular split screen shooter game, we would declare two player pointers ("player1" and "player2", for example) and we would use these pointers to control the players and so on. Things change in a multiplayer game: the server player (let's name it "player1") can be accessed easily because all the action is controlled by the server; however, the client player pointer ("player2") is valid only on the client computer and we can't be accessed directly by the server.

 

Why is that, I hear you asking? A line of code like "player2 = my;" that runs on the client is only known on the client, so the server doesn't know and can't access or control "player2". It would be great if we would find a way to send the client "player2" pointer to the server, isn't it?

 

If we don't want to control all the action on the server and we allow the clients to manage the firing by themselves we will certainly run into trouble; just think what would happen if the client would fire whenever it wants to and the connection with the server would be sloppy: the bullets would reach their target much later! The client would see its bullets hitting the target instantly, but because the connection with the server would be lost or lagging, nothing would happen on the server for fractions of second or entire seconds. The solution is clear: the server has to fire all the bullets, no matter if they are triggered by the server or by one of the clients.

 

We need to find out how to get a pointer to the client player and send the pointer over the network to the server; fortunately, the solution isn't complicated. Let's open the code for multiplayer7.c; it all starts with a few includes and variable definitions that can be safely ignored for now.

 

#include <acknex.h> 

#include <default.c>

 

var client_ent = 0;

var server_click = 0;

var client_click = 0;

 

Then there's the slightly modified move_players( ) function we've discussed about in the previous workshop:

 

function move_players()

       var walk_percentage;

       var stand_percentage;

       while (1) 

       {

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

               my.pan += my.skill2;

               if (my.skill1)

               {

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

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

                       stand_percentage = 0;

               }

               else

               {

                       stand_percentage += 1.2 * time_step;

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

                       walk_percentage = 0;

               }

               ent_sendnow(my);

               wait (1);

       }

}

 

Finally, some fresh code!

 

function players_click()

{

       if (connection == 2)

       {

               client_click = 1;

               wait (1);

               client_click = 0;

       }

       if (connection == 3)

       {

               server_click = 1;

               wait (1);

               server_click = 0;

       }

}

 

Function players_click( ) is called from function main whenever one of the players (client or server) clicks the left mouse button (LMB).

 

Important tip: function main runs on the server, as well as on all the clients. If you want to make sure that a certain snippet is executed on all the computers put it inside function main( ) or call it from inside function main( ).

 

Back to our code: if connection = 2, the function is run on the client; we set client_click to 1 for a frame - this means that the client player has clicked the LMB. If connection = 3, the code runs on the server, so we set server_click to 1 for a frame (the server player has clicked the LMB). These "client_click" and "server_click" variables will be used inside function main( ) so let's examine its content right away.

 

function main() 

{

       fps_max = 60;

       level_load ("multiplayer7.wmb");

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

       on_mouse_left = players_click;

       if (!connection)

               sys_exit(NULL);

 

We have encountered similar lines of code in the previous workshops, so let's take a look at the fresh code below.

 

       if (connection == 2)

       {

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

               wait (-0.5);

               client_ent = handle(my);

               send_var (client_ent);

               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);

                       send_var (client_click); // send client_click to the server

                       wait (1);

               }

       }

 

If connection = 2, the game runs as a client. We create the red soldier, and then we wait for 0.5 seconds, until the handle to the client player is ready and valid for all the clients. If you forget to insert the "wait (-0.5);" line in your code, don't wonder why you can't access the client player pointer.

 

The following line stores the client player handle inside the variable named "client_ent". Yes, we are going to use handles to transfer the client entities over the network because these handles are nothing more than numbers, and we all know that we can send numbers over the network using the "send_var" instruction.

 

In fact, this is exactly what the following line of code does: it sends client_ent from the client to the server, which will use ptr_for_handle to get the client entity pointer back.

The rest of the lines inside the "if (connection == 2)" branch take care if client's movement, which, as you may recall, is also done by the server. We make sure to send the "client_click" variable do the server as well; if the client presses the left mouse button, the server should react to that.

 

I could have used the unused skill3 instead of client_click, sending less data over the network (SEND_VEC sends skill1... skill3 at once) but I wanted to make sure that the code is as easy to understand as possible.

 

       if (connection == 3)

       {

               my = ent_create ("bluesoldier.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;

                       if (server_click)

                       {

                               my.z += 10;

                       }

                       if (client_click)

                       {

                               you = ptr_for_handle(client_ent);

                               you.z += 10;

                       }

                       wait (1);

               }

       }

}

 

The "if (connection == 3)" branch runs on the server, creating the blue soldier model; the loop moves the server player using its own skill1 and skill2 values.

If server_click is set to 1 (the server player has clicked the LMB), we increase the height of the server player by 10 quants. In this case, "my" holds the pointer to the server player, so we don't need to do anything special in order to access the player pointer.

 

If client_click is set to 1 (the client player has clicked the LMB), we restore the client player entity using ptr_for_handle and the value of client_ent, which was sent over the network only once, at the very beginning. I have used the predefined "you" pointer to restore the client entity, but you can use any other pointer name if you want to. The following line of code increases the height of the client player (you) by 10 quants.

 

Let's review the entire mechanism one more time: 

1) The client sends a handle to the client player over the network, to the server, at the very beginning.

2) The client sends a variable named client_click to the server each frame.

3) When client_click = 1 (the client player has pressed the LMB), we restore the pointer to the client player using ptr_for_handle.

4) Now that we have managed to get the client player pointer on the server, we can control the client players from the server, as desired.

 

Time to test multiplayer7.c: run server.bat, and then client.bat.

 

aum77_workshop1

 

Click anywhere inside the server and client windows and you will see that the corresponding models change their height. We have managed to access both player pointers (client and server) on the server, so now we can start firing some bullets.

 

Let's examine multiplayer8.c right away; it starts with an almost identical piece of code:

 

#include <acknex.h> 

#include <default.c>

 

var client_ent = 0;

var server_fired = 0;

var client_fired = 0;

var bullet_pan;

 

function move_players()

       var walk_percentage;

       var stand_percentage;

       while (1) 

       {

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

               my.pan += my.skill2;

               if (my.skill1)

               {

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

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

                       stand_percentage = 0;

               }

               else

               {

                       stand_percentage += 1.2 * time_step;

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

                       walk_percentage = 0;

               }

               ent_sendnow(my);

               wait (1);

       }

}

 

function fire_bullets() // this function is called from main, so it runs on the server and on the client

{

       if (connection == 2) // this section runs on the client

       {

               client_fired = 1; // we set client_fired for a frame, and then we reset it

               wait (1);

               client_fired = 0;

       }

       if (connection == 3) // this section runs on the server

       {

               server_fired = 1; // we set server_fired for a frame, and then we reset it

               wait (1);

               server_fired = 0;

       }

}

 

Function simple_camera( ) creates a simple 3rd person camera that stays 200 quants behind the player and 70 quants above the origin, sharing the same pan angle with the player. This function is called from inside function main( ) by the client and by the server each and every frame.

 

function simple_camera()

{

       vec_set (camera.x, vector (-200, 0, 70));

       vec_rotate (camera.x, my.pan);

       vec_add (camera.x, my.x);

       camera.pan = my.pan;

}

 

function main() 

{

       fps_max = 60;

       level_load ("multiplayer8.wmb");

       on_mouse_left = fire_bullets;

       if (!connection)

               sys_exit(NULL);

       if (connection == 2)

       {

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

               wait (-0.5);

               client_ent = handle(my);

               send_var (client_ent);

               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);

                       send_var (client_fired);

                       simple_camera();

                       wait (1);

               }

       }

 

The section that runs on the client (connection = 2) creates the red soldier, waits until the handle is ready and valid for all the clients, and then sends it over the network, just like in our previous example. I have renamed the variable "client_clicked" to "client_fired" for this example; you might remember that it is set to 1 for a frame whenever the player presses the LMB. Once again, we send client_fired to the server because we want client's bullets to be created on the server as well and client_fired will trigger their creation on the server. Finally, the simple_camera( ) call makes sure that our client player is seen from a correct 3rd person perspective at all times.

 

       if (connection == 3)

       {

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

               VECTOR bullet_pos[3];

               while (1) 

               {

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

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

                       simple_camera();

                       if (server_fired)

                       {

                               vec_set (bullet_pos.x, vector (30, -5, 15));

                               vec_rotate (bullet_pos.x, my.pan);

                               vec_add (bullet_pos.x, my.x);

                               bullet_pan = my.pan;

                               ent_create("bullet.mdl", bullet_pos.x, move_bullets);

                       }

                       if (client_fired)

                       {

                               you = ptr_for_handle(client_ent);

                               vec_set (bullet_pos.x, vector (30, -5, 15));

                               vec_rotate (bullet_pos.x, you.pan);

                               vec_add (bullet_pos.x, you.x);

                               bullet_pan = you.pan;

                               ent_create("bullet.mdl", bullet_pos.x, move_bullets);

                       }

                       wait (1);

               }

       }

}

 

The code above runs on the server (connection = 3); it creates the blue server player, and then it takes care of its movement and camera. If server_fired is set to 1, the server player has pressed the LMB, so we create a bullet that is placed at an offset of 30, -5 and 15 quants in relation to the origin of the player model (my). The bullet will have the same angle with the player, of course, and will run the function named move_bullets.

 

If client_fired is set to 1, the client player has pressed the LMB, so we have to create a bullet on the server, using client player's coordinates. We restore the client player pointer (it will be known as "you"), and then we create the client bullet using the same offset. As you can see, the bullet will have the same pan angle with the client player entity (you) and will run the same move_bullets( ) function.

 

function move_bullets()

{

       VECTOR bullet_speed[3];

       my.emask = ENABLE_IMPACT | ENABLE_ENTITY | ENABLE_BLOCK;

       my.event = remove_bullets;

       my.pan = bullet_pan;

       bullet_speed.x = 50 * time_step;

       bullet_speed.y = 0; // the bullet doesn't move sideways

       bullet_speed.z = 0; // or up / down on the z axis

       while (my)

       {

               c_move (my, bullet_speed, nullvector, IGNORE_PASSABLE | IGNORE_YOU);

               ent_sendnow(my); 

               wait (1);

       }                

}

 

function remove_bullets() // this function runs when the bullet collides with something

{

       wait (1); // wait a frame to be sure (don't trigger engine warnings)

       ent_remove (my); // and then remove the bullet

}

 

First of all, please note that function move_bullets( ) runs on the server.

 

Important tip: any ent_create instruction is run on the server! Use ent_createlocal if you need to create something only on a client.

 

The bullet is sensitive to impact with other entities or with level blocks and uses a simple event function that removes the bullets as soon as they hit something. The bullet speed is set to 50 * time_step quants / second; feel free to play with this value. The only thing that really needs our attention is the "ent_sendnow (my);" line of code, which sends the updated bullet position each frame, making sure that the bullet is moving smoothly on the clients as well.

 

Time to test the multiplayer8 example: run server.bat, then client.bat, and then fire bullets at will!

 

aum77_workshop2

 

Next month we will take care of player damage, health and maybe even some ent_createlocal stuff. I'll see you all then!

 

P.S. We aren't sending lots of data over the network yet, but my tests show that at this moment we can have over 20 clients running fine over a dialup connection.