Perfekte KI - Teil III

In diesem Monat werden wir Zeuge, wie der Spieler in Panik ausbricht, weil er von einer Drohne attackiert wird, die den Code für die perfekte KI nutzt, den wir bislang besprochen haben. Es kommen viele neue Dinge dran, aber keine Sorge, der schwere Teil ist vorbei.

Ihre Aufgabe ist leicht: rennen Sie um Ihr Leben! Wenn die Drohne Sie trifft, sterben Sie. Wenn die Drohne Sie einfach sieht, schießt sie mit Raketen auf Sie, die jeweils 10 Punkte Ihrer Gesundheit abziehen. Die Drohne wird Sie bis zum bitteren Ende verfolgen, aber Sie können schneller laufen, also sollte es Ihnen gelingen, eine Weile am Leben zu bleiben. :)

Diese Serie begann vor zwei Monaten und daher ermuntere ich Sie hiermit, die Artikel aus AUM 27 und AUM 28 zu lesen, bevor Sie hier weitermachen. Dieser Artikel beschreibt nur solche Funktionen und Actions, die neu sind oder verändert wurden. Die Skriptdatei – ai3.wdl – ist in zwei Teile unterteilt, der neue Code für die Demo in diesem Monat steht unter diesen Zeilen aus ai3.wdl:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// new stuff for the 3rd Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Schauen wir uns den neuen Code an:

var dist_to_drone[30];
var drone_start[12] = 830, 700, 100, -830, 700, 100, 830, -700, 100, -830, -700, 100;

Das erste Array speichert die Distanz der Drohne zu den Knoten, die sie umgeben (oder einfach Null, wenn die Knoten sie nicht sehen). Drone_start enthält die Startkoordinaten für das Model der Drohne.

Ich habe mir vier mögliche Startpositionen für die Drohne überlegt und drone_start[12] enthält eben diese Koordinaten. Die Drohne sicht sich bei jedem Spielstart einen zufälligen Startpunkt aus.

text ai_txt // displays the texts on the screen
{
font = system_font;
pos_x = 0;
pos_y = 0;
string = "Current path: \n\nHealth:\nTarget:\nStart:";
flags = visible;
}

panel ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 24, 2, system_font, 1, player.health;
digits = 80, 37, 2, system_font, 1, target_node;
digits = 80, 50, 2, system_font, 1, start_node;
digits = 120, 0, 4, system_font, 1, path[0];
digits = 150, 0, 4, system_font, 1, path[1];
digits = 180, 0, 4, system_font, 1, path[2];
digits = 210, 0, 4, system_font, 1, path[3];
digits = 240, 0, 4, system_font, 1, path[4];
digits = 270, 0, 4, system_font, 1, path[5];
digits = 300, 0, 4, system_font, 1, path[6];
digits = 330, 0, 4, system_font, 1, path[7];
digits = 360, 0, 4, system_font, 1, path[8];
digits = 390, 0, 4, system_font, 1, path[9];
digits = 420, 0, 4, system_font, 1, path[10];
digits = 450, 0, 4, system_font, 1, path[11];
digits = 480, 0, 4, system_font, 1, path[12];
digits = 510, 0, 4, system_font, 1, path[13];
digits = 540, 0, 4, system_font, 1, path[14];
flags = overlay, refresh, visible;
}

Mit Hilfe eines Textes und eines Panels zeigen wir den aktuellen Pfad von der Drohne zum Spieler an, ebenso wie start_node, target_node und die Gesundheit des Spielers.

Die Action für die Knoten hat sich etwas geändert, also schauen wir sie nochmals an:

action node
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x; // store x and y
nodey[my.skill48] = my.y; // for this node
while (number_of_nodes < (max_nodes - 1)) {wait (1);}

Ich entschied mich, die Knoten in der Demo nicht anzuzeigen, aber Sie können Sie sichtbar machen, indem Sie die erste Zeile auskommentieren. Die Knoten reagieren auf Scan Events; werden Sie gescannt, läuft die Funktion trace_back(). Wir setzen skill47 auf einen ungewöhnlichen Wert (1234), um sicherzugehen, dass wir eine Entity als Knoten identifizieren können und setzen die Knoten auf passierbar, weil sie weder den Spieler, noch die Drohne behindern sollten. Jeder Knoten erhält eine einzigartige ID Zahl (0..29), die in Skill48 gesichert wird. Ebenso werden sie x und y Koordinaten jeden Knotens in nodex bzw. nodey gesichert. Schließlich warten wir ab, bis alle Knoten im Level plaziert wurden.

while (current_node <= number_of_nodes)
{
if (current_node == my.skill48)
{
temp.x = 360; // horizontal scanning angle
temp.y = 30; // vertical scanning angle
temp.z = 1000; // scanning range
scan_entity (my.x, temp); // scan the nodes nearby
wait (1);
current_node += 1; // move to the next node
}
wait (1);
}
distances_computed = 1;

Jeder Knoten scannt alle anderen Knoten, die näher als 1000 Quants sind. Wenn alle Knoten ihre Nachbarn ermittelt haben, wird die Variable distances_computed auf 1 gesetzt.

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 ab, bis der Spieler existiert und speichern dann die Distanzen von ihm zu jedem Knoten in dem Array mit Namen node_to_player. Wenn der Spieler näher als 500 Quants an einen Knoten kommt, führen wir einen trace vom Knoten zu ihm durch und wenn der Knoten ihn “sehen” kann, setzt er see_player auf 1, andernfalls auf 0. Jeder Knoten tut dies zehn Mal in der Sekunde.

Nun ist es Zeit sich die Event Funktion der Knoten anzuschauen:

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); // restore the "you" pointer
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
else // scanned by the drone (skill47 = 5678)
{
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);
dist_to_drone[my.skill48] = vec_dist (my.x, you.x);
if ((dist_to_drone[my.skill48] < min_drone) && (dist_to_drone[my.skill48] != 0))
{
min_drone = dist_to_drone[my.skill48];
}
wait (2);
if (dist_to_drone[my.skill48] == min_drone) // this is the closest node to the drone?
{
start_node = my.skill48;
}
}
else
{
dist_to_drone[my.skill48] = 0;
}
}
}
}

Wird der Knoten von einer anderen Entity gescannt, gibt es zwei Möglichkeiten:
1) Der Knoten wurde von einem anderen Knoten gescannt (skill47 = 1234). Wenn dies der Fall ist, sichern wir den Zeiger auf die scannende Entity (you) und tracen dann in diese Richtung. Wenn der Knoten die scannende Entity sehen kann, speichern wir den Abstand in node_to_node.
2) Der Knoten wurde von der Drohne gescannt (skill47 = 5678). In diesem Fall speichern wir auch den “you” Zeiger und tracen in diese Richtung. Wenn die Drohne vom Knoten aus gesehen wird, speichern wir den Abstand in dist_to_drone. Die Variable mit Namen min_drone wird den Abstand vom nächsten Knoten zur Drohne speichern; start_node ist der Knoten am nächsten zur Drohne, falls ihr dist_to_drone Wert gleich ist zu min_drone.

Wie Sie sehen hat die Drohne 5 Knoten gescannt, aber nur Knoten 2 und Knoten 4 können sie sehen; start_node wird auf 2 gesetzt, weil Knoten 2 am nächsten zur Drohne ist und diese sehen kann.

Der Code für den Spieler hat sich ebenfalls geändert:

action player1
{
player = me;
my.health = 100;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health; // I loose health when I collide with entities

Der Spieler hat eine Gesundheit von 100 (health = skill40) und reagiert auf andere Entities (die Drohne und ihre Raketen). Das Event, das ihm zugeordnet ist, heißt decrease_health:

while (my.health > 0)
{
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); // play "walk" frames animation
my.skill46 += 10 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else // the player is standing
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}

Solange der Spieler lebt, kann er sich mit Hilfe der Pfeiltasten bewegen und drehen. Wenn der Spieler läuft, spielt er seine “walk” Animation, ansonsten eine Animation “stand”. Bei der Bewegung ignoriert er die passierbaren Entities (die Knoten) und gleitet an Wänden entlang, wenn er mit ihnen kollidiert.

my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46); // play "death" animation
my.skill46 += 2 * time;
wait (1);
}
}

Wenn der Spieler stirbt, läuft seine “death” Animation. Schauen wir uns nun den Code für die Drohne an:

starter drone
{
while (total_frames < 5) {wait (1);}
ent_create (drone_mdl, nullvector, init_drone);
}

Die Drohne wird von einer “starter” Funktion erstellt, die 5 Frames wartet und dann die Drohne in das Level stellt, zusammen mit der Funktion init_drone:

function init_drone()
{
while (player == null) {wait (1);}
var corner;
my.skill47 = 5678;
corner = sys_seconds % 4;
if (corner == 0)
{
my.x = drone_start[0];
my.y = drone_start[1];
my.z = drone_start[2];
}
if (corner == 1)
{
my.x = drone_start[3];
my.y = drone_start[4];
my.z = drone_start[5];
}
if (corner == 2)
{
my.x = drone_start[6];
my.y = drone_start[7];
my.z = drone_start[8];
}
if (corner == 3)
{
my.x = drone_start[9];
my.y = drone_start[10];
my.z = drone_start[11];
}

Die Funktion wartet bis es den Spieler gibt und erstellt dann eine lokale Variable namens corner. Wir setzen skill47 auf 5678 als eine einzigartige Kennung und erzeugen dann eine Zufallszahl von 0 bis 3 mit Hilfe von sys_seconds. Aufgrund dieser Zahl wählen wir eine der vier Startpositionen für die Drohne aus.

while (player.health > 0)
{
my.lightrange = 400; // emit light
my.lightred = 0;
my.lightgreen = 255; // green light
my.lightblue = 0;
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
shoot_rocket();
sleep (1);
}

Solange der Spieler lebt, sendet die Drohne ein grünes Licht mit einem Durchmesser von 400 Quants aus. Wenn die Drohne den Spieler sehen kann (trace = 0), feuert sie pro Sekunde eine Rakete auf ihn.

else
{
min_drone = 999999;
temp.x = 360; // horizontal scanning angle
temp.y = 360; // vertical scanning angle
temp.z = 800; // scanning range
scan_entity (my.x, temp);
sleep (1);

k = 0;
while (k < max_nodes)
{
path[k] = 0; // reset the array, get rid of the previously stored path
k += 1;
}
k = 0; // start with the first element of path[30]
path[k] = target_node; // write the first path element

Kann die Drohne den Spieler nicht sehen, wird min_drone auf eine große Zahl gesetzt und die Drohne beginnt in einer Reichweite von 800 Quants zu scannen, um den zu ihr am nächsten Knoten zu bestimmen. Wir setzen den Array “path” zurück (der den Pfad enthält, den die Drohne nehmen soll) und schreiben als erstes Element des neuen Pfades die target_node.

snd_play (getpath_wav, 70, 0);
if (start_node != target_node)
{
find_path(start_node, target_node);
wait (2);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = my.z; // use drone's height for z
d1_marker = ent_create (marker_mdl, temp, follow_path); // create an invisible entity that guides the drone
wait (1);
while (vec_dist (d1_marker.x, my.x) > 10)
{
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}
i -= 1;
wait (1);
}
}
}
}
}
Ein Sound ertönt, der einen neu gesuchten Pfad ankündigt und falls dann start_node und target_node verschieden sind, starten wir die find_path Funktion aus AUM 28, die rekursiv den kürzesten Pfad zwischen zwei Knoten findet, die ihr als Parameter übergeben werden. Das Ergebnis der Funktion wird im Array “path” gespeichert, wir haben im vorigen Abschnitt darüber gesprochen.

Wir warten 2 Frames und dann bewegen wir die Drohne von einem Pfadpunkt zum Nächsten, indem eine Entity namens d1_marker über jedem Knoten entlang des kürzesten Pfades erstellt wird und die Drohne dann in Richtung dieser Entity bewegt wird, bis der Abstand zu ihr kleiner ist als 10 Quants. Dann erstellen wir d1_marker auf dem nächsten Knoten des Pfades und bewegen die Drohne wieder dorthin. Dies wiederholt sich, bis die Drohne den letzten Knoten im Array path erreicht hat.

Die Entity namens d1_marker hat diese simple Funktion:

function follow_path()
{
my.invisible = on;
my.passable = on;
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp);
while (d1_marker == me) {wait (1);}
ent_remove (me);
}

Sie macht ihn unsichtbar, passierbar und richtet die Drohne auf ihn aus. Die älteren, unbenutzten Marker werden entfernt, wenn neue erstellt werden.

Schauen wir uns nun die Funktionen für die Raketen an:

function shoot_rocket()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn the drone towards the player
if (player.health > 0)
{
ent_create (rocket_mdl, my.pos, move_rocket); // create a rocket
}
}

Erinnern Sie sich daran, dass die Funktion shoot_rocket() abläuft, wenn die Drohne den Spieler sehen kann. Diese Funktion richtet die Drohne auf den Spieler aus und erzeugt Raketen, sofern er lebt, die mit der folgenden Funktion ablaufen:

function move_rocket()
{
my.pan = you.pan;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_rocket;
while (my != null)
{
my.skill1 = 15 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable; // ignores the drone (its creator = you)
ent_move(my.skill1, nullvector);
wait (1);
}
}

Die Raketen und die erzeugende Entity (die Drohne) haben den gleichen pan Wert und reagieren auf Zusammenstöße mit anderen Entities oder Level Blocks. Die Rakete bewegt sich mit der Geschwindigkeit, die durch ihren skill1 vorgegeben ist und ignoriert die Drohne (you) bei der Bewegung. Wenn sie auf etwas trifft, läuft die Funktion remove_rocket():

function remove_rocket()
{
snd_play (hit_wav, 100, 0);
wait (1);
ent_remove (me);
}

Diese Funktion ist leicht, ich überlasse es Ihnen, sie zu analysieren. Schauen wir uns nun die letzte Funktion an:

function decrease_health()
{
if (you.skill47 == 5678) // collided with the drone?
{
my.health = 0; // instant death
}
else // hit by a rocket?
{
my.health -= 10; // loose 10 health points
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav, 40, 0);
}
}

Dies ist die Eventfunktion des Spielers. Wenn er mit der Drohne kollidiert (skill47 = 5678), wird er sofort sterben, ansonsten hat eine Rakete ihn getroffen und er verliert 10 Punkte seiner Gesundheit. Wenn diese auf 0 sinkt, wird er nicht mehr auf Events reagieren und das “Tod” Geräusch ertönt.

Dies war die erste spielbare Demo, die den Kern der Perfekten KI nutzt. Die Drohne sucht den Spieler und wird ihn jedes Mal finden; der einzige Nachteil ist, dass die Schleife läuft, bis alle Pfadpunkte abgelaufen sind, wenn der Spieler also schnell fortläuft, kann die Drohne ihn erst wieder verfolgen, wenn sie den letzten Knoten auf ihrem Weg erreicht hat. Ich habe die Drohne auf diese Weise programmiert, damit Sie gut erkennen, wie die Pfade sich ändern; natürlich kann man diese Beschränkung leicht überwinden, indem eine neue trace Anweisung in die Schleife eingefügt wird, die die Drohne bewegt.

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!

Oh und geben Sie nicht mir die Schuld, wenn der Spieler im Zuge seiner “death” Animation die Wand durchdringt:

Das liegt an der Animation des Models und hat nichts mit meinem Code zu tun.

“Lazer” Augen

Dieser Artikel wird zeigen, wie man Strahlen aus Partikeln erstellt und an die Augen Ihrer Models heftet.

Ich weiß, dass ich diesem Typen nicht in einer dunklen Gasse begegnen wollte... ich meine natürlich den kleinen roten Typen. :)

Das Model spielt seine “stand” Animation ab und die zwei Laserstrahlen aus seinen Augen bewegen sich mit, wenn es seinen Kopf bewegt.

action night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // play "stand" frames animation
my.skill1 += 0.4 * time; // animation speed
my.skill1 %= 100; // loop animation
vec_for_vertex (left_eye, my, 235); // get the vertex coords for the left eye
vec_for_vertex (right_eye, my, 247); // get the vertex coords for the right eye
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
while (ray_size > 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}
wait (1);
}
}

Die “stand” Animation wird in einer Schleife abgespielt. Wir ermitteln die Vertex Koordinaten des linken Auges (Vertex #235) und speichern diese in left_eye; ebenso für das rechte Auge (Vertex #247) und right_eye. Dann erstellen wir zwei flare Effekte mit left_eye und right_eye als Startpositionen. Alle, die nicht mehr als ein Paar glühender Augen in der Dunkelheit brauchen, sollten die folgende Schleife auskommentieren.

action night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // play "stand" frames animation
my.skill1 += 0.4 * time; // animation speed
my.skill1 %= 100; // loop animation
vec_for_vertex (left_eye, my, 235); // get the vertex coords for the left eye
vec_for_vertex (right_eye, my, 247); // get the vertex coords for the right eye
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
// while (ray_size > 0)
// {
// vec_for_normal (temp_left, my, 235);
// vec_for_normal (temp_right, my, 247);
// vec_normalize (temp_left, ray_size);
// vec_normalize (temp_right, ray_size);
// vec_add (left_eye, temp_left);
// vec_add (right_eye, temp_right);
// effect (attach_rays, 1, left_eye, normal);
// effect (attach_rays, 1, right_eye, normal);
// ray_size -= 0.01;
// }
wait (1);
}
}

Und nun, für diejenigen, die den vollen Effekt wollen, schauen wir uns die Schleife nochmal an:

while (ray_size > 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}

Wir ermitteln die Richtungen der Normalen für die Flächen, die das linke und das rechte Auge umgeben, setzen ihre Länge auf ray_size und addieren left_eye und right_eye hinzu und erstellen so Partikel solange ray_size größer ist als 0. Schauen wir uns ein Bild an, das zeigt, was geschieht:

In diesem Beispiel kommen weniger Partikel vor, um es leichter zu machen; das Erste wird bei ray_size = 2 erstellt, das zweite nach der ersten Subtraktion in der Schleife (ray_size = 1.99) und so weiter. Dies endet erst, wenn ray_size den Wert 0 erreicht hat.

Schauen wir uns nun die Funktion der Partikel an:

function attach_rays()
{
my.bmap = flare_pcx;
my.flare = on;
my.bright = on;
my.size = ray_width;
my.function = remove_flares;
ray_width -= 0.01;
}

function remove_flares()
{
my.lifespan = 0; // remove the flare particle
}

Diese leben nur für einen Frame; die einzige Sache, die erwähnt werden sollte, ist dass ihre Größe variabel ist; die ersten Partikel (die weit weg vom Auge sind), sind die größten, wohingegen die später plazierten, die näher an den Augen sind, kleiner und kleiner werden. Dieser Effekt wird erreicht, indem ray_width für jedes erzeugte Partikel um 0.01 reduziert wird. Ray_width ist die Variable, die für die Größe der Partikel zuständig ist, somit müssen wir nicht mehr tun, um diesen Effekt zu erreichen.

Ich ermuntere Sie, mit den Werten für ray_size und ray_width zu experimentieren und die Bitmap zu ändern, um eine Fülle interessanter Effekte herzustellen. Vergessen Sie auch nicht, die Zahlwerte in den Schleifen zu ändern (0.01 in meinem Beispiel).

Diese Laseraugen können niemanden verletzen, aber an Ihrer Stelle würde ich sie mit dem Code für das Sichtfeld aus AUM 28 kombinieren (ein kleiner Kegel für jedes Auge). Setzen Sie enable_scan = on für den Spieler und verletzen Sie ihn, wenn das Event aufgerufen wird. Auf diese Weise könnte ein interessanter Endgegner geschaffen werden!