Der vierte Teil dieser Reihe fügt eine wichtige Features zu dem Code hinzu, den wir bisher haben. Vergessen Sie nicht, die Artikel aus AUM 27 – 29 zu lesen, um zu sehen wie wir angefangen haben.
Öffnen Sie level4, lassen Sie ihn ausrechnen und starten Sie ihn mit ai4.wdl:
Holen Sie tief Luft und laufen Sie los; die vier Feinde werden versuchen, Sie zu jagen. Benutzen Sie WASD, um sich zu bewegen und die linke Maustaste, um zu feuern. Versuchen Sie, so lange wie möglich zu überleben; sehen Sie sich die vier Pfade an, die auf dem Bildschirm angezeigt werden und beachten Sie, wie sie sich je nach Ihrer Position ändern. Ich hätte mehr Gegner in dieser Demo benutzen können, aber glauben Sie mir, vier reichen erstmal. Oh, es ist möglich, alle vier zu eliminieren, ich habe es einmal geschafft.
Einiges an Code aus Aum29 hat sich geändert, es kam auch neuer Code hinzu, aber keine Sorge, wir werden alle neuen oder modifizierten Actions und Functions durchgehen, die unter dem Header in ai4.wdl stehen:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
new stuff for the 4th Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
define
health skill42;
define
start_node skill43;
define
closest_node skill44;
Mit einem simplen Panel zeigen wir die Pfade für die vier Gegner an, ebenso wie die Gesundheit des Spielers und den Zielknoten (das Ziel der Gegner):
panel
ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 72, 2, system_font, 1, player.health;
digits = 80, 85, 2, system_font, 1, target_node;
digits = 120, 0, 4, system_font, 1, path[0]; // display the first 15 points
on the shortest path
digits = 150, 0, 4, system_font, 1, path[1]; // should be enough for everybody
digits = 180, 0, 4, system_font, 1, path[2];
// many more digits here
............................................................
flags = overlay, refresh, visible;
}
Diese Demo benutzt 30 Knoten, die Informationen austauschen, schauen wir uns die Action an, die ihnen zugeordnet ist:
action
node // using 30 nodes in this demo
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
node_id += 1;
my.skill48 = node_id;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (node_id < (max_nodes - 1)) {wait (1);}
Jeder Knoten ist unsichtbar und reagiert auf Scans; wird ein Knoten gescannt, läuft sein trace_back Event. Wir setzen skill47 auf 1234 für jeden Knoten und machen ihn passierbar, damit er kein Hindernis für den Spieler oder die Gegner darstellt. Jeder Knoten erhält außerdem eine eindeutige ID Zahl, die in skill48 gespeichert wird. Außerdem sichern wir die x und y Koordinaten des Knotens in zwei Arrays namens nodex und nodey und dann warten wir, bis alle Knoten im Level sind.
while (current_node <= node_id)
{
if (current_node == my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
wait (1);
current_node += 1;
}
wait (1);
}
distances_computed = 1;
Die Knoten scannen einander in der Reihenfolge: Knoten 0 scannt Knoten 1, dann Knoten 2, ... Knoten 29 und dann scannt Knoten 1 Knoten 0, Knoten 2, ... bis Knoten 29 usw. Wir benutzen einen horizontalen Scanwinkel von 360 Grad und einen vertikalen von 30 Grad mit einer Reichweite von 1000 Quants. Sobald der letzte Knoten (29) die Knoten 0 – 28 gescannt hat, wird die Variable distances_computed auf 1 gesetzt.
while (1)
{
node_to_player[my.skill48] = vec_dist(player.x, my.x);
if (node_to_player[my.skill48] < 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}
Jeder Knoten speichert seinen Abstand zum Spieler im Array node_to_player; wenn der Spieler näher als 500 Quants kommt, wird ein Trace ausgeführt und wenn der Knoten den Spieler “sehen” kann, wird see_player auf 1 gesetzt, ansonsten auf 0. All diese Dinge geschehen 10 Mal pro Sekunde.
Schauen wir uns nun an, was geschieht, wenn ein Knoten einen anderen scannt:
function
trace_back()
{
if (event_type == event_scan)
{
if (you.skill47 == 1234) // scanned by a node
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
Wird einer der Knoten gescannt, prüfen wir ob die scannende Entity ihren skill47 auf 1234 hat; ist dies der Fall, so wissen wir, dass der Scanner ein anderer Knoten war. Wir speichern die “you” Entity in skill45, weil der Trace diese überschreibt und tracen dann zurück zum Scanner. Wenn der Knoten den scannenden Knoten “sehen” kann, stellt es den “you” Zeiger wieder her und speichert den Abstand der Knoten im Vektor namens node_to_node (ein zweidimensionales Array simuliert durch ein einziges).
else
{
my.skill45 = handle(you); // store "you" because trace will destroy it
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0) // if this node can "see" the enemy that
scanned it
{
you = ptr_for_handle(my.skill45); // restore the "you" pointer
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = vec_dist (my.x,
you.x);
if ((dist_to_enemy[my.skill48 + max_enemies * you.skill48] < you.closest_node)
&& (dist_to_enemy[my.skill48 + max_enemies * you.skill48] != 0))
{
you.closest_node = dist_to_enemy[my.skill48 + max_enemies * you.skill48];
}
wait (2);
if (dist_to_enemy[my.skill48 + max_enemies * you.skill48] == you.closest_node)
{
you.start_node = my.skill48;
}
}
else
{
you = ptr_for_handle(my.skill45);
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = 0;
}
}
}
}
Der “else” Teil läuft, wenn ein Knoten von einem der Gegner gescannt wurde; wir speichern “you” wieder und tracen zurück zum Gegner, wobei der Abstand im Array namens dist_to_enemy gespeichert wird. Wenn der Abstand des Knotens zum Gegner geringer ist als die alte Distanz (closest_node = skill44), die der Gegner gespeichert hat, aber größer als 0, so wird dieser Knoten als dem Feind am Nächsten zugeordnet. Wir geben dem Rest der Knoten die Chance, dies innerhalb von zwei Frames zu ändern (falls ein anderer noch näher dran ist) und setzen dann den Startknoten für den Feind als denjenigen, der ihm am nächsten ist. Wenn der Knoten den Feind nicht “sehen” kann, wird dist_to_enemy auf 0 gesetzt.
All diese Dinge laufen ab, weil der Gegner wissen muß, welcher Knoten ihm am nächsten ist und ihn sehen kann. Der Gegner bewegt sich auf den nächsten Knoten zu und startet dann die Funktion, die den kürzesten Weg zum Spieler erzeugt (mehr dazu später).
Sehen wir uns meine Lieblings-Action an:
action
player1
{
player = me;
my.health = 100;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health;
while (my.health > 0)
{
my.pan += 8 * (key_a - key_d) * time;
my.skill1 = 15 * (key_w - key_s) * time;
Der Spieler hat 100 Punkte Gesundheit; sein Push Wert ist –1, also kann er sich nicht durch Wände bewegen, die einen Push von 0 haben. Der Spieler reagiert auf andere Entities und das decrease_health Event startet, wenn er von etwas getroffen wird. Solange der Spieler lebt, kann er sich mit den Tasten “A” und “D” drehen und mit “W” und “S” bewegen.
if (key_w + key_s > 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else
{
ent_cycle("idle", my.skill46);
my.skill46 += 2 * time;
my.skill46 %= 100;
}
if (mouse_left == 1)
{
players_bullet();
}
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}
Wenn der Spieler sich bewegt (“W” oder “S” sind gedrückt), läuft die “Walk” Animation des Models in einer Schleife, ansonsten ist die “Idle” Animation zu sehen. Drückt der Spieler die linke Maustaste, so läuft die Funktion players_bullet. Das Spieler Model ignoriert passierbare Entities und gleitet an Mauern entlang.
my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46);
my.skill46 += 2 * time;
wait (1);
}
}
Die obigen Zeilen laufen ab, wenn der Spieler gestorben ist: die “Death” Animation wird einmal abgespielt. Schauen wir uns die komplizierteste Action an – die Funtion, die den Gegner zugeordnet ist:
action
enemy
{
my.push = -2;
my.enable_entity = on;
my.enable_impact = on;
my.health = 100;
my.event = enemy_event;
enemy_id += 1;
my.skill48 = enemy_id;
my.skill47 = 5678;
while ((player.health > 0) && (my.health > 0))
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
my.skill46 = 0;
shoot_bullet();
while (my.skill46 < 100)
{
ent_cycle("standshoot", my.skill46);
my.skill46 += 15 * time;
wait (1);
}
}
Jeder Gegner hat einen Push von –2, auf diese Weise können sie sich nicht gegenseitig töten, weil ihre Kugeln einen push von –1 haben. Der Gegner reagiert auf andere Entities und Kollisionen (impact), seine Gesundheit steht auf 100 und sein Event ist enemy_event. Jeder Gegner erhält eine einzigartige ID (0 – 3 in dieser Demo), die in skill48 gesichert wird. Skill47 wird auf 5678 gesetzt, auf diese Weise können wir feststellen, ob eine bestimmte Entity ein Gegner ist oder nicht.
Die Schleife läuft, solange der Spieler und der Gegner am Leben sind. Darin wird ein Trace zum Spieler ausgeführt; wenn der Gegner den Spieler sehen kann, beginnt er zu schießen, indem er die Funktion shoot_bullet verwendet. Außerdem läuft seine “standshoot” Animation ab.
else
{
my.closest_node = 999999;
temp.x = 360;
temp.y = 360;
temp.z = 800;
scan_entity (my.x, temp);
if (vec_dist (my.x, player.x) > 1000)
{
my.skill46 = 0;
while (my.skill46 < 100)
{
ent_cycle("alert", my.skill46);
my.skill46 += 2 * time;
wait (1);
}
}
else
{
wait (2);
}
while (function_busy == 1) {wait (1);}
function_busy = 1;
Wenn der Gegner den Spieler nicht sieht, setzt er seine closest_node Distanz auf einen riesigen Wert und scannt dann die Knoten in der Nähe, um den zu ermitteln, der ihm am nächsten ist. Ist der Spieler mehr als 1000 Quants entfernt, zeigt der Gegner seine “alert” Animation, um anzudeuten, dass er nachdenkt, bevor er einen Pfad wählt, der ihn zum Spieler führt. Ist der Gegner näher als 1000 Quants am Spieler, so wartet er 2 Frames, ehe er einen neuen Pfad wählt.
Eine einzelne Funktion berechnet die Pfade für alle Gegner. Wenn sie beschäftigt ist, wird eine Variable namens function_busy auf 1 gesetzt, sonst auf 0. Der Gegner wartet, bis function_busy auf 0 steht und benutzt die Funktion dann und setzt function_busy auf 1.
path_index = 0;
while (path_index < max_nodes)
{
path[path_index + max_enemies * my.skill48] = 0;
path_index += 1;
}
path_index = 0;
path[path_index + max_enemies * my.skill48] = target_node;
snd_play (getpath_wav, 100, 0);
if (my.start_node != target_node)
{
node_index = find_path(my.start_node, target_node, my.skill48, path_index);
wait (2);
function_busy = 0;
Als erstes wird der Array “path” zurückgesetzt, weil wir den vorher gespeicherten Pfad loswerden müssen. Target_node, der Knoten, der dem Spieler am nächsten ist und ihn sehen kann, ist das erste Element des neuen Pfades. Die Engine läßt ein Geräusch ertönen und deutet so an, dass einer der Gegner den kürzesten Pfad zum Spieler sucht. Wenn Startknoten und Zielknoten verschieden sind, läuft die Funktion find_path, die vier Parameter erhält und das Ergebnis in node_index schreibt. Wir warten, bis der Pfad berechnet ist und befreien die Funktion durch Setzen von function_busy auf 0.
while (node_index >= 0)
{
temp.x = nodex[path[node_index + max_enemies * my.skill48]];
temp.y = nodey[path[node_index + max_enemies * my.skill48]];
temp.z = my.z;
vec_set (destination_node, temp);
vec_set (temp.x, destination_node.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
while ((vec_dist (destination_node.x, my.x) > 10) && (relax ==
0))
{
ent_cycle("run", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
if (trace (my.pos, player.pos) == 0)
{
node_index = -1;
}
wait (1);
}
node_index -= 1;
wait (1);
}
}
}
}
Der kürzeste Pfad zum Spieler wurde von der Funktion find_path zurückgeliefert; nun müssen wir den Gegner auf diesen Pfad bringen! Wir holen uns die x und y Koordinaten der Knoten auf dem Pfad und benutzen die Höhe des Gegners für den z Wert; dann drehen wir den Feind in Richtung des Zielknotens. Solange der Gegner nicht näher als 10 Quants am Spieler ist und der Spieler noch nicht tot ist (relax = 0), wird die “Run” Animation des Gegners angezeigt und er bewegt sich, wobei er passierbare Entities ignoriert und an Wänden entlang gleitet.
Wenn der Gegner den Spieler sehen kann (Trace liefert 0 zurück), wird node_index auf –1 gesetzt, damit der Feind aus der Schleife kommt, um einen neuen Pfad zu berechnen, weil der kürzeste Pfad sich geändert hat. Schließlich wird node_index verringert, falls der Spieler näher als 10 Quants an den Knoten gekommen ist, damit er zum nächsten aufbrechen kann.
// the player is dead, so switch to "stand" if you are alive
while (my.health > 0)
{
ent_cycle("stand", my.skill46); // play "stand" frames animation
my.skill46 += 4 * time; // "stand" animation speed
my.skill46 %= 100;
wait (1);
}
}
Die Zeilen oben laufen, wenn der Spieler gestorben ist; der Gegner schaltet auf seine “stand” Animation, falls er auch lebt und spielt diese Animation weiter ab. Die Funktion unten berechnet und schreibt den Pfad für jeden Gegner auf, der sie benutzt:
function
find_path(sn, tn, id, px)
{
var p_i = 0;
while (p_i < max_nodes)
{
if ((p_i != tn) && (visited[p_i + max_nodes * tn] == 0))
{
if (node_to_node[sn + max_nodes * tn] == node_to_node[sn + max_nodes *
p_i] + node_to_node[p_i + max_nodes * tn])
{
px += 1;
path[px + max_enemies * id] = p_i;
next_node = p_i;
p_i = max_nodes - 1;
if (path[px + max_enemies * id] == sn)
{
return(px);
}
}
}
p_i += 1;
}
tn = next_node;
find_path(sn, tn, id, px);
}
Sie
erhält 4 Parameter:
-
Startknoten (sn), der Knoten, der dem Gegner am nächsten ist und Sichtkontakt
zu ihm hat;
-
Zielknoten (tn), der Knoten, der dem Spieler am nächsten ist und Sichtkontakt
zu ihm hat;
-
Gegner id (id), eine Zahl, die der Funktion sagt, welcher Gegner den Pfad
anfordert;
-
Pfad Index (px), benutzt als ein Index für den kürzesten Pfad;
Die Funktion sucht von dem aktuellen Knoten zum Rest der Knoten; in dieser Demo kommen 30 Knoten vor, also muß jeder Knoten 29 andere testen. Wenn der aktuelle Knoten einen oder mehrere der anderen Knoten sehen kann und wenn wir einen Knoten haben, die im kürzesten Pfad enthalten ist, gehen wir zum nächsten Array Eintrag und speichern die Zahl des Knotens im path[] Array (zweidimensionales Array durch ein einfaches simuliert). Wir speichern den neuen Knoten auf dem kürzesten Pfad in next_node, weil der Wert gleich verloren ist und hören dann auf, andere Werte zu testen (p_i = max_nodes – 1), weil wir andere Pfade der gleichen Länge ausschließen wollen.
Haben wir den Knoten erreicht, der den Startpunkt des Gegners darstellt, verlassen wir die Funktion und geben den Wert in px zurück. Läuft die Funktion aber weiter, wird p_i erhöht; wenn der aktuelle Knoten keinen anderen sehen kann, setzt tn = next_node den nächsten Knoten auf dem Pfad als Ziel und die rekursive Funktion wird erneut aufgerufen, einen Schritt näher am Ziel.
Der schwere Teil ist vorbei, der Rest der Funktionen sind simpel:
function
shoot_bullet()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
if (player.health > 0)
{
ent_create (bullet_mdl, my.pos, move_bullet);
}
}
Diese Funktion läuft, wenn der Gegner den Spieler sehen kann; er wird zum Spieler gedreht und wenn der Spieler noch lebt, schießt er mit der Funktion move_bullet():
function
move_bullet()
{
my.skill47 = 5;
my.pan = you.pan;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
while (my != null)
{
my.skill1 = 15 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable + ignore_push;
ent_move(my.skill1, nullvector);
wait (1);
}
}
Ich habe skill47 auf 5 gesetzt für jede Kugel, die von einem Gegner kommt; die Kugel und der Gegner, von dem sie kommt, haben denselben Pan Winkel. Der Push Wert der Kugel ist –1, da sie auf diese Weise durch Gegner fliegen kann ohne sie zu treffen (deren Push ist –2), aber den Spieler trifft (sein Push war –1). Die Kugel reagiert auf Entities und Level Blocks, die Event Funktion heißt remove_bullet. Solange sie noch existiert, bewegt sie sich mit der Geschwindigkeit gegeben durch skill1, ignoriert die erzeugende Entity, passierbare Entities und alle Entities, deren Push unterhalb –1 liegt.
function
remove_bullet()
{
if ((my.skill47 == 5) && (you.skill47 == 5))
{
my.passable = on;
wait (2);
ent_remove (me);
return;
}
my.event = null;
ent_playsound (my, hit_wav, 1000);
sleep (0.1);
ent_remove (me);
}
Die Funktion oben läuft, wenn die Kugel etwas trifft. Falls zwei Kugeln kollidieren, wird eine davon passierbar gemacht und nach zwei Frames entfernt; trifft die Kugel etwas anderes, reagiert sie nicht mehr auf andere Events, läßt ein hit_was Geräusch ertönen und verschwindet nach 0,1 Sekunden.
function
decrease_health()
{
if (you.skill47 == 5678)
{
my.health = 0;
}
else // hit by a bullet?
{
my.health -= 1;
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav, 40, 0);
}
}
Die Funktion decrease_health läuft, wenn der Spieler von etwas getroffen wurde; ist der Spieler in einen Gegner gerannt (you.skill47 == 5678), stirbt er sofort, weil der Gegner ihn erdolcht (mehr weiter unten). Wird er von einer Kugel getroffen, verliert er einen Punkt Gesundheit pro Tick; die Kugel ist 0,1 Sekunden aktiv, die Mathematik überlasse ich Ihnen. Sinkt die Gesundheit des Spielers unter 0, reagiert er nicht mehr auf Events und läßt sein death_wav Geräusch ertönen. Schauen wir uns die Event Funktion der Gegner an:
function
enemy_event()
{
if (you.skill47 == 5678)
{
if ((vec_dist (my.x, player.x)) > (vec_dist (you.x, player.x)))
{
my.passable = on;
sleep (0.5);
my.passable = off;
}
}
Falls der Gegner mit einem anderen Gegner kollidiert ist und “you” näher am Spieler steht als “me”, wird “me” passierbar und erlaubt so, dass “you” weitergeht; diese Situation hält 0,5 Sekunden an.
if (you == player)
{
my.event = null;
player.health = 0;
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
my.skill46 = 0;
while (my.skill46 < 90)
{
ent_cycle("point", my.skill46);
my.skill46 += 1 * time;
wait (1);
}
relax = 1;
}
Kollidiert der Gegner mit dem Spieler, hört er auf, auf andere Events zu reagieren. Der Spieler verliert sein Leben und der Gegner dreht sich zu ihm und zeigt seine “point” (erdolchen) Animation einmal, dann wird die Variable “relax” auf 1 gesetzt, was den Gegnern erlaubt, sich auszuruhen (stehenbleiben und die Animation auf “stand” schalten)
if (you.skill47 == 1)
{
my.health -= 1;
if (my.health <= 0)
{
my.event = null;
my.invisible = on;
my.passable = on;
}
}
}
Wenn ein Gegner von der Kugel des Spielers getroffen wurde (skill47 == 1), verliert er einen Punkt Gesundheit pro Tick. Ist der Gegner tot, hört er auf, auf Events zu reagieren und wird dann unsichtbar und passierbar.
function
players_bullet()
{
proc_kill(4);
while (mouse_left == 1) {wait (1);}
snd_play (bullet_wav, 50, 0);
ent_create (bullet_mdl, player.pos, move_players_bullet);
}
Die Funktion oben läuft, wenn der Spieler die linke Maustaste drückt; proc_kill(4) stoppt alle anderen Instanzen der laufenden Funktion. Wir warten, bis die Maustaste losgelassen wird (kein Autofeuer), lassen ein Geräusch ertönen und erzeugen eine Kugel, die von der Funktion move_player_bullet() bewegt wird:
function
move_players_bullet()
{
my.skill47 = 1;
my.pan = you.pan;
my.tilt = you.tilt;
my.passable = on;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
my.ambient = 100;
Jede vom Spieler gefeuerte Kugel hat skill47 auf 1 gesetzt; sie hat denselben Pan und Tilt Winkel wie der Spieler. Anfangs ist sie passierbar und reagiert auf andere Entities und Level Blocks. Die Kugeln vom Gegner und Spieler teilen sich die Eventfunktion remove_bullet().
while (my != null)
{
if (vec_dist (player.x, my.x) > 30) {my.passable = off;}
my.skill1 = 50 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_move (my.skill1, nullvector);
wait (1);
}
}
Die Kugel hört auf, passierbar zu sein, wenn der Abstand zum Spieler größer ist als 30 Quants. Sie bewegt sich mit der Geschwindigkeit, die in skill1 steht und ignoriert dabei der erzeugende Entity und alle passierbaren Entities.
“Im nächsten Monat verlassen wir unser liebgewonnenes Labyrinth und gehen in ein Level, das typischer für einen 3D Shooter ist, mit menschlichen Gegnern. Ich verspreche Ihnen, dass auch der Spieler eine Waffe erhält, bleiben Sie also am Ball!” Das schrieb ich am Ende des Artikels vom letzten Monat, also könnten einige von Ihnen jetzt ein wenig frustriert sein. Die Wahrheit ist, dass ich das Labyrinth wirklich sehr liebgewonnen habe und ich wollte, dass Sie sehen, wie der Spieler von den Gegner durch die ganze Map gejagt wird.
Trotzdem halte ich meine Versprechen, also habe ich eine neue, simple Player Action namens “player2” ans Ende von ai4.wdl eingebaut. Öffnen Sie das Level, ändern Sie die Action des Spielers (player1) in die Neue (player2), um die Kamera so zu ändern, dass man einen Ego-Shooter vor sich hat. Mit WASD können Sie sich bewegen und mit der Maus drehen und schießen.
Was
kommt im nächsten Monat? Neuer Code und ein spielbares Level mit einer
großen Zahl von Knoten und Feinden, Monster, die warten bis Sie näher
kommen ehe sie angreifen und vieles mehr!
Stationäre
Waffen
Dieser Artikel zeigt Ihnen, wie Sie stationäre Waffen programmieren können: Kanonen, große Maschinengewehre, etc.
Der komplizierteste Code dafür steht hier:
action
sweapon
{
while (player == null) {wait (1);}
player._health = 100;
my.passable = on;
while (player._health > 0)
{
if (player_immobile == 0)
{
while (vec_dist (player.x, my.x) > 100) {wait (1);}
vec_set (players_speeds, strength);
vec_set (players_angles, astrength);
vec_set (strength, nullvector);
vec_set (astrength, nullvector);
player_immobile = 1; // can't move
while (key_any == 1) {wait (1);}
player.pan = 0;
player.tilt = 0;
}
Wir warten bis der Spieler existiert und setzen seine Gesundheit auf 100 (nur für den Fall, dass die Action des Spielers das vergißt). Die stationäre Waffe (eine Kanone, in meinem Beispiel) ist passierbar.
Die Action sweapon läuft, solange der Spieler eine Gesundheit größer als 0 hat; ich habe eine Variable namens “player_immobile” definiert, die auf 0 gesetzt wird, wenn der Spieler sich bewegen kann und auf 1, wenn er das nicht kann. Kann er sich bewegen, warten wir, bis er näher als 100 Quants and die Kanone gekommen ist und dann kopieren wir seine Geschwindigkeit in x, y und z Richtung, sowie seine Winkelgeschwindigkeiten in players_speed bzw. players_angles. Die nächsten Code Zeilen setzsn strength und astrength auf 0, so dass sich der Spieler vorerst nicht bewegen oder drehen kann. Player_immobile wird auf 1 gesetzt, wir warten, bis alle Tasten losgelassen wurden und setzen dann die Winkel für Pan und Tilt des Spielers zurück.
else // player_immobile == 1
{
player.x = -1250;
player.y = -728;
if ((key_cuu == 1) && (player.tilt < 30))
{
player.tilt += 2 * time;
}
if ((key_cud == 1) && (player.tilt > -10))
{
player.tilt -= 2 * time;
}
if ((key_cul == 1) && (player.pan < 30))
{
player.pan += 2 * time;
}
if ((key_cur == 1) && (player.pan > -30))
{
player.pan -= 2 * time;
}
my.pan = player.pan;
my.tilt = player.tilt;
Wenn der Spieler sich nicht mehr bewegen kann, setzen wir seine x und y Koordinaten auf die Werte, wie wir sie brauchen: ich habe mir diese zur Laufzeit mit Hilfe des Debugpanels geholt. Die “Pfeil Oben” Taste erhöht den Tilt, falls dieser unter 30 Grad ist, die “Pfeil Unten” Taste verringert ihn, sollte er größer sein als –10 Grad. Ebenso verfahren wir mit dem Pan, er darf Werte von –30 bis 30 annehmen und wird mit “Pfeil Links” bzw. “Pfeil Rechts” geändert.
Die letzten zwei Code Zeilen setzen die Winkel der Kanone auf die des Spielers.
if (key_ctrl == 1)
{
while (key_ctrl == 1) {wait (1);}
vec_for_vertex (rocket_coords, my, 190);
snd_play (fire_wav, 100, 0);
ent_create (rocket_mdl, rocket_coords, move_rocket);
}
if (key_space == 1)
{
vec_set (strength, players_speeds);
vec_set (astrength, players_angles);
player.pan = 0;
player.tilt = 0; // restore pan and tilt
player_immobile = 0; // can move
while (vec_dist (player.x, my.x) < 200) {wait (1);}
}
}
wait (1);
}
}
Wenn die “Strg” Taste gedrückt wurde, warten wir bis sie losgelassen wird (kein Autofeuer), ermitteln die Vertex Koordinaten der Rakete (Vertex #190 für mein Model der Kanone), lassen ein Geräusch ertönen und erzeugen eine Rakete, die sich mit Hilfe der move_rocket Funktion bewegt.
Wenn die Leertaste gedrückt wird, werden die ursprünglichen strength und astrength Werte wiederhergestellt, damit der Spieler sich wieder bewegen kann. Pan, Tilt und player_immobile werden auf 0 gesetzt, so dass die Action nun weiß, dass der Spieler sich wieder bewegen kann. Wir erlauben dem Spieler sich von der Kanone zu entfernen, wenn er sie nicht mehr benötigt.
Schauen wir uns nun die Funktion für die Rakete an.
function
move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my != null)
{
rocket_speed.x = 100;
rocket_speed *= time;
move_mode = ignore_you + ignore_passable;
ent_move (rocket_speed, nullvector);
wait (1);
}
}
Die Rakete reagiert auf Entities und Level Blocks; wenn sie mit einer Entity oder einem Block kollidiert, wird die Event Funktion remove_rocket aufgerufen. Die Rakete übernimmt ihren Pan und Tilt Winkel von der Kamera und ihre Geschwindigkeit wird durch rocket_speed.x angegeben. Sie bewegt sich mit Hilfe einer Schleife und ignoriert dabei die erzeugende Entity (die Kanone) und alle passierbaren Entities.
Sind Sie neugierig auf die Event Funktion? Hier kommt sie schon:
function
remove_rocket()
{
wait (1);
if (you != null)
{
you._health -= 150;
}
ent_create(explosion_pcx, my.pos, sprite_explosion);
my.event = null;
my.invisible = on;
wait (2);
ent_remove (me);
}
Falls die Rakete eine Entity trifft (you != null), werden 150 Gesundheitspunkte davon entfernt, ein Explosionssprite entsteht, die Eventfunktion wird auf null gesetzt (keine weiteren Events), die Rakete wird unsichtbar und wird nach 2 Frames entfernt. Die Funktion für das Explosionssprite ist hier:
function
sprite_explosion()
{
my.scale_x = 5;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 400);
while (my.frame < 7)
{
my.frame += 1 * time;
wait (1);
}
ent_remove (me);
}
Die
Größe des Sprites wird auf das Fünffache erhöht, das
könnte praktisch werden, wenn Sie es durch ein Model ersetzen wollen.
Die Explosion ist passierbar und hat die “flare” und “bright” Flags gesetzt,
mit einem Ambient Wert von 100. Wir hören ein Geräusch am Ort
des Auftreffens und die Frames der Explosion werden angezeigt; sobald die
Animation vorüber ist, wird das Sprite entfernt.