Multiplayer

Top  Previous  Next

Let's start this month's workshop by opening the multiplayer9\multiplayer9.cd folder and running server.bat, followed by client.bat.

 

aum78_workshop1

 

This time, our bullets will kill the opponent; fire a bullet and the enemy, be it server or client, will fall to the ground, dieing. Here's the code that makes it happen:

 

#include <acknex.h> 

#include <default.c>

 

var client_ent = 0; // stores the handle to the client entity (client's player)

var server_fired = 0; // is set to 1 if the player that runs the server fires a shot

var client_fired = 0; // is set to 1 if the player that runs the client fires a shot

var bullet_pan; // stores the pan angle of the server or client player

 

Function damage_players is the event function for our players; it checks if they are hit by a bullet or not. If the player hasn't collided with a bullet, who has its skill10 set to 1234, the code doesn't do anything; otherwise, if the player was hit by a bullet, its skill10 is set to 1.

 

function damage_players()

{

       if (you.skill99 != 1234) {return;}

       my.skill10 = 0;

}

 

function move_players()

       var walk_percentage;

       var stand_percentage;

       var death_percentage = 0;

       my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY);

       my.event = damage_players;

       // new code

       my.skill10 = 1;

       while (my.skill10)

 

As you can see, we have set skill10 to 1; the "while" loop will run for as long as skill10 is set to 1; as you know, the event function from above will set skill10 to 0 as soon as the player is hit by a bullet. The content of the loop didn't change since the previous workshop, so we won't discuss it here.

 

       {

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

       }

 

This tiny loop below will run as soon as the player dies; it plays its death animation once, until death_percentage reaches 100. Please note the "ent_sendnow(my)" instruction, which sends updated entity positions every frame, regardless of the "dplay_entrate" value, and so on. If you don't use that instruction, the death animation won't be looking that smooth on the client; however, you will send less data over the network.

 

       while (death_percentage < 100)

       {

               ent_animate(my, "DeathHeadshot", death_percentage, NULL);

               death_percentage += 5 * time_step;

               ent_sendnow(my);

               wait (1);

       }

}

 

You have seen the code below in the previous workshop; the only addition is the "my.skill99 = 1234;" line of code inside function move_bullets. That line sets a unique identifier for the bullets, making sure that the players don't die if they collide with each other, and so on.

 

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 remove_bullets()

{

       wait (1);

       ent_remove (my);

}

 

function move_bullets()

{

       VECTOR bullet_speed[3];

       my.emask = ENABLE_IMPACT | ENABLE_ENTITY | ENABLE_BLOCK;

       my.event = remove_bullets;

       my.pan = bullet_pan;

       my.skill99 = 1234; // set a unique identifier for the bullets

       bullet_speed.x = 50 * time_step;

       bullet_speed.y = 0;

       bullet_speed.z = 0;

       while (my)

       {

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

               ent_sendnow(my);

               wait (1);

       }                

}

 

function fire_bullets()

{

       if (connection == 2)

       {

               client_fired = 1;

               wait (1);

               client_fired = 0;

       }

       if (connection == 3)

       {

               server_fired = 1;

               wait (1);

               server_fired = 0;

       }

}

 

function main() 

{

       fps_max = 60;

       level_load ("multiplayer9.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);

               }

       }

       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;

                               if (my.skill10) // server's skill10 is still set to 1? (the server player is still alive?)

                                       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;

                               if (you.skill10) // client's skill10 is still set to 1? (the client player is still alive?)

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

                       }

                       wait (1);

               }

       }

}

 

The "if (connection == 3)" branch inside function main (the code above) has changed a little bit; the bullets will be created only if server's skill10 is set to 1 or (for the client player) only if client's skill10 is set to 1. This way we make sure that the players can fire bullets only if they are alive.

 

Run the .bat files a few times and kill the server player and / or the client player. It's too bad that we have to restart the game every time when one of the players is hit, but this will fortunately change with the multiplayer10 demo.

 

aum78_workshop2

 

Yes! This time the players have got some (primitive) health indicators; each bullet hit will decrease their health by 10, making them die as soon as their health is zero. Let's take a look at the multiplayer10.c snippet and see how it's done.

 

#include <acknex.h>

#include <default.c>

 

var client_ent = 0; // stores the handle to the client entity (client's player)

var server_fired = 0; // is set to 1 if the player that runs the server fires a shot

var client_fired = 0; // is set to 1 if the player that runs the client fires a shot

var bullet_pan; // stores the pan angle of the server or client player

var players_health;

 

#define health skill20

 

PANEL* test_pan =

{

       digits(20, 20, 3, *, 1, players_health);

       flags = visible;

}

 

First of all, notice that we have got a new variable named "players_health" which displays player's health in the upper left corner of the screen. We have also defined "health" as skill20, so when you see "my.health" in the code it's actually the same thing with "my.skill20".

 

function damage_players()

{

       if (you.skill99 != 1234) {return;}

       my.health -= 10;

       my.health = maxv(0, my.health);

       send_skill(my.health, 0);

}

 

Function damage_players() has changed a little; if the player is hit by a bullet, its health is decreased by 10 points. The following line makes sure that player's health stays at 0 even if the player is hit by lots of bullets; it doesn't look professional to have a health indicator that displays -60 or so, even if the player is dead. Finally, the "send_skill" line of code sends the new health value over the network whenever player's health has changed.

 

function move_players()

{

       var walk_percentage;

       var stand_percentage;

       var death_percentage = 0;

       my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY);

       my.event = damage_players;

 

Take a look at the following two lines: the first sets player's health to 100, while the second sends the health value over the network immediately, in order to update the health value of the client player at game start. Without this line, the client player would still have its health set to 100, but its panel would display zero, because it didn't receive the health value from the server. Please note that the following "while" loop runs for as long as my.health is greater than zero.

               

       my.health = 100;

       send_skill(my.health, 0);

       while (my.health > 0)

       {

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

       }

       while (death_percentage < 100)

       {

               ent_animate(my, "DeathHeadshot", death_percentage, NULL);

               death_percentage += 5 * time_step;

               ent_sendnow(my);

               wait (1);

       }

}

 

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 remove_bullets()

{

       wait (1);

       ent_remove (my);

}

 

function move_bullets()

{

       VECTOR bullet_speed[3];

       my.emask = ENABLE_IMPACT | ENABLE_ENTITY | ENABLE_BLOCK;

       my.event = remove_bullets;

       my.pan = bullet_pan;

       my.skill99 = 1234;

       bullet_speed.x = 50 * time_step;

       bullet_speed.y = 0;

       bullet_speed.z = 0;

       while (my)

       {

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

               ent_sendnow(my);

               wait (1);

       }                

}

 

function fire_bullets()

{

       if (connection == 2)

       {

               client_fired = 1;

               wait (1);

               client_fired = 0;

       }

       if (connection == 3)

       {

               server_fired = 1;

               wait (1);

               server_fired = 0;

       }

}

 

function main()

{

       fps_max = 60;

       level_load ("multiplayer10.wmb");

       on_mouse_left = fire_bullets;

       if (!connection)

               sys_exit(NULL); // then shut down the engine

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

 

Finally, some fresh code! The following line displays the proper player health value for the client player. Don't forget that the "connection == 2" branch runs on the client.

 

                       players_health = my.health;

                       wait (1);

               }

       }

       if (connection == 3) // this instance of the game runs as a server and client at the same time? (connection = 3)

       {

               my = ent_create ("bluesoldier.mdl", vector (-100, -50, 40), move_players); // create the blue soldier

               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(); // call the simple camera function

 

We've got an identical line of code here; it displays the proper health value on the server (connection = 3).

 

                       players_health = my.health;

                       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;

 

If the server has fired a bullet and the server player is alive, we can create his bullets.

 

                               if (my.health > 0) // the server player is still alive?

                                       ent_create("bullet.mdl", bullet_pos.x, move_bullets); // then create the server player 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;

 

If the client has fired a bullet and the client player is alive, we can create his bullets.

 

                               if (you.health > 0) // the client player is still alive?

                                       ent_create("bullet.mdl", bullet_pos.x, move_bullets);        // create client's bullets (on the server)

                       }

                       wait (1);

               }

       }

}

 

See? That wasn't so complicated. The last example in this workshop will help us replace the ugly health indicators with health bars and will show us how and when to use local functions.

 

aum78_workshop3

 

Let's take a look at the multiplayer11.c source code right away:

 

#include <acknex.h>

#include <default.c>

 

var client_ent = 0;

var server_fired = 0;

var client_fired = 0;

var bullet_pan;

var players_health;

 

#define health skill20

 

BMAP* hit_tga = "hit.tga";

 

The following panel displays the red horizontal health bar at the top of the screen; we will change its scale_x value depending on player's health. Please note that the pos_x value of the panel is set to -6; I did this because when player's health is zero, we would have to set scale_x to zero and this doesn't work. The solution is simple: when player's health is zero, we set scale_x to a tiny values (0.01) and the negative pos_x value does the rest, hiding the tiny red bar that would otherwise be visible on the screen even when the player is dead.

 

PANEL* health_pan =

{

       bmap = "health.tga";

       pos_x = -6;

       pos_y = 0;

       flags = visible;

}

 

function damage_players()

{

       if (you.skill99 != 1234) {return;}

       my.health -= 10;

       my.health = maxv(0, my.health);

       send_skill(my.health, 0);

}

 

function move_players()

{

       var walk_percentage;

       var stand_percentage;

       var death_percentage = 0;

       my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY);

       my.event = damage_players;

       my.health = 100;

       send_skill(my.health, 0);

       while (my.health > 0)

       {

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

       }

       while (death_percentage < 100)

       {

               ent_animate(my, "DeathHeadshot", death_percentage, NULL);

               death_percentage += 5 * time_step;

               ent_sendnow(my);

               wait (1);

       }

}

 

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;

}

 

The following functions are your standard particle effect functions; there isn't any multiplayer code inside them.

 

function fade_hits(PARTICLE *p)

{

       p.alpha -= 4 * time_step;

       if (p.alpha < 0)

               p.lifespan = 0;

}

 

function player_hit(PARTICLE *p)

{

      p->vel_x = 0.2 - random(1) / 4;

      p->vel_y = 0.2 - random(1) / 4;

      p->vel_z = 0.3 + random(1) / 3;

      p.alpha = 25 + random(50);

      p.bmap = hit_tga;

      p.size = 5;

      p.flags |= (BRIGHT | MOVE);

      p.event = fade_hits;

}

 

Take a good look at the functions named hit_effect() and remove_bullets():

 

function hit_effect()

{

       effect(player_hit, 5, my.x, nullvector);

}

 

function remove_bullets()

{

       if (connection == 2)

       {

               proc_client(my, hit_effect);

       }

       else

       {

               effect(player_hit, 5, my.x, nullvector);

       }

       wait (1);

       ent_remove (my);

}

 

I have decided to add some particle effects in the areas / body parts that are hit by the bullets. Let me show you the previous version of function remove_bullets():

 

function remove_bullets()

{

       wait (1);

       ent_remove (my);

}

 

The old function was simply removing the bullet one frame after the impact. We can add a single line of code and get the desired particle effects like this:

 

function remove_bullets()

{

       effect(player_hit, 5, my.x, nullvector);

       wait (1);

       ent_remove (my);

}

 

This would work for sure, but there is better: we can get some of the load from the server and move it on the client, who will generate its own particles and save some of server's CPU's time, as well as some bandwidth. Server's particles will still be generated by the server, of course.

 

With this fresh information in our minds, let's take another look at function remove_bullets( ), who can do two different things:

 

function remove_bullets()

{

       if (connection == 2)

       {

               proc_client(my, hit_effect);

       }

       else

       {

               effect(player_hit, 5, my.x, nullvector);

       }

       wait (1);

       ent_remove (my);

}

 

1) If the code runs on the client (connection = 2), we use "proc_client" to start a local particle effect function; proc_client will call a simple function (hit_effect) that runs only on the "my" entity, which in our case is the client, because connection = 2.

2) If the code runs on the server, we start the usual particle effect function.

 

We don't need to run particle effects, explosions or (to be honest) even animations on the server. On the other hand, players' movements, bullets and other key actions have to be run on the server. As a general rule, if something can go wrong because of the bad timing, run it on the server.

 

Let's imagine that we have a flying bird; if it doesn't interact with anything in the level, we should run its function locally, on the client. On the other hand, if the bird is supposed to attack the player, make sure to run its function on the server. Try to run as many functions as possible locally; the local particle effects have decreased the needed bandwidth with 2-3%. It's not much, but when you are designing a real-life multiplayer project you have to squeeze as much performance as possible.

 

function move_bullets()

{

       VECTOR bullet_speed[3];

       my.emask = ENABLE_IMPACT | ENABLE_ENTITY | ENABLE_BLOCK;

       my.event = remove_bullets;

       my.pan = bullet_pan;

       my.skill99 = 1234;

       bullet_speed.x = 50 * time_step;

       bullet_speed.y = 0;

       bullet_speed.z = 0;

       while (my)

       {

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

               ent_sendnow(my);

               wait (1);

       }                

}

 

function fire_bullets()

{

       if (connection == 2)

       {

               client_fired = 1;

               wait (1);

               client_fired = 0;

       }

       if (connection == 3)

       {

               server_fired = 1;

               wait (1);

               server_fired = 0;

       }

}

 

function main()

{

       fps_max = 60;

       level_load ("multiplayer11.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();

                       players_health = my.health;

 

Take a look at the fresh line of code below: it sets the proper scale_x value for the health panel on the client, limiting its inferior value to 0.01.

 

                       health_pan.scale_x = maxv(0.01, my.health / 100);

                       wait (1);

               }

       }

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

                       players_health = my.health;

                       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;

                               if (my.health > 0)

                                       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;

                               if (you.health > 0)

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

                       }

 

The line below sets the proper scale_x value for the health panel on the server, just like we did it for the client.

 

                       health_pan.scale_x = maxv(0.01, my.health / 100);

                       wait (1);

               }

       }

}

 

And believe it or not, my friends, this is the end of our workshop.