Multiplayer

Top  Previous  Next

In diesem Monat bringen wir unseren Spielern bei, wie sie aufeinander schießen können. Zunächst müssen wir aber ein ernstes Problem lösen: Wie kommen wir an die Pointer für die Spieler?

 

In einem normalen Splitscreenspiel würden wir zwei Pointer (z.B. "player1" und "player2") definieren und mit diesen dann die Spieler kontrollieren. In einer Multiplayer Anwendung laufen die Dinge ein wenig anders: der Spieler auf dem Server (nennen wir ihn "player1") kann problemlos kontrolliert werden, da die Action auf dem Server läuft. Der Pointer für den Spieler auf dem Client ("player2") ist aber nur auf dem Client gültig und wir können vom Server aus nicht direkt zugreifen.

 

Warum das so ist? Eine Codezeile wie "player2 = me;", die auf dem Client ausgeführt wird, setzt den Zeiger auch nur auf dem Client richtig und so können wir vom Server aus nicht darauf zugreifen. Es wäre also vorteilhaft, wenn wir diesen Pointer irgendwie an den Server senden könnten.

 

Falls wir die Kontrolle einiger Actions an den Client abgeben wollen und ihm z.B. erlauben, selbst zu feuern, werden wir mit Sicherheit Probleme bekommen; stellen Sie sich einfach vor, der Spieler auf dem Client könnte jederzeit feuern und die Verbindung mit dem Server wäre schlecht: die Kugeln würden ihr Ziel viel später erreichen! Der Client würde sehen, dass sie sofort treffen, aber aufgrund einer schlechten Verbindung würde der Server davon erst später etwas mitbekommen. Daher ist klar, dass der Server das Abfeuern der Kugeln übernehmen muss, egal ob es ein Spieler auf dem Server oder auf dem Client ist, der diese Aktion auslöst.

 

Wir müssen also einen Pointer auf den Client Spieler ermitteln und diesen über das Netzwerk zum Server senden. Zum Glück geht das ganz einfach. Öffnen Sie den Code in multiplayer7.c; er beginnt mit einigen Include Anweisungen und Deklarationen, die wir vorerst ignorieren können.

 

#include <acknex.h> 

#include <default.c>

 

var client_ent = 0;

var server_click = 0;

var client_click = 0;

 

Dann folgt die leicht modifizierte move_players() Funktion, die wir im letzten Workshop behandelt haben:

 

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

       }

}

 

Schließlich folgt frischer 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;

       }

}

 

Die Funktion players_click() wird von der main() Funktion aus aufgerufen, wenn immer einer der Spieler (Client oder Server) die linke Maustaste drückt.

 

Wichtiger Tipp: Die Funktion main() läuft auf dem Server und allen Clients. Wenn Sie also sicherstellen wollen, dass ein bestimmter Teil des Codes auf allen Computern ausgeführt wird, dann schreiben Sie ihn in die main() Funktion oder rufen Sie ihn von dort auf.

 

Zurück zum Code: Falls connection den Wert 2 hat, dann befinden wir uns auf dem Client und setzen client_click einen Frame lang auf 1. Das soll das Signal dafür sein, dass der Spieler auf dem Client die linke Maustaste betätigt hat. Hat connection hingegen den Wert 3, dann läuft der Code auf dem Server und entsprechend wird server_click auf 1 gesetzt. Die Variablen werden in der main() Funktion verarbeitet, also schauen wir uns diese mal an.

 

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

 

Wir sind ähnlichem Code bereits in früheren Workshops begegnet, also wenden wir uns direkt den Neuerungen zu.

 

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

               }

       }

 

Falls connection den Wert 2 hat, läuft das Spiel auf dem Client. Wir erstellen den roten Soldaten und warten dann eine halbe Sekunde bis der handle auf den Client Spieler bereit steht. Falls Sie die "wait(0.5);" Anweisung vergessen haben, dürfen Sie sich nicht wundern, warum der Pointer auf dem Client keine Gültigkeit besitzt.

 

Die folgenden Zeilen speichern das Handle in der Variable "client_ent". Der Grund dafür ist, dass wir mit Hilfe von handles die Client Entity Pointer über das Netzwerk versenden, da diese handles nichts weiter als Zahlen sind, die mit der "send_var" Anweisung versandt werden können.

Und dies ist auch, was in der folgenden Codezeile geschieht: client_ent wird vom Client zum Server geschickt, welcher dann mit Hilfe der Anweisung ptr_for_handle den Zeiger rekonstruiert.

 

Der Rest der Zeilen kümmert sich um die Bewegung der Client Entity, die ja auch auf dem Server durchgeführt wird. Wir stellen auch sicher, dass der Inhalt der "client_click" Variablen zum Server geschickt wird, schließlich soll der Server darauf reagieren, wenn der Client die linke Maustaste drückt.

 

Ich hätte auch den bislang ungenutzten skill3 für client_click verwenden können, weil send_vec automatisch die ersten drei Skills versendet, aber ich wollte sicherstellen, dass der Code leichter lesbar und verständlicher ist.

 

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

               }

       }

}

 

Der "if (connection == 3)" Teil läuft auf dem Server und erstellt den blauen Soldaten; in der Schleife wird dieser mit Hilfe der Werte skill1 und skill2 bewegt.

Falls server_click den Wert 1 hat (der Spieler auf dem Server hat die linke Maustaste betätigt), vergrößern wir die Höhe des Spielers um 10 Quants. In diesem Fall enthält "my" einen Pointer zu der entsprechenden Entity, wir können also direkt darauf zugreifen.

 

Falls client_click auf 1 steht, ermitteln wir den Zeiger auf die Entity mit ptr_for_handle und dem Wert von client_ent, welcher ganz am Anfang über das Netzwerk versandt wurde. Ich habe den vordefinierten "you" Pointer verwendet, Sie können natürlich auch jeden anderen benutzen. Die folgende Zeile vergrößert die Höhe der Client Entity um 10 Quants.

 

Betrachten wir den Mechanismus noch einmal:

1) Der Client schickt ganz zu Anfang ein Handle zu der Entity des Client Spielers über das Netzwerk an den Server.

2) In jedem Frame sendet der client den Inhalt der Variablen client_click.

3) Falls diese den Wert 1 hat, ermittelt der Server mit ptr_for_handle den Zeiger auf die Client Entity.

4) Nun kann der Client Spieler vom Server aus Aktionen ausführen, wie gewünscht.

 

Zeit, all dies zu testen. Starten Sie erst server.bat und dann client.bat.

 

aum77_workshop1

 

Klicken Sie irgendwo in die Server und Client Fenster und Sie werden sehen, dass die Models ihre Höhe ändern. Es ist uns also gelungen, beide Spieler Entities auf dem Server anzusprechen, wir können also damit beginnen zu schießen.

 

Schauen wir uns direkt multiplayer8.c an. Er beginnt beinahe ebenso wie der vorige Teil.

 

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

       }

}

 

Die Funktion simple_camera() erstellt eine einfache Außenkamera, die 200 Quants hinter und 70 Quants über dem Spieler steht und seinen Blickwinkel einnimmt. Diese Funktion wird von der Funktion main() aus sowohl vom Server als auch vom Client in jedem Frame aufgerufen.

 

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

               }

       }

 

Der Abschnitt, der auf dem Client läuft (connection == 2) erstellt den roten Soldaten, wartet bis der Pointer bereitsteht und schickt das handle wie vorhin über das Netzwerk. Ich habe die Variable "client_clicked" in "client_fired" umbenannt, aber die Funktionsweise ist noch dieselbe. Wieder wird diese Variable einmal pro Frame an den Server geschickt, da dieser die Aufgabe hat, die Kugeln zu erstellen. Schließlich stellt simple_camera() sicher, dass der Client Spieler immer aus einer korrekten Außenansicht zu sehen ist.

 

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

               }

       }

}

 

Dieser code läuft auf dem Server und erstellt den blauen Spieler und kümmert sich dann um die Kamera und die Bewegung. Falls server_fired auf 1 gesetzt ist, dann hat der Spieler des Servers die linke Maustaste gedrückt, also erzeugen wir eine Kugel, die relativ zum Origin des Spielermodels um 30 Quants in x-Richtung, -5 Quants in y-Richtung und 15 Quants in z-Richtung versetzt ist. Natürlich hat die Kugel dieselbe Ausrichtung wie der Spieler und startet die Funktion move_bullets.

 

Falls client_fired auf 1 steht, ermitteln wir wie vorher den Zeiger auf den Client Spieler und verwenden die Koordinaten, um die Kugel zu erzeugen.

 

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

}

 

Beachten Sie, dass die Funktion move_bullets() stets auf dem Server läuft.

 

Wichtiger Hinweis: Jede ent_create Anweisung bezieht sich auf den Server! Verwenden Sie ent_createlocal, wenn Sie etwas nur auf dem Client benötigen.

 

Die Kugel reagiert auf Kollisionen mit anderen Entities und Level Blocks und verwendet eine einfache Event Funktion, welche die Kugel einfach entfernt, wenn sie auf etwas trifft. Sie bewegt sich mit 50 * time_step Quants / Sekunde; ändern Sie diesen Wert, wenn Sie möchten. Besonders wichtig hier ist die "ent_sendnow(my);" Codezeile, welche dafür sorgt, dass die neue Position der Kugel in jedem Frame gesendet wird, so dass sie sich auch auf den Clients flüssig bewegt.

 

Zeit für einen Test! Starten Sie server.bat und danach client.bat und schießen Sie los!

 

aum77_workshop2

 

Im nächsten Monat kümmern wir uns um Schaden und Gesundheit der Spieler, vielleicht besprechen wir auch ent_createlocal. Bis dahin!

 

P.S.: Wir schicken noch nicht besonders viele Daten über das Netzwerk. Meine Tests zeigen, dass wir bislang problemlos 20 Clients über eine Modemverbindung anbinden können.