|
Multiplayer |
Top Previous Next |
|
Lassen Sie uns den Workshop starten, indem Sie den multiplayer9\multiplayer9.cd Ordner öffnen und dann erst server.bat und danach client.bat starten.
Dieses Mal können unsere Kugeln den Gegner verletzen; feuern Sie auf den anderen Spieler und er wird auf den Boden fallen und sterben. Hier ist der Code, der das bewerkstelligt:
#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
Die Funktion damage_players ist die Eventfunktion der Spieler; diese prüft, ob ein Treffer durch eine Kugel stattgefunden hat. Falls keine solche Kollision stattfand (die Kugeln haben Skill99 auf 1234 gesetzt), geschieht nichts, andernfalls wird Skill10 des Spielers auf 0 gesetzt.
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)
Wie Sie sehen hat Skill10 anfangs den Wert 1 und die Schleife läuft, solange dies der Fall ist. Wie eben gesehen wird die Eventfunktion diesen Skill auf 0 setzen, sobald der Spieler getroffen wird. Der Inhalt der Schleife hat sich seit dem letzten Workshop nicht geändert, wir werden also nicht näher darauf eingehen.
{ 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); }
Die kleinere Schleife startet, sobald der Spieler stirbt; die Todesanimation wird abgespielt, bis death_percentage den Wert 100 erreicht. Beachten Sie die "ent_sendnow(my)" Anweisung, die dafür sorgt, dass die neue Position in jedem Frame geschickt wird, unabhängig vom Wert der "dplay_entrate" Variablen. Ohne diese Vorgehensweise wäre die Animation auf dem Client weniger flüssig, man würde aber weniger Daten über das Netzwerk schicken.
while (death_percentage < 100) { ent_animate(my, "DeathHeadshot", death_percentage, NULL); death_percentage += 5 * time_step; ent_sendnow(my); wait (1); } }
Diesen Code kennen Sie aus dem letzten Workshop. Die einzige Änderung ist die "my.skill99 = 1234;" Zeile, die dafür sorgt, dass die Kugeln als solche identifiziert werden können, damit die Spieler nicht sterben, wenn sie miteinander kollidieren.
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); } } }
Der "if (connection == 3)" Teil in der Main Funktion hat sich etwas geändert; Kugeln können nur noch abgefeuert werden, wenn Skill10 auf 1 gesetzt ist, also nur wenn der entsprechende Spieler noch lebt.
Starten Sie die Demo einige Male und bekämpfen Sie mal den einen und mal den anderen Spieler. Es ist etwas unbequem, das Spiel jedes Mal erneut starten zu müssen, wenn einer der Spieler getroffen wird, aber das wird sich glücklicherweise gleich ändern.
Dieses Mal haben die Spieler (primitive) Gesundheitsanzeigen; jeder Treffer verringert die Gesundheit um 10 und der Tod tritt erst ein, wenn diese bei 0 angelangt ist. Schauen wir uns multiplayer10.c an und sehen wir nach, wie das funktioniert.
#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; }
Zunächst haben wir eine neue Variable namens "players_health", welche die Gesundheit des Spielers in der oberen linken Ecke anzeigt. Wir haben auch "health" als skill20 definiert, wenn Sie also irgendwo "my.health" im Code sehen bedeutet dies das gleiche wie "my.skill20".
function damage_players() { if (you.skill99 != 1234) {return;} my.health -= 10; my.health = maxv(0, my.health); send_skill(my.health, 0); }
Die Funktion damage_players() hat sich etwas verändert. Wird ein Spieler getroffen, verringert sich seine Gesundheit um 10 Punkte. Die folgende Zeile stellt sicher, dass die Gesundheit nicht negativ werden kann, auch wenn der Spieler häufiger getroffen wird; es sieht nicht sehr professionell aus wenn eine Gesundheit von -60 angezeigt wird, selbst wenn der Spieler tot ist. Schließlich sorgt die "send_skill" Anweisung dafür, dass der neue Gesundheitswert über das Netzwerk geschickt wird, wenn dieser sich geändert hat.
function move_players() { var walk_percentage; var stand_percentage; var death_percentage = 0; my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY); my.event = damage_players;
Sehen Sie sich die folgenden beiden Zeilen an: die erste setzt die Gesundheit des Spielers auf 100 und die zweite sendet diesen Wert sofort über das Netzwerk, damit der Client zum Spielstart über die passende Gesundheit verfügt. Ohne diese Zeile würde der Client Spieler zwar am Anfang über 100 Health verfügen, dies würde aber nicht angezeigt, da der Wert nicht vom Server stammt. Beachten Sie, dass die folgende Schleife solange läuft bis my.health kleiner oder gleich 0 ist.
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();
Endlich frischer Code! Diese Zeilen zeigen den korrekten Gesundheitswert für den Client an. Vergessen Sie nicht, dass "connection == 2" auf dem Client richtig ist.
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
Hier geschieht dasselbe für den 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;
Falls der Spieler auf dem Server gefeuert hat und noch lebt, wird die Kugel erstellt.
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;
Desgleichen für den Client.
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); } } }
Sehen Sie? Das war ganz einfach. Das letzte Beispiel im Workshop wird uns dabei helfen, die häßlichen Gesundheitsanzeigen durch Balken zu ersetzen und uns zeigen wie und wann wir lokale Funktionen nutzen.
Schauen wir uns den Code in der Datei multiplayer11.c direkt an.
#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";
Das folgende Panel zeigt den roten Balken am oberen Bildschirmrand an; wir ändern den scale_x Wert in Abhängigkeit der Gesundheit des Spielers. Beachten Sie, dass der pos_x Wert des Panels auf -6 gesetzt wurde. Das hat folgenden Hintergrund: bei einer Gesundheit von 0 müssten wir scale_x auf 0 setzen, was nicht zugelassen ist. Die Lösung ist einfach: bei einer Gesundheit von 0 wird scale_x auf einen sehr kleinen Wert gesetzt (0.01) und der negative pos_x Wert sorgt dafür, dass man das Panel nicht mehr sehen kann, wenn der Spieler tot ist.
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; }
Die folgenden Funktionen beinhalten gewöhnliche Partikeleffekte ohne Multiplayer Code.
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; }
Sehen Sie sich die Funktionen hit_effect() und remove_bullets() gut an.
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); }
Ich habe mich dafür entschieden, einige Partikeleffekte dort zu erstellen, wo die Kugeln einschlagen. So sah die Funktion remove_bullets() vorher aus:
function remove_bullets() { wait (1); ent_remove (my); }
In der alten Funktion wurden die Kugeln einfach entfernt. Mit einer einzelnen Codezeile erreichen wir den gewünschten Partikeleffekt.
function remove_bullets() { effect(player_hit, 5, my.x, nullvector); wait (1); ent_remove (my); }
Dies funktioniert, aber es geht noch besser: wir können dem Server ein wenig Last abnehmen und die Funktion auf den Client verlagern, der seine eigenen Partikel erzeugt und damit CPU Zeit des Servers und Daten zum Übertragen einspart. Die Partikel für den Server werden natürlich weiter dort generiert.
Mit diesem Wissen im Hinterkopf können wir remove_bullets() erneut anschauen.
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) Falls der Code auf dem Client läuft, verwenden wir "proc_client" und starten so einen lokalen Partikeleffekt. Die Funktion hit_effect läuft nur auf der aufrufenden Entity, in diesem Fall also auf dem Client. 2) Falls der Code auf dem Server läuft, rufen wir die Partikelfunktion auf wie immer.
Wir müssen Partikeleffekte, Explosionen oder ehrlicherweise auch Animationen nicht auf dem Server ausführen. Auf der anderen Seite sollten die Bewegungen der Spieler, die Kugeln und andere wichtige Dinge vom Server geregelt werden. Eine Grundregel lautet: Wenn eine Sache zeitkritisch ist, sollte sie auf dem Server stattfinden.
Stellen wir uns vor, dass wir einen Vogel im Level haben. Wenn er nur zur Zierde dient und mit nichts im Level interagiert, kann seine Funktion lokal auf dem Client laufen. Falls der Vogel aber den Spieler angreifen soll, dann muss seine Bewegung vom Server gesteuert werden. Versuchen Sie, soviele Funktionen wie möglich lokal aufzurufen, um die Bandbreite so gut wie möglich für wichtige Dinge auszunutzen. In unserem Beispiel verringern lokale Partikeleffekte die genutzte Bandbreite um 2 - 3%. Nicht viel, aber in einem ambitionierten Multiplayerprojekt zählt jedes Quentchen an Daten.
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;
Schauen Sie sich die neue Codezeile an: sie setzt den korrekten scale_x Wert für den Gesundheitsbalken auf dem Client und beschränkt diesen Wert nach unten durch 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); }
Und hier geschieht dasselbe auf dem Server.
health_pan.scale_x = maxv(0.01, my.health / 100); wait (1); } } }
Ob Sie es glauben oder nicht, hier endet unser Workshop.
|