Intelligente Fahrstühle

Ich bin ziemlich sicher, dass Sie, als Sie den Titel des Artikels sahen, so etwas dachten wie: “Ja, sicherlich... wie kann man Fahrstühle mit Intelligenz ausstatten?” Der Fahrstuhlcode in diesem Artikel funktioniert, wenn der Spieler sich einem Schalter nähert. Die “intelligente” Idee ist, dass der Schalter und der zugehörige Fahrstuhl nicht verbunden sind! Sie können so viele Schalter und Fahrstühle benutzen, wie Sie möchten und Sie werden wissen, welcher wozu gehört, ohne Pointer, Handles etc.! Sie benutzen dieselbe Action für jeden Schalter und für jede Plattform und der Code wird den Rest tun! Nun, da ich ihre Aufmerksamkeit habe, schauen wir uns an, wie es funktioniert.

Die Idee ist simpel: in einem gewöhnlichen Level ist die Distanz zwischen den Fahrstühlen in diesem Level relativ groß. Ich werde einen Schalter benutzen, welches in einer Umgebung scannt und sobald er eine Plattform in der Nähe findet, wird er sie benutzen. Hier ist der Code für den Schalter:

action elevator_button
{
while (1)
{
while (vec_dist (player.x, my.x) > 50) {wait (1);}
temp.pan = 360; // scan on a sphere
temp.tilt = 360; // because the elevator can be anywhere around this button
temp.z = 500; // the elevator should be closer than 500 quants all the time
scan_entity (my.x, temp);
sleep (0.4); // scan 2 times a second (sleep (0.4 + 0.1))
my.ambient = 100;
sleep (0.1);
my.ambient = 0;
}
}
Wenn der Spieler sich dem Schalter nähert, wird dieser in einem Radius von 500 Quants um sich scannen. Der Schalter wird blinken, da wir seinen ambient Wert ändern.

Die Action für den Fahrstuhl ist:

action elevator_entity
{
if (my.elevator_speed == 0) // if we forget to set skill1 in Wed
{
my.elevator_speed = 5; // set the default speed value
}
if (my.elevator_height == 0) // if we forget to set skill2 in Wed
{
beep; beep; exit; // shut down the engine
}
my.enable_scan = on;
my.event = move_elevator;
}
Der Fahrstuhl hat eine Standardgeschwindigkeit, elevater_speed = 5, wenn wir vergessen, den skill1 im WED zu setzen. Skill2 ist die Höhe des Zielpunktes (elevator_height) und sollte jedes Mal gesetzt werden. Ich möchte dessen sicher sein, daher wird die Engine zweimal piepen und sich beenden, wenn skill2 vergessen wird. Sollten Sie wirklich den Punkt 0 als Ziel brauchen, setzen Sie skill2 einfach auf 0.001 oder so.

Der Fahrstuhl reagiert darauf, wenn er gescannt wird: dann wird ein event ausgelöst:

function move_elevator()
{
proc_kill(1);
my.enable_scan = off;

Wir beenden alle weiteren Instanzen dieser Funktion und machen den Fahrstuhl unempfindlich für weitere Scans.

if (my.z < you.z)
{
while (my.z < (player.z - (player.max_z - player.min_z) / 2))
{
my.z += my.elevator_speed * time;
wait (1);
}
}

Ist der Fahrstuhl hinter dem Schalter, so muß er hinauffahren, bis er eine Höhe erreicht hat, die mit den Füßen des Spielers übereinstimmt.

if (my.z > you.z)
{
while (my.z > (player.z - (player.max_z - player.min_z) / 2))
{
my.z -= my.elevator_speed * time;
wait (1);
}
}

Ebenso hier: der Fahrstuhl bewegt sich nach unten, bis seine Höhe unterhalb der Füße des Spielers ist.

my.z = player.z - (player.max_z - player.min_z) / 2;

Wir setzen die Höhe nun auf die Fußhöhe des Spielers (sie war etwas zu klein oder zu groß)

while (vec_dist (player.x, my.x) > 50) {wait (1);}
my.enable_scan = on;
vec_set (player_strength, strength); // store player's speed on x, y, z
vec_set (strength, nullvector); // stop the player

Nun warten wir, bis der Spieler nah genug an die Mitte der Plattform gekommen ist, bevor wir das Scannen wieder aktivieren. Wir speichern die Geschwindigkeit des Spielers (strength) und setzen sie auf 0, da wir ihn für den Moment anhalten wollen (wir erlauben ihm, sich zu drehen).

while (my.z < my.elevator_height)
{
my.z += my.elevator_speed * time;
player.x = my.x;
player.y = my.y;
player.z = my.z + (player.max_z - player.min_z) / 2;
wait (1);
}

Solange die Z Koordinate des Fahrstuhls unterhalb von skill2 ist, wird sie erhöht und der Spieler wird in der Mitte (und auf) der Plattform behalten, indem die x y und z Koordinaten entsprechend angepaßt werden.

while (my.z > my.elevator_height)
{
my.z -= my.elevator_speed * time;
player.x = my.x;
player.y = my.y;
player.z = my.z + (player.max_z - player.min_z) / 2;
wait (1);
}

Ebenso, wenn der Fahrstuhl nach unten fährt.

vec_set (strength, player_strength);
}

Wenn der Fahrstuhl sein Ziel erreicht hat, stellen wir strength wieder her und erlauben dem Spieler damit, sich zu bewegen.

Ich habe ein Beispiellevel mit 2 Fahrstühlen und zwei Schaltern bereitgestellt. Natürlich können Sie so viele Fahrstühle und Schalter hinzufügen, wie Sie möchten, solange sie nicht zu nah aneinander sind. Ändern Sie den Wert temp.z = 500 in elevater_button, um die Scanreichweite zu ändern; die Distanz zwischen meinen Fahrstühlen ist größer als 500.

Inventory

Das folgende Projekt demonstriert Ihnen, wie Sie ein Inventar programmieren. Der Spieler wird in der Lage sein, einen Streitkolben, ein Schild und den Ring des Feuers aufzuheben. Er kann diese Gegenstände im Inventar behalten, oder er kann sie benutzen, um seine Fähigkeiten zu verbessern. Sehen wir uns die Main Funktion an:

function main()
{
level_load (inventory_wmb);
wait (2);
clip_size = 0;
on_d = null; // disable the debug panel (key "D") because it is used for movement
fps_max = 40;
check_inventory();
}

Ich habe die “D”-Taste deaktiviert, damit nicht der Debug-Screen erscheint, wenn ich den Spieler mit den WASD-Tasten bewege. Bevor ich die grausige check_inventory Funktion vorstelle, kommen einige Defines:

define items skill30;
define armour skill41;
define attack skill42;
define strength skill43;

define shield 1;
define mace 2;
define armor 4;
define ring 8;

Wenn der Spieler einen Gegenstand aufhebt, wird der entsprechende Wert zu player.items (einfach ein anderer Name für skill30) addiert. Wenn der Spieler ein Schild und einen Ring aufhebt, wird der Wert von player.items 1 (Schild) + 8 (Ring) = 9 sein, ja? Dies ist die Funktion:

function check_inventory()
{
while (1)
{
if (player.items > 0)
{

Hat der Spieler mindestens ein Item, so gilt player.items > 0

if (player.items == 1) // 1 + 0 + 0 + 0
{
shield_pan.visible = on;
mace_pan.visible = off;
armor_pan.visible = off;
ring_pan.visible = off;
}

Falls der Wert = 1 ist, hat der Spieler das Schild. Nur shield_pan wird sichtbar gemacht.

if (player.items == 2) // 0 + 2 + 0 + 0
{
shield_pan.visible = off;
mace_pan.visible = on;
armor_pan.visible = off;
ring_pan.visible = off;
}

Ist der Wert 2, so hat der Spieler nur den Streitkolben, also wird nur das sichtbar gemacht.

if (player.items == 3) // 1 + 2 + 0 + 0
{
shield_pan.visible = on;
mace_pan.visible = on;
armor_pan.visible = off;
ring_pan.visible = off;
}

Ist der Wert 3, hat der Spieler das Schild und den Streitkolben, also werden beide sichtbar gemacht.

...................................

Und so weiter bis zum Wert 15.

if (player.items == 15) // 1 + 2 + 4 + 8
{
shield_pan.visible = on;
mace_pan.visible = on;
armor_pan.visible = on;
ring_pan.visible = on;
}
}

Ist der Wert 15, so hat der Spieler alle Gegenstände, also werden alle Panels sichtbar gemacht.

else
{
shield_pan.visible = off;
mace_pan.visible = off;
armor_pan.visible = off;
ring_pan.visible = off;
}
mouse_map = cursor_pcx;
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;

Ist der Wert 0, sind alle Panels unsichtbar; wir setzen den Mauszeiger auf cursor_pcx; die Position wird in jedem Frame erneut berechnet.

if (mouse_right == 1)
{
mouse_mode = (mouse_mode + 2) % 4;
while (mouse_right == 1) {wait (1);}
}
wait (1);
}
}

Wenn die rechte Maustaste gedrückt wird, dann wird der Mouse_mode auf 0 oder 2 gesetzt, je nachdem wie oft gedrückt wird. Dies zeigt / verbirgt den Mauszeiger und erlaubt, Gegenstände im Inventar anzuklicken und zu benutzen. Die While Schleife wartet bis die Maustaste losgelassen wurde, ansonsten würde der Zeiger in jedem Frame erscheinen / verschwinden.

Ich verwende 5 Panels: das Hauptinventarpanel und 4 weitere Panels für die Gegenstände.

panel main_pan // the main panel for the items in the inventory
{
bmap = main_pcx;
pos_x = 700;
pos_y = 0;
layer = 10;
hbar = 7, 488, 85, blue_pcx, 1, player.armour;
hbar = 7, 518, 85, red_pcx, 1, player.attack;
hbar = 7, 550, 85, green_pcx, 1, player.strength;
flags = refresh, d3d, visible;
}

Das Hauptpanel enthält 3 hbars, welche die Rüstung des Spielers (armour, da armor vergeben ist), seinen Angriffswert und seine Stärke anzeigen.

panel shield_pan // displays the shield on the panel
{
bmap = shield_pcx;
pos_x = 715;
pos_y = 30;
layer = 11;
flags = overlay, refresh, d3d;
on_click use_shield;
}

panel mace_pan // displays the mace on the panel
{
bmap = mace_pcx;
pos_x = 730;
pos_y = 145;
layer = 11;
flags = overlay, refresh, d3d;
on_click use_mace;
}

panel armor_pan // displays the armor on the panel
{
bmap = armor_pcx;
pos_x = 710;
pos_y = 255;
layer = 11;
flags = overlay, refresh, d3d;
on_click use_armor;
}

panel ring_pan // displays the ring on the panel
{
bmap = ring_pcx;
pos_x = 710;
pos_y = 365;
layer = 11;
flags = overlay, refresh, d3d;
on_click use_ring;
}

Die Panels für die Gegenstände erscheinen über dem Hauptpanel, wenn sie aufgehoben werden; ich habe sie erstellt, indem ich die Gegenstände in einem schwarzen Raum plaziert habe und Screenshots gemacht habe. Jedes Gegenstandspanel hat eine simple Funktion, die aufgerufen wird, wenn das Panel angeklickt wird:
function use_shield()
{
player.items -= shield; // remove the shield from the panel
ent_create(shield_mdl, player.pos, attach_item);
player.strength += 30; // bigger hbar
}

Wir subtrahieren das Schild (1) von player.items; check_inventory wird das entsprechende Panel aus dem Inventar entfernen. Wir erstellen das richtige Schild, geben es dem Spieler und erhöhen Stärke; die entsprechende hbar wird größer.

function use_mace()
{
player.items -= mace; // remove the mace from the panel
ent_create(mace_mdl, player.pos, attach_item);
player.attack += 50; // bigger hbar
}

Wir subtrahieren Streitkolben (2) von player.items. Wieder wird die Funktion check_inventory den Rest übernehmen. Wir erstellen den Streitkolben und geben ihn dem Spieler; der Angriffswert steigt.

function use_armor()
{
player.items -= armor; // remove the armor from the panel
ent_create(armor_mdl, player.pos, attach_item);
player.armour += 40; // bigger hbar
}

Hier subtrahieren wir die Rüstung (4) von player.items; wiederum entfernt check_inventory das Panel. Die Anzeige für den Rüstungswert steigt.

function attach_item()
{
proc_late();
my.passable = on;
my.metal = on;
my.albedo = 0;
while(player != null)
{
vec_set(my.x, player.x);
vec_set(my.pan, player.pan);
my.frame = player.frame;
my.next_frame = player.next_frame;
wait(1);
}
}

Diese 3 Gegenstände werden mit dieser Funktion an den Spieler “geklebt”:

function attach_item()
{
proc_late();
my.passable = on;
my.metal = on;
my.albedo = 0;
while(player != null)
{
vec_set(my.x, player.x);
vec_set(my.pan, player.pan);
my.frame = player.frame;
my.next_frame = player.next_frame;
wait(1);
}
}

Der Streitkolben, das Schild und die Rüstung wurden gemeinsam mit dem Spieler animiert und als getrennte Models gespeichert, haben also dieselben Frames. Ich habe die Rüstung als Model erstellt, indem ich alle Polygone gelöscht habe, die nicht benötigt wurden; dann habe ich sie ein wenig skaliert, bis sie gut aussah.

Solange der Spieler existiert, werden diese Items an seine Position gebracht und benutzen denselben Winkel und dasselbe Frame.

Der Ring ist ein spezieller Gegenstand, hier ist der Code:

function use_ring()
{
player.items -= ring;
while (1)
{
if (mouse_left == 1 && mouse_mode == 0 && number_of_flames == 0) // the mouse pointer is invisible and not too many entities are visible
{
temp_angle = 0;
while (temp_angle < 360)
{
temp_angle += 3;
temp.x = player.x + 100 * cos(temp_angle);
temp.y = player.y + 100 * sin(temp_angle);
temp.z = player.z;
ent_create (fire_pcx, temp, fire_action);
}
}
while (mouse_left == 1) {wait (1);}
wait(1);
}
}

Wir subtrahieren den Ring (8) von player.items und kommen in eine endlose while Schleife. Wenn die linke Maustaste gedrückt wird und der Mauszeiger unsichtbar ist und zugleich die Anzahl der Flammen 0 ist, erstellen wir 120 Flammen um den Spieler herum (in 100 Quants Abstand) und warten, bis die Taste losgelassen wird.

function fire_action()
{
number_of_flames += 1; // add another flame entity to this var
my.passable = on;
my.flare = on;
my.bright = on;
my.oriented = on;
my.pan = random(360);
while (my.pan < 1500)
{
my.pan += (5 + random(3)) * time;
my.z -= 0.4 * time;
wait (1);
}
number_of_flames -= 1;
ent_remove (me);
}

Wir möchten kein “Autofeuer” benutzen, also wird number_of_flames mit jeder erzeugten Flamme erhöht; so können wir sicher sein, dass stets nur ein einzelner Flammenring entsteht und die Frame Rate im Rahmen bleibt. Die Flammen sind passierbar und bekommen einen zufälligen Startwinkel. Sie rotieren, bis sie einen Pan-Wert von 1500 haben (das funktioniert!) und verringern zugleich ihren z Wert. Nachdem alle Flammen entfernt wurden, wird number_of_flames wieder 0 sein und die Funktion use_ring kann einen weiteren Feuerring erstellen.

Nun ist es Zeit, dass wir uns die Actions anschauen, die zu den Gegenständen gehören, welche aufgehoben werden können:

action shield_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("idle", my.skill20); // play stand frames animation
my.skill20 += 3 * time; // "idle" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += shield;
ent_remove (me);
}

Das Schild ist passierbar und spielt seine “idle” Animation ab, bis der Spieler näher als 50 Quants ist, um es aufzuheben. Wenn dies geschieht, wird player.items um Schild (1) erhöht und das Schild wird entfernt. Die anderen Actions sind alle sehr ähnlich:

action mace_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("idle", my.skill20); // play stand frames animation
my.skill20 += 3 * time; // "idle" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += mace;
ent_remove (me);
}

action armor_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("stand", my.skill20); // play stand frames animation
my.skill20 += 2 * time; // "stand" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += armor;
ent_remove (me);
}

action ring_init
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (my.x, player.x) > 50)
{
ent_cycle("frame", my.skill20); // play "frame" animation
my.skill20 += 2 * time; // "frame" animation speed
my.skill20 %= 100;
wait (1);
}
player.items += ring;
ent_remove (me);
}

Zeit für die letzte Action? Die Rede ist von player_moves, die Action, die dem Spieler selbst zugeordnet ist:

action player_moves // attached to the player
{
player = me; // I'm the player
player.items = 0;
player.armour = 20;
player.attack = 30;
player.strength = 35;

Der Spieler startet ohne irgendeinen Gegenstand und mit Werten für Rüstung, Angriff und Stärke.

while (1)
{
camera.x = player.x - 200 * cos(player.pan);
camera.y = player.y - 200 * sin(player.pan);
camera.z = player.z + 200;
camera.pan = player.pan;
camera.tilt = -30;

Die Kamera wird 200 Quants hinter den Spieler und ein wenig darüber plaziert und zeigt nach unten.

my.pan += 4 * (key_a - key_d) * time - 20 * mouse_force.x * time;
player_speed.x = 10 * (key_w - key_s) * time;
player_speed.y = 0;
player_speed.z = 0;

Der Spieler kann sich mit Hilfe der “A” und “D” Tasten, sowie mit der Maus drehen; er kann mit den “W” und “S” Tasten nach vorne bzw. rückwärts laufen.

if ((key_w + key_s) != 0)
{
ent_cycle("walk", my.skill20);
my.skill20 += 4 * (key_w - key_s) * time;
my.skill20 %= 100;
}
else
{
if (mouse_left != 1)
{
ent_cycle("stand", my.skill20); // play stand frames animation
my.skill20 += 2 * time; // "stand" animation speed
my.skill20 %= 100;
}

Wenn die linke Maustaste nicht gedrückt ist, steht der Spieler, also spielt das Model seine “stand” Animation ab.

else // the player has pressed the left mouse button
{
if (ring_pan.visible != on && mouse_mode == 0 && number_of_flames == 0) // the ring has been used
{
my.skill20 = 0;
ent_cycle("attack", my.skill20); // play stand frames animation
if (my.skill20 < 99)
{
my.skill20 += 0.5 * time; // "attack" animation played once (no loop)
}
}
while (mouse_left == 1) {wait (1);}
}
}

Ist die Taste gedrückt und der Ring des Feuers wurde benutzt und der Mauszeiger ist unsichtbar und number_of_flames ist 0, dann spielt das Model seine “Attack” Animation einmal und “friert” dann ein, bis wir die Taste loslassen.

move_mode = ignore_passable;
ent_move(player_speed, nullvector);
wait (1);
}
}
Schließlich erlaubt move_mode = ignore_passable dem Spieler, die Gegenstände zu tragen, ohne dadurch verlangsamt zu werden. Ent_move erzeugt die Bewegung.

Schön und gut, aber was, wenn ich nun möchte, dass die Gegner durch den Ring des Feuers verletzt werden? Wie geht das?
Die einfachste Methode (meiner Meinung nach) wäre es, den Spieler 100 Quants um seine Position scannen zu lassen, solange number_of_flames != 0 ist. Die Gegner können Events verwenden, um ihre Gesundheit zu senken, wenn event_type == event_scan. Auf diese Weise braucht man keine zeitraubenden Scan-Funktionen für jede Flamme.