Perfekte KI – Teil 2

Im letzten Monat begann ich, den Code für eine perfekte KI zu schreiben, eine KI, die den kürzesten Weg von einem Anfangspunkt (dem Gegner) zum Zielpunkt (dem Spieler) findet. In diesem Monat werden Sie den Algorithmus in Aktion sehen. Öffnen Sie Level2.wmp, berechnen Sie die Map und starten sie:

Auf dem Bildschirm sieht man “Touched” (der letzte Knoten, der von der Maus berührt wurde), “Start” (der Startknoten) und “Target” (der Zielknoten). Bewegen Sie nun die Maus über Knoten 27 und klicken; Sie werden sehen, dass die Knoten, die den kürzesten Weg von dort zum Spieler ihre Farbe von weiß in rot ändern, wie im Bild:

Sie können den Spieler an eine beliebige Position bewegen, der Zielknoten wird derjenige sein, der dem Spieler am nächsten ist und diesen sehen kann. Sie können auf einen beliebigen Knoten im Level klicken und erhalten jedes Mal den kürzesten Pfad zum Spieler. Die Nummern der Knoten, die diesen Weg beschreiben erscheinen am oberen Rand des Bildschirms. Es leuchtet ein, dass der Gegner den Spieler jagen wird wie einen Hund, selbst wenn mein Beispiellevel ein kompliziert aussehender Irrgarten ist.

Ich habe schon oft gehört, dass eine gute KI schwer zu schreiben ist, weil die Spielgeschwindigkeit darunter zu leiden hat, etc. Um so besser ist die Nachricht, dass die perekte KI sehr schnell arbeitet, der kürzeste Weg ist in weniger als einem Frame berechnet. Ich habe einen Floyd – Dijkstra – George :) Algorithmus benutzt, weil er leicht zu implementieren ist und sehr schnell arbeitet.

Ich rate Ihnen, den ersten Teil des Artikels in AUM 27 zu lesen, ich werde hier nicht noch einmal erklären, was dort geschah:

function main()
{
mouse_range = 5000;
fps_max = 60;
clip_size = 0;
level_load(level2_wmb);
wait (3);
camera.z = 2400;
camera.tilt = -90;
init_mouse();
while (distances_computed == 0) {wait (1);}
run_algorithm();

Mit der Maus berühren wir die Knoten; bewegen Sie sie über einen beliebigen Knoten und Sie sehen seine Nummer auf dem Bildschirm. Die erste Codezeile stellt sicher, dass wir jeden Knoten auf diese Weise berühren können, selbst wenn er bis zu 5000 Quants entfernt ist. Dann begrenzen wir die Framerate auf 60 fps und sagen der Engine, dass alle Polygone dargestellt werden sollen; daraufhin wird der Level geladen und eine praktische Kameraposition und ein entsprechender Winkel werden eingestellt. Die Funktion init_mouse ist eine typische Mausfunktion, ich werde den Code gleich zeigen. Ich habe mich dafür entschieden, die Distanz der Knoten, die einander direkt sehen (nicht die kürzeste Entfernung zum Spieler oder den eigentlichen Pfad!) direkt am Spielstart zu berechnen, daher beginnt der Algorithmus nicht, bis distance_computed von einer anderen Funktion auf 1 gesetzt wurde.

while (1)
{
index %= max_nodes;
if ((node_to_player[index] < closest_distance) && (see_player[index] == 1))
{
closest_distance = node_to_player[index];
target_node = index;
}
else
{
closest_distance = node_to_player[target_node];
}
index += 1;
wait (1);
}
}

Der Rest des Codes prüft, welche der Knoten (0..29) am nächsten zum Spieler ist und diesen sehen kann. Dies wird der Zielknoten für unseren Gegner. Lesen Sie den Artikel in AUM 27 für eine ausführliche Erläuterung diesen Teils.

action player1
{
player = me;
while (1)
{
my.pan += 4 * (key_cul - key_cur) * time;
my.skill1 = 15 * (key_cuu - key_cud) * time;
if (key_cuu + key_cud > 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;
}
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}
}

Ich habe die Action für den Spieler nicht geändert, bis auf die Tatsache, dass ich seine Geschwindigkeit erhöht habe und die Variable player_speed entfernt habe (ich brauche nun einige Skills). Aber der Rest des Codes ist genau wie in AUM 27.

Lassen Sie mich die Arrays für diese Demo zeigen:

var node_to_player[30];
var see_player[30];
var nodex[30];
var nodey[30];
var node_to_node[900];
var visited[900];
var path[30];

Node_to_player wird die Distant jeden Knotens zum Spieler speichern; see_player wird auf 1 gesetzt, wenn der entsprechende Knoten Sichtkontakt zum Spieler hat und auf 0, wenn das nicht der Fall ist. NodeX und NodeY speichern die x und y Koordinaten der 30 Knoten, mit Hilfe dieser Werte navigiert der Gegner von einem Knoten zum nächsten. Node_to_node enthält den Abstand zwischen zwei beliebigen Knoten und funktioniert wie eine Matrix mit 30 x 30 Elementen. Visited speichert den Pfad von einem Knoten zu jedem anderen und Path enthält den eigentlichen Weg vom Start zum Ziel. Keine Angst, wir werden unterwegs oft mit Bildern arbeiten.

Los geht’s!

action node
{
my.enable_scan = on;
my.enable_touch = on;
my.enable_click = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;

Jeder Knoten reagiert auf Scan, Touch und Click Events. Wenn eines dieser Dinge geschieht, läuft die trace_back Funktion. Skill47 wird für später auf einen seltsamen Wert gesetzt und der Knoten wird passierbar gemacht:
number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (number_of_nodes < (max_nodes - 1)) {wait (1);}

Jeder Knoten braucht eine ID Nummer (0..29); die Knoten erscheinen in zufälliger Reihenfolge im Level, also ist number_of_nodes = 0 für den ersten Knoten, gleich 1 für den zweiten etc., da es zum Spielstart auf –1 steht. Jeder Knoten speichert seine ID in Skill48, die Koordinaten werden in den entsprechenden Arrays gesichert. Und dann wird gewartet, bis alle Knoten im Level plaziert sind:

while (current_node <= number_of_nodes)
{
if (current_node == my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
current_node += 1;
}
wait (1);
}
distances_computed = 1;

Der erste Knoten (0) scannt nach nahegelegenen Knoten; wenn welche gefunden werden, dann werden ihre Events ausgelöst. Dieser Scan findet in alle Richtungen (360 Grad rundum) statt und hat eine Reichweite von 1000 Quants. Wir warten einen Frame, dann wird der zweite Knoten dieselbe Prozedur ausführen, etc. Auf diese Weise laufen nicht 30 scan_entity Anweisungen zugleich, um die Framerate hoch zu halten. Sobald alle Knoten nach Nachbarn gescannt haben, setzen wir distances_computed auf 1 und erlauben so der run_algorithm Funktion zu starten. Darüber reden wir später.

while (player == null) {wait (1);}
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);
}
}

Wir warten bis der Spieler existiert, dann speichern wir die Distanz von jedem Knoten zum Spieler im Array node_to_player. Wenn der Spieler näher als 500 Quants am Knoten ist, wird dieser einen trace ausführen und setzt see_player auf 1, wenn der Spieler im Sichtbarkeitsbereich ist. Die Knoten machen dies 10 Mal in der Sekunde, das ist der einzige Teil der perfekten KI, die Computer Ressourcen braucht. Beachten Sie, dass nur die Knoten überprüft werden, die nahe am Spieler sind, wenn also zum Beispiel sechs Knoten nahe genug sind, brauchen wir theoretisch weniger als einen trace pro Frame und das ist sehr gut!

Der folgende Code wird für die Knoten benutzt und sieht nicht so kompliziert aus. Bevor ich fortfahre, zeige ich Ihnen, wie die Knoten zusammenarbeiten.

Knoten 1 scannt in einem Radius von 1000 Quants; die Knoten 2, 3, 4, 5, 6 und 7 werden davon erfaßt. Alle diese Knoten führen nun einen trace zurück zu Knoten 1 aus; wie Sie sehen können die Knoten 2 und 3 den Ursprungsknoten sehen, aber der Rest nicht. Dieser Prozeß wird für alle Knoten wiederholt.

Schauen wir uns die Event Funktion an:

function trace_back()
{
if (event_type == event_scan)
{
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);
}
}
if (event_type == event_touch)
{
touched_node = my.skill48;
}
if (event_type == event_click)
{
start_node = my.skill48;
}
}

Wenn ein Knoten gescannt wird, dann wird die “you” Entity in Skill45 gescannt. Welche ist das? Es ist der Knoten, der unseren Knoten gescannt hat und das Event auslöste (Knoten 1 im Beispiel oben). Jede Trace Anweisung überschreibt den “you” Zeiger, deshalb haben wir ihn in skill45 gesichert. Wenn unser Knoten den scannenden Knoten sehen kann, stellen wir “you” wieder her und speichern die Distanz zwischen “my” (der Knoten, der gescannt wurde) und “you” (der Knoten, der den Scan ausführte) in node_to_node. Ich benutze “index” als einen Index im Array, der für die Matrix verantwortlich ist.

Node_to_node hat 900 Elemente, Sie sollten das als Matrix mit 30 Zeilen und 30 Spalten sehen.

Gehen wir zurück zur Event Funktion, wenn der Knoten von der Maus berührt wurde, wird touched_node (einer der angezeigten Zahlen auf dem Bildschirm) auf den Wert gesetzt, den der Knoten in Skill40 hat, die Nummer des Knotens. Auf diese Weise können wir die Nummer eines Knotens ermitteln, indem wir ihn mit der Maus berühren. Schließlich, wenn der Knoten mit der linken Maustaste angeklickt wurde, wird start_node (der erste Knoten unseres Pfades) auf die Zahl des Knotens gesetzt.

Nun kommt eine leichte Funktion, die für die Mausbewegung verantwortlich ist:

function init_mouse()
{
mouse_map = pointer_pcx;
mouse_mode = 2;
while (1)
{
if (mouse_left == 1) {display_path();}
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}

Die einzige Sache, die erwähnt werden sollte, ist, dass ein Linksklick auch die Funktion display_path aufruft, die den kürszesten Pfad zwischen start_node und target_node anzeigt.

Ok, halten wir einen Moment inne. Wir haben Knoten, die einander scannen und zurücktracen... es sieht aus, als hätten wir eine Menge Daten, aber wie sehen diese aus? Das ist eine sehr gute Frage!

So sieht node_to_node für die ersten 10 Knoten aus; Knoten 0 und Knoten 1 können einander sehen und sie haben einen Abstand von 384 Quants, Knoten 2 und Knoten 4 können einander nicht sehen (die Distanz ist auf 0 vorinitialisiert), etc. Die Knoten in der Demo haben andere Zahlen, sehen Sie diese Tabelle als ein Beispiel an.

Nun zu der schrecklichsten Funktion, die Sie je gesehen haben:

function run_algorithm()
{
i = 0;
j = 0;
while (i < max_nodes)
{
while (j < max_nodes)
{
index = j + i * max_nodes;
if (node_to_node[index] == 0)
{
node_to_node[index] = 999999;
}
j += 1;
}
i += 1;
j = 0;
}

Ich habe mir diesen ersten Teil angesehen und der sieht noch sehr harmlos aus. Ich habe einige Variablen mit kurzen Namen benutzt (i, j, k, l), um den Code lesbarer zu halten. Der erste Teil oben belegt alle Elemente des Arrays, die auf 0 stehen neu mit 999999. Erinnern Sie sich an die Tabelle? Wenn zwei Knoten einander nicht sehen konnten, hatten sie den Abstand 0. Wir möchten aber den kürzesten Weg zwischen zwei Knoten berechnen, wenn sich zwei also nicht sehen können, sollten wir eine große Distanz zwischen ihnen setzen! Die Tabelle sieht nun folgendermaßen aus:

Gut, wenn ein Knoten also einen anderen sehen kann, steht die Distanz zwischen diesen in der Tabelle, wenn er ihn nicht sehen kann, ist die Distanz 999999. Nun ist es Zeit, daraus den Abstand zwischen zwei beliebigen Knoten zu berechnen, selbst wenn sie sich sehen können:

i = 0;
j = 0;
k = 0;
while (i < max_nodes)
{
while (j < max_nodes)
{
while (k < max_nodes)
{
if (node_to_node[j + i * max_nodes] + node_to_node[i + k * max_nodes]) < (node_to_node[j + k * max_nodes])
{
node_to_node[j + k * max_nodes] = node_to_node[j + i * max_nodes] + node_to_node[i + k * max_nodes];
visited[j + k * max_nodes] = i;
}

Ich werde ein Zahlenbeispiel verwenden, um Ihnen die Sache zu vereinfachen. Wir gehen alle Knoten durch und wenn “distance(2, 10) + distance(10, 15) < distance(2,15)”, dann ist 10 ein Knoten auf einem kürzeren Weg, daher nehmen wir die Änderung vor, nicht mehr die alte, lange Route zu verwenden. Außerdem speichern wir den neuen Knoten (10, in dem Beispiel).

Sehen Sie sich das Bild oben an: Knoten 2 wußte, dass der kürzeste Pfad zu Knoten 15 über die Knoten 4, 5, 6, 7, 8, 9 und 11 führte, hat aber nun erfahren, dass es einen kürzeren Weg über Knoten 10 gibt. Dieser Prozeß wiederholt sich für alle Knoten.

if (j == k)
{
node_to_node[j + k * max_nodes] = 0;
visited[j + k * max_nodes] = 0;
}
k += 1;
}
k = 0;
j += 1;
}
k = 0;
j = 0;
i += 1;
}
}

Falls j = k ist, setzen wir die Distanzen distance(0,0) .. distance(29,29) auf 0. Ein Knoten sollte von sich selbst schließlich den Abstand 0 haben, nicht wahr? Wir machen das gleiche mit dem besuchten Knoten. Diese Variablen werden erhöht, initialisiert, etc., stellen Sie also sicher, dass die drei verschachtelten Schleifen alle Elemente von node_to_node besuchen. Möchten Sie nun die neue node_to_node Tabelle sehen?

Das ist eine schöne Tabelle! Ich kann den kürzesten Abstand zwischen je zwei Knoten sehen, selbst wenn diese einander nicht sehen können! Der Abstand zwischen Knoten 2 und Knoten 6 ist 768 Quants usw. Nun, da wir den eigentlichen Weg berechnen müssen, sind wir bereit dazu! Erinnern Sie sich an die Funktion für die Maus?

Wenn die linke Maustaste gedrückt wird, läuft die Funktion display_path:

function display_path()
{
proc_kill(4);
while (mouse_left == 1) {wait (1);}
k = 0;
while (k < 30)
{
path[k] = 0;
k += 1;
}

Nur eine Instanz dieser Funktion sollte laufen; wir warten, bis die Maustaste losgelassen wird und setzen dann das Array auf 0 zurück, welches den alten Pfad enthält, das mit Namen Path. Dies tun wir, weil ein eventueller alter, längerer Pfad darin stehen könnte:

k = 0;
path[k] = target_node;
find_path(start_node, target_node);
sleep (1);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = 50;
ent_create (marker_mdl, temp, dynamic_light);
i -= 1;
sleep (0.5); // every 0.5 seconds
}
}

Wir schreiben das erste Element des Pfades (target_node) und starten die Funktion, die den kürzesten Pfad zwischen start_node und target_node finden soll. Diese Funktion arbeitet sehr schnell, Sie brauchen also keine Sekunde zu warten; wait(1) wäre auch in Ordnung. Ich habe eine volle Sekunde Wartezeit gewählt, weil ich einige dynamische Lichter benutze und möchte, dass sie in ihrer ganzen Pracht gewürdigt werden können. Die Variable k speichert die Anzahl der Knoten auf dem kürzesten Pfad, wir werden gleich sehen wie. Solange noch nicht alle Lichter, die den Pfad beschreiben erstellt haben, holen wir uns die Koordinaten des entsprechenden Knotens aus NodeX und NodeY und erzeugen unsichtbare Entities, die rotes Licht abstrahlen 50 Quants über dem Boden. Auf diese Weise leuchten die Knoten rot, ohne dass wir direkt mit ihnen arbeiten müssen.

Diese Lichter werden alle 0,5 Sekunden erzeugt und ihre Funktion hat den folgenden Code:

function dynamic_light()
{
my.invisible = on;
my.passable = on;
my.lightrange = 100;
my.lightred = 255;
my.lightgreen = 0;
my.lightblue = 0;
sleep (3);
ent_remove(me);
}

Die roten Lichter benutzen das gleiche Model wie die Knoten, aber da sie unsichtbar und passierbar sind, wird es niemand bemerken. Die Lichter existieren für 3 Sekunden und werden dann entfernt. Auf diese Weise können 6 – 7 Lichter zugleich den Pfad beleuchten.

Schon müde? Ich auch. Es bleibt nur eine Funktion zu diskutieren:

function find_path(l, j)
{
i = 0;
while (i < max_nodes)
{
if ((i != j) && (visited[i + max_nodes * j] == 0))
{
if (node_to_node[l + max_nodes * j] == node_to_node[l + max_nodes * i] + node_to_node[i + max_nodes * j])
{
k += 1;
path[k] = i;
next_node = i;
i = max_nodes - 1;
if (path[k] == l)
{
return;
}
}
}
i += 1;
}
j = next_node;
find_path(l, j);
}

Diese schreibt den eigentlichen Pfad in das “path” Array. Schauen wir uns an, wie die Funktion display_path sie aufrief:

find_path(start_node, target_node);

Das heißt, wenn ich find_path(14,5) aufrufe, wird die Funktion die Knoten ausfindig machen, die den kürzesten Weg zwischen Knoten 14 und Knoten 5 beschreiben, zum Beispiel: 14, 2, 7, 19, 5. Schauen wir uns an, wie das geschieht.

Die Variable i beginnt bei 0 und wird diesen Knoten und die anderen 29 untersuchen. Wenn der aktuelle Knoten einen der anderen sehen kann (außer sich selbst, i != j) und dies ein Knoten auf dem kürzesten Pfad ist, dann haben wir ein neues Element des Pfades gefunden; wir erhöhen k und speichern die neue Knotennummer im Path Array. Wir kopieren den neuen Knoten auf den kürzesten Pfad in next_node, da der Wert gleich verloren geht; dann setzen wir i auf 29, um aus der Schleife zu entkommen, da wir andere Pfade der gleichen Länge nicht beachten wollen:

Falls path[k] == l ist haben wir den Endpunkt erreicht, also verlassen wir die Funktion, ansonsten setzen wir den nächsten Knoten auf dem Pfad als Ziel (j = next_node) und rufen die Funktion erneut auf (so funktionieren rekursive Funktionen). Falls die Funktion eben nach einem Pfad zwischen den Knoten 2 und 10 gesucht hat, sucht es nun nach einem Pfad zwischen 2 und 7, falls Knoten 7 der nächste Knoten auf dem kürzesten Weg ist. Der Prozeß geht so lange weiter, wie es nötig ist und am Ende steht der gesamte Pfad im Array namens path.

Das war alles! Nehmen Sie es nicht schwer, wenn Sie den ganzen Code nicht sofort verstehen, eine gute KI ist schließlich recht schwer zu programmieren. Der letzte Artikel der Serie wird zeigen, wie man einen spielbaren Level erstellt, ohne auch nur eine Zeile Code schreiben zu müssen!

Perfekte KI wurde mit folgenden Zielen geschrieben:
- Sehr leichte Handhabung für den Level Designer
- Simple Benutzung für den Programmierer.
Die KI ist fertig, nächsten Monat werden wir also ein spielbares Level haben. Bis dahin! Was sagen Sie? Sie haben einen Bug in meinem Code gefunden?

Ah, ich verstehe... Sie haben also Knoten 1 angeklickt und anstatt 1 – 2 – 3 – 4 – 5 auf dem Schirm zu sehen, ergab sich nur 1 – 3 – 4 – 5! Das ist kein Bug, denn Knoten 1 kann Knoten 3 direkt sehen, also wird Knoten 2 nicht für den kürzesten Pfad benötigt. Wenn Sie mehr Punkte auf Ihren Pfaden möchten (wer würde das wollen?), verringern Sie die Reichweite der Scans und plazieren Sie mehr Knoten in Ihren Levels.

Ein letzter Rat: testen Sie Ihre Pfade! Ich mußte einige Knoten meines Demo Levels ein wenig verschieben, da sie einander direkt sehen konnten, wie im Bild unten:

Diese kleinen Änderungen ändern nichts am Gameplay, also seien Sie nicht schüchtern und modifizieren Sie Ihre Levels bei Bedarf auf dieselbe Weise.

Hitzesuchende Raketen

In diesem Monat habe ich mir ein kleines Porjekt ausgedacht, dass Ihnen zeigen soll, wie man hitzesuchende Raketen programmiert.

Sehen wir uns zunächst die Main Funktion an:

function main()
{
on_d = null;
level_load (heatseek_wmb);
}

Hier geschieht nichts besonderes, ich habe die Debug-Taste deaktiviert (“D” für A5), weil ich die WASD Tasten für die Spielersteuerung brauche.

action player_moves
{
player = me;
camera.pan = player.pan;
my.invisible = on;
while (1)
{
vec_set (camera.pos, my.pos);
camera.tilt += 20 * mouse_force.y * time;
camera.pan -= 20 * mouse_force.x * time;
my.pan = camera.pan;
my.tilt = camera.tilt;
player_speed.x = 15 * (key_w - key_s) * time;
player_speed.y = 10 * (key_a - key_d) * time;
vec_set (temp, my.x);
temp.z -= 1000;
trace_mode = ignore_me + use_box;
player_speed.z = -trace (my.x, temp);
move_mode = ignore_you + ignore_passable;
ent_move(player_speed, nullvector);
if (mouse_left == 1)
{
fire_rocket();
}
wait (1);
}
}

Der Spieler ist unsichtbar, weil wir eine Kamera mit Ego-Perspektive benutzen. Die Kamera wird mit der Maus rotiert und die Tasten “W”, “S”, “A” und “D” steuern den Spieler. Mit trace sorgen wir dafür, dass seine Füße die ganze Zeit am Boden bleiben. Die linke Maustaste ruft die Funktion fire_rocket auf.

function fire_rocket()
{
proc_kill(4); // only one instance of this function is running
while (mouse_left == 1) {wait (1);}
ent_create (rocket_mdl, player.pos, shoot_rocket);
snd_play (rocket_wav, 50, 0);
}

Nur eine Instanz der Funktion sollte laufen; wir warten, bis die Maustaste losgeladden wird, erzeugen eine Rakete und lassen ein Geräusch ertönen. Die Rakete startet die Funktion shoot_rocket:

function shoot_rocket()
{
wait (1);
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
my.skill20 = 0;
my.skill1 = 10;
my.skill2 = 0;
my.skill3 = 0;
my.skill1 *= time;
my.skill10 = 0;

Die Rakete reagiert auf Entities und Level Blocks; das aufgerufene Event ist remove_rocket. Die Rakete hat denselben pan und tilt Winkel wie die Kamera, wenn sie erzeugt wird. Die Skills 1 – 3 werden als ein Vektor benutzt, der die Geschwindigkeit der Rakete angibt.

while (my.skill20 < 500)
{
temp.x = 40; // horizontal scanning angle
temp.y = 60; // vertical scanning angle
temp.z = 500; // scanning range
if ((my.skill10 == 0) && (vec_dist (my.x, player.x) > 100))
{
scan_entity (my.x, temp);
}
my.skill20 += 1 * time;
ent_move (my.skill1, nullvector);
temp.x = my.x - 6 * cos(my.pan);
temp.y = my.y - 6 * sin(my.pan);
temp.z = my.z;
effect (particle_trail, 10, temp, normal);
wait (1);
}
remove_rocket();
}

Solange die Rakete existiert (skill20 < 500), setzen wir Winkel und Reichweite für einen Scan: 40 Grad horizontal, 60 Grad vertikal und 500 Quants Reichweite. Wenn die Rakete sich 100 Quants vom Spieler entfernt hat, beginnt sie mit dem Scannen und sucht nach Entities, die ihr enable_scan Flag gesetzt haben. Dabei fliegt sie die ganze Zeit weiter und wir sorgen noch für Rauchpartikel 6 Quants hinter dem Raketenmodel.

function particle_trail()
{
temp.x = random(2) - 1;
temp.y = random(2) - 1;
temp.z = random(2) - 1;
vec_rotate (temp, my.pan);
vec_normalize (temp, 3);
vec_add (my.vel_x, temp);
my.alpha = 40 + random(30);
my.bmap = ptrail_map;
my.size = random(2) + 5;
my.flare = on;
my.bright = on;
// my.beam = on;
my.move = on;
my.lifespan = 20;
my.function = fade_trail;
}

function fade_trail()
{
my.alpha -= 10 * time;
if (my.alpha < 0) {my.lifespan = 0;}
}

Das ist eine typische Partikelfunktion, zufällige Geschwindigkeit in alle Richtungen, variable Transparenz (alpha) und Größe und eine simple Funktion, die ältere Partikel verblassen läßt. Versuchen Sie my.beam = on, wenn Sie A5 oder A6 Pro besitzen, das läßt die Rauchpartikel noch interessanter aussehen.

Schauen wir uns an, was geschieht, wenn die Rakete auftrifft:

function remove_rocket()
{
wait (1);
my.event = null;
ent_playsound (my, destroyed_wav, 1000); // play the explosion sound
my.invisible = on; // hide the rocket, keep ent_playsound playing
sleep (1.5); // wait for 1.5 seconds
ent_remove(me); // now remove it
}

Die Event Funktion wird auf null gesetzt, damit die Rakete nicht auf weitere Events reagieren kann und ein Explosionsgeräusch ertönt. Wir verbergen die Rakete für 1,5 Sekunden, weil wir wollen, dass das Geräusch weiterläuft und entfernen die Rakete dann.

Zeit, sich um die Gegner zu kümmern!

action enemy
{
my.enable_scan = on;
my.event = enemy_event;
}

Alle Gegner müssen ihr enable_scan Flag gesetzt haben. Sobald die Rakete einen von ihnen scannt, wird die Funktion enemy_event starten:

function enemy_event()
{
if (event_type == event_scan)
{
you.skill10 = 1; // stop scanning
while (you != null) // as long as the rocket exists
{
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp.x);
wait (1);
}
}
}

You.skill10 = 1 sorgt dafür, dass die Rakete aufhört zu scannen, um keine Ressourcen zu vergeuden; sehen Sie sich die Funktion shoot_rocket nochmal an, wenn Sie sich nicht erinnern. Solange die Rakete existiert (you != null), dreht sie sich in Richtung Gegner. Meine Gegner stehen still, aber selbst wenn sie sich fortbewegen würden, hätten sie keine Chance, falls die Rakete sich schneller bewegt als sie es tun.