Moon Alert

Dieses Spiel wurde von Sincliar Spectrum vor ca. 10 Jahren für den Commodore 64 entwickelt. Es ist meiner Meinung nach immer noch ein sehr gutes Spiel, wenn Sie sich das Konzept anschauen und nicht meine schlechte Grafik.

Ehe wir beginnen, sehen wir uns das Spiel einmal an (Kommentare in gelb):

Sie steuern den Panzer (ich brauche ein Model mit einem Turm) und können damit über einige Löcher springen, indem Sie die Geschwindigkeit verändern: eine hohe Geschwindigkeit ermöglicht es, über größere Löcher zu kommen. Außerdem müssen Sie die auf die Ufos schießen, damit diese Sie nicht abschießen.

Ich starte mit den Definitionen für Texte und Panels; ich verwende einen einzigen String, um "Score:" und "Lives:" darzustellen und zwei Zahlfelder für die jeweiligen Werte:

text moon_text
{
font = alert_font;
pos_x = 0;
pos_y = 0;
string = "Score: Lives:";
flags = visible;
}

panel moon_panel // displays the score using 3 digits and lives using 1 digit
{
pos_x = 0;
pos_y = 0;
digits = 190, 0, 3, alert_font, 1, score;
digits = 720, 0, 1, alert_font, 1, lives;
flags = refresh, d3d, visible;
}

Die Main Funktion sorgt dafür, dass immer alle Polygone der Models angezeigt werden, setzt die Framerate auf 30 und lädt das Level:

function main()
{
clip_size = 0;
fps_max = 30;
level_load (alert_wmb);
}

Der Spieler steuert dne Panzer mit dieser Action:

action players_vehicle
{
player = me;
my.z = 20;
ent_create(barrel_mdl, nullvector, attach_barrel);
vehicle_speed.y = 0;
vehicle_speed.z = 0;

Der Panzer wird 20 Quants über dem Boden plaziert, auf diese Weise kann er auch nach einem großen Sprung nicht steckenbleiben. Wir erstellen das Kanonenrohr und fügen es unserem Panzer mit der Funktion attach_barrel() zu. Der Panzer bewegt sich mit der durch vehicle_speed gegebenen Geschwindigkeit fort; er bewegt sich nur von links nach rechts (in x-Richtung), also setzen wir die anderen beiden Komponenten (y und z) zurück.

while (lives > 0)
{
if (key_cul == 1 && speed_offset > 0) // use cursor left to slow down the vehicle
{
speed_offset -= 1 * time;
}
if (key_cur == 1 && speed_offset < 5) // use cursor up to increase the speed of the vehicle
{
speed_offset += 1 * time;
}
vehicle_speed.x = 5 + speed_offset; // speed = 5...10
vehicle_speed *= time;
if (key_space == 1)
{
vehicle_jumps();
}

Solange der Spieler lebt, kann er die Geschwindigkeit des Panzers erhöhen oder senken, indem er die Pfeiltasten nach links bzw. rechts drückt. Die Geschwindigkeit variiert zwischen 5 und 10, weil der Wert für speed_offset zwischen 0 und 5 variieren kann. Die Space Taste läßt den Panzer springen; dazu später mehr.

move_mode = ignore_passable;
ent_move (vehicle_speed, nullvector);
camera.x = my.x;
camera.y = -2500;
camera.z = 700;
camera.pan = 90;
score += 0.01 * time; // add to the score as long as the player manages to survive
wait (1);
}
}

Der Panzer ignoriert Entities. Die Kamera folgt ihm Frame für Frame, wie im Bild zu sehen:

Die Punktzahl erhöht sich (langsam) die ganze Zeit über, da es schon schwierig genug ist, zu überleben. Wenn der Spieler einen Feind abschießt, steigt die Punktzahl sprunghaft an. Hier nun die Funktion für das Kanonenrohr:

function attach_barrel()
{
proc_late(); // prevent shaking
my.passable = on;
while(lives > 0) // as long as the player isn't dead
{
vec_set(my.x,you.x);
my.pan = you.pan;
my.tilt = barrel_offset;
if (key_cuu == 1 && barrel_offset < 90) // use cursor up
{
barrel_offset += 3 * time;
}
if (key_cud == 1 && barrel_offset > 0) // and cursor down to change the speed
{
barrel_offset -= 3 * time;
}

Die erste Coezeile verhindert, dass der Turm um ein Frame hinter dem Panzer hinterherhinkt. Solange der Spieler lebt, erhält der Turm dieselbe Position wie der Panzer und auch den gleichen Winkel (pan). Wir möchten den Tilt des Turms selbst kontrollieren, weil wir so auf die Ufos schießen können, die an verschiedenen Stellen auftauchen. Mit den Pfeiltasten Hoch bzw. runter wird der barrel_offset manipuliert; dies variiert den Tilt Winkel des Turms zwischen 0 und 90 Grad.

vec_for_vertex(rocket_coords, my, 33); // vertex for player's rocket
if (key_ctrl == 1 && rocket_ptr == null && lives > 0) // ctrl fires a rocket
{
ent_create (rocket_mdl, rocket_coords, fire_rocket);
snd_play (rocket_wav, 100, 0);
}
if (you.invisible == off) // used for blinking when the player loses a life
{
my.invisible = off;
}
else
{
my.invisible = on;
}
wait(1);
}
ent_remove(my);
}

Wir werden vom 33. Vertex des Turmes aus feuern, also sichern wir diese Position in dem Vektor rocket_coords. Wenn der Spieler "Strg" drückt und keine andere Rakete zu sehen ist (rocket_ptr == null) und der Spieler nicht tot ist, erstellen wir eine Rakete und lassen ein Geräusch ertönen. Die Funktion fire_rocket() enthält alles Wissenswerte über den rocket_ptr Zeiger. Dazu kommen wir später.

Der Spieler verliert ein Leben, wenn er in ein Loch fährt oder von einer Rakete eines Ufos getroffen wird. Tritt eines dieser Ereignisse ein, blinkt der Panzer einige Male, um zu verdeutlichen, was geschehen ist. Ich dachte mir, es sähe nicht so gut aus, wenn der Panzer blinkt und der Turm die ganze Zeit sichtbar ist, daher ist dieser nur dann sichtbar, wenn die erstellende Entity (you), also der Panzer, sichtbar ist. Falls die Anzahl der Leben kleiner ist als 1 (der Spieler ist tot), verschwindet der Turm.

Kommen wir nun zu der Funktion, die der Rakete zugeordnet ist:

function fire_rocket()
{
while (rocket_ptr != null) {wait (1);} // disable auto fire
rocket_ptr = me;
wait (1);
my.pan = you.pan;
my.tilt = you.tilt;
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = rocket_explodes;
while ((my != null) && (vec_dist(my.x, player.x) < 2000))
{
rocket_speed.x = 100;
rocket_speed.y = 0;
rocket_speed.z = 0;
rocket_speed *= time;
move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
ent_move (rocket_speed, nullvector);
wait (1);
}
ent_remove (me);
}

Sie könnten sich fragen, wofür wir den rocket_ptr Zeiger benötigen. Das ist eine neue Methode, um Dauerfeuer zu verhindern. Wenn der Spieler eine Rakete abfeuert, wird eine neue Entity mit Zeiger rocket_ptr erstellt. Wenn der Spieler eine neue Rakete abfeuern will, obwohl die alte noch sichtbar ist, dann ist rocket_ptr nicht gleich null und deshalb kann er nicht schießen:

while (rocket_ptr != null) {wait (1);} // disable auto fire

Die Rakete erhält denselben pan und tilt Winkel wie die erstellende Entity (der Turm); sie reagiert auf impact, level blocks und andere Entities. Wenn sie mit irgendetwas zusammenstößt, läuft die rocket_explodes Eventfunktion. Solange die Rakete existiert und ihr Abstand zum Spieler kleiner als 2000 Quants ist, ignoriert sie alle passierbaren Entities und den Turm (you). Auf diese Weise kollidiert sie nicht gleich mit dem Panzerturm. Falls sich die Rakete weiter als 2000 Quants vom Spieler entfernt hat, entfernen wir sie, damit wir keine Ressourcen verschwenden.

Was aber geschieht nun, wenn die Rakete etwas trifft? Die Antwort steht in der folgenden Funktion:

function rocket_explodes()
{
wait (1);
snd_play (explosion_wav, 40, 0);
vec_set (temp, my.pos);
temp.y -= 50; // move the explosion sprite a little closer to the player
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
}

Wir warten einen Frame, um die "dangerous event" Warnung der Engine zu vermeiden, lassen das Explosionsgeräusch ertönen und erstellen ein Sprite 50 Quants näher an der Kamera, wie in dem Bild:

Dies verhindert, dass ein Teil des Sprites selbst von der getroffenen Entity verdeckt wird, was nicht sehr gut aussähe. Das Sprite startet seine animated_explosion Funktion und die Rakete wird entfernt. Hier also die Funktion, die das Sprite animiert:

function animated_explosion()
{
wait (1);
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
while (my.frame < 7)
{
my.frame += 1 * time;
wait (1);
}
ent_remove (me);
}

Wir warten ein Frame und machen das Sprite passable, setzen die flare und bright Flags und lassen es durch einen hohen Ambient Wert leuchten. Die Animationsframes für die Explosion werden mit der Geschwindigkeit 1 * time abgespielt. Sobald das Sprite das Ende der Frames erreicht, wird es entfernt.

Die letzte Funktion gehört zum Spieler und ist für das Springen verantwortlich:

function vehicle_jumps()
{
if (my.skill40 == 1) {return;} // disable multiple jumps
snd_play (jump_wav, 30, 0);
my.skill40 = 1;
while (my.z < 300)
{
vehicle_speed.z = 5 * time;
wait (1);
}
while (my.z > 20)
{
vehicle_speed.z = -5 * time;
wait (1);
}
vehicle_speed.z = 0;
my.z = 20; // restore the exact vehicle height
my.skill40 = 0; // allow a new jump
snd_play (landed_wav, 30, 0);
}

Erinnern Sie sich, dass ich Ihnen erzählt habe, dass der Panzer sich nur mit Hilfe der x-Koordinate von vehicle_speed bewegt? Ich habe nicht ganz die Wahrheit gesagt; wie könnte er springen, ohne vehicle_speed.z zu verändern? Es ist bekannt, dass alle Skills einer Entity zum Spielstart auf 0 stehen und my.skill40 ist keine Ausnahme. Wir lassen einen Sprungsound erklingen und setzen skill40 auf 1. Auf diese Weise kann der Panzer nicht ein zweites Mal springen, wenn er in der Luft ist. Solange die Höhe des Panzers unter 300 Quants liegt, erhöhen wir diese, indem wir vehicle_speed.z einen positiven Wert zuweisen. Erreicht der Panzer die 300 Quants, wird die erste Schleife beendet und eine zweite beginnt, in der vehicle_speed.z einen negativen Wert erhält und der Panzer daher fällt. Diese läuft solange bis die Höhe des Panzers unter 20 Quants fällt. Falls Sie sich fragen, warum ich gerade diesen Wert gewählt habe: ich möchte nicht, dass der Panzer im Boden steckenbleibt, der etwa bei der z-Koordinate 0 ist.

Ist die zweite Schleife beendet, setzen wir den Panzer auf seine alte Höhe (20 Quants), setzen skill40 zurück (um einen neuen Sprung zu gestatten) und spielen die landed_wav Datei ab.

Nun ist es Zeit, sich die Löcher anzusehen. Sie sind sicher überrascht, dass es keine wirklichen Löcher sind, sondern schwarze Map Entities. Normalerweise benutze ich die Texturen der standard.wad für meine Projekte, doch dieses Mal brauchte ich einfach einige eintönig gefärbte Flächen und habe deshalb eine neue WAD erstellt (colors.wad), die Sie in Ihren /wads Ordner kopieren sollten.

Wir möchten, dass der Panzer mit den Löchern "kollidiert", richtig? Das ist aber nicht ganz so leicht. Stellen Sie sich diese Situation vor:

Glauben Sie, dass der Panzer aus diesem Loch herauskommt? Sehen Sie, ich auch nicht. Deshalb müssen wir ein anderes System für die Löcher verwenden und daher benutzen wir schwarze Entities wie in diesem Bild:

Stellen Sie sich vor, dass die weiße Entity im Bild schwarz ist; es würde wie ein Loch aussehen, da der Hintergrund auch schwarz ist. Der Panzer kann mit der Entity zusammenstoßen und es wird aussehen, als sei er in das Loch gefahren. Damit besteht das Level aus einem einzigen Block und diesen Entities.

Jedes dieser "Löcher" hat eine einfache Action:

action obstacle
{
my.enable_impact = on;
my.enable_entity = on;
my.event = obstacle_hit;
}

Jede Loch-Entity reagiert auf Impact und andere Entities; die Eventfunktion ist obstacle_hit:

function obstacle_hit()
{
my.passable = on; // don't trigger other events for this entity
lives -= 1;
player.invisible = on; // make the player blink 3 times
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
sleep (0.3);
player.invisible = on;
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
sleep (0.3);
player.invisible = on;
sleep (0.3);
snd_play (lost_wav, 60, 0);
player.invisible = off;
}

Sobald der Panzer auf ein Loch fährt, wird dieses passierbar gemacht. Die nächste Zeile verringert die Anzahl der Leben um 1 - deshalb haben wir das Loch passierbar gemacht, wir möchten nicht mehr als ein Leben für ein einzelnes Loch abziehen. Der Spieler blinkt drei Mal und jedes Mal ertönt ein Geräusch.

Das Spiel wäre aber zu leicht, wenn die Löcher die einzigen Gegner wären. Schauen wir uns die richtigen Gegner einmal an:

action ufo
{
while (player == null) {wait (1);}
my.y = player.y; // we don't have to align the enemies in Wed
my.enable_impact = on;
my.enable_entity = on;
my.event = destroy_ufo;
while (vec_dist (player.x, my.x) > 1000) // if the player is far away
{
wait (1); // wait
}

Der Feind wartet, bis der Spieler erstellt wurde und gleicht dann seine y Koordinate an die des Spielers an, wie im Bild:

Der Panzer schießt die Raketen geradlinig, daher würden die Raketen verfehlen, wenn wir im WED nicht aufpassen. Diese einfache Codezeile erledigt die Ausrichtung für uns.

Das Ufo reagiert auf impact und andere Entities (die Raketen des Spielers); wird es getroffen, startet die destroy_ufo Funktion. Ist der Spieler mehr als 1000 Quants entfernt, steht das Ufo still.

while (my.x > player.x)
{
my.x -= 5 * time;
if (total_frames % 90 == 1)
{
ent_create (rocket_mdl, my.pos, fire_bomb); // fire a rocket every 3 seconds
}
wait (1);
}
}

Diese Schleife beginnt, wenn der Panzer näher als 1000 Quants herangekommen ist. Das Ufo wird sich nach links und rechts bewegen, indem die x Koordinate geändert wird (wir benötigen die ent_move Anweisung hier nicht). Wir möchten eine glatte Bewegung, daher müssen wir die Position in jedem Frame ändern, aber wir möchten nicht, dass das Ufo in jedem Frame schießt, da der Spieler dann keine Chance hätte. Ich habe mich entschieden, die Variable total_frames zu benutzen, in der steht, wieviele Frames seit dem Spielstart schon vergingen. Da wir die Framerate auf 30 begrenzt haben, wird total_frames pro Sekunde um höchstens 30 erhöht. Der Ausdruck (total_frames % 90 == 1) ist wahr, wenn total_frames = 1, 91, 181, 271, etc. ist. Auf diese Weise schießt das Ufo etwa alle 3 Sekunden.

Schauen wir uns die Funktion der gegnerischen Raketen an:

function fire_bomb()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // rotate the bomb towards the player
my.enable_impact = on;
my.enable_block = on;
my.enable_entity = on;
my.event = hit_player; // same event here
while (my != null)
{
bomb_speed.x = 100;
bomb_speed.y = 0;
bomb_speed.z = 0;
bomb_speed *= time;
move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
ent_move (bomb_speed, nullvector);
wait (1);
}
}

Diese Funktion ist ähnlich zu der fire_rocket Funktion, die der Spieler benutzt. Der einzige Unterschied besteht darin, dass die ersten 3 Codezeilen die Rakete auf den Spieler ausrichten. Wenn der Spieler das Ufo trifft, läuft diese Funktion:

function destroy_ufo()
{
score += 20;
ent_remove (me);
}

Sie erhöht die Punktzahl um 20 und entfernt das Ufo. Trifft ein Ufo aber den Spieler, läuft diese Funktion:

function hit_player()
{
snd_play (explosion_wav, 40, 0);
vec_set (temp, my.pos);
temp.y -= 50; // move the explosion sprite a little closer to the player
ent_create(explosion_pcx, temp, animated_explosion);
ent_remove (me);
lives -= 1;
}

Ein Geräusch ertönt und wir erstellen das animierte Sprite für die Explosion. Die gegnerische Rakete wird entfernt und wir nehmen ein Leben weg.

Raketenkamera

Wenn Sie durch die "Augen" Ihrer Rakete schauen möchten, dann wird Ihnen dieser Code helfen. Wenn Sie lernen möchten, wie man eine komplett neue Waffe erstellt, dann wird er Ihnen sogar sehr helfen.

Ich wollte keine der Templatewaffen verwenden, da ich die Templates nur ungern abändere, auch wenn ich es schon ein paar Mal mußte. Als erstes definieren wir eine "Entity", die als Waffe auf dem Bildschirm erscheinen wird; stellen Sie die x y und z Koordinaten ein, bis es gut aussieht:

entity rocket_ent
{
type = <rlauncher.mdl>;
layer = 10;
view = camera;
x = 20; // x y z set he position of the entity
y = -10;
z = -10;
}

Die Kamera der Rakete benutzt eine neue View:

view rocket_view
{
layer = 15;
pos_x = 0; // appears in the upper left corner
pos_y = 0;
size_x = 640;
size_y = 100;
arc = 80;
flags = visible;
}

Das neue View Fenster ist 640 x 100 Pixel groß. Wenn Ihr Spiel eine andere Auflösung hat, ändern Sie diesen wert. Rocket_view hat einen etwas größeren arc-Wert, als die Standardkamera (Default ist 60), auf diese Weise sind mehr Details sichtbar. Schauen wir uns die Action des Raketenwerfers an:

action rocket_launcher
{
my.passable = on;
while (player == null) {wait (1);}
while (vec_dist (player.x, my.x) > 100)
{
my.pan += 3 * time;
wait (1);
}
ent_remove (me);
rocket_ent.visible = on;

Die Waffe ist passierbar und dreht sich, bis der Spieler näher ist als 100 Quants. Wenn dies geschieht, wird der Raketenwerfer entfernt und die Raketenwerfer "Entity" wird sichtbar gemacht.

while (player._health > 0) // fire only if the player is alive
{
if (key_ctrl == 1)
{
while (key_ctrl == 1) {wait (1);} // disable auto fire
vec_set (temp, rocket_ent.pos);
vec_for_screen (temp, camera);
ent_create (rocket_mdl, temp, move_rocket);
}
wait (1);
}
}

Der Spieler kann nur feuern, wenn er am Leben ist. Drückt er die Strg Taste zum Schießen, warten wir, bis diese wieder losgelassen wurde und erstellen die Rakete in der rocket_ent Entity. Wenn Sie das Handbuch gelesen haben, wissen Sie, dass diese Entities in der 3D Welt nicht existieren; sie funktionieren eher wie ein Panel. Wie also erstellen wir die Rakete? Nun, wir setzen temp auf die Position von rocket_ent und benutzen dann die vec_for_screen Anweisung, um diese 2D Position in Koordinaten in der 3D Welt zu übersetzen.

Schauen wir uns die Funktion an, welche die Rakete bewegt:

function move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.passable = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;

Die Rakete reagiert auf Entities und Level Blocks; wir setzen Sie für den Anfang auf passable, damit sie nicht schon mit dem Spieler zusammenstößt. Falls die Rakete mit einer Entity oder einem Block kollidiert, wird das remove_rocket Event ausgelöst. Die Rakete erhält den pan und den tilt Winkel der Kamera, also fliegt sie in die Richtung, in die die Kamera weist.

while (my != null)
{
rocket_view.x = my.x - 100 * cos(my.pan);
rocket_view.y = my.y - 100 * sin(my.pan);
rocket_view.z = my.z + 10;
rocket_view.pan = my.pan;
rocket_view.tilt = -5;
rocket_speed.x = 30;
rocket_speed *= time;
ent_move (rocket_speed, nullvector);
if (vec_dist (camera.x, my.x) > 30)
{
my.passable = off;
}
wait (1);
}
}

Solange die Rakete existiert, plazieren wir die neue View (rocket_view) 100 Quants dahinter und 10 Quants darüber. Diese neue View hat den tilt Winkel auf -5, sieht also nach unten. Die Rakete fliegt mit der Geschwindigkeit, die durch rocket_speed.x gegeben ist; ändern Sie die 30, um die Geschwindigkeit anzugleichen. Falls der Abstand zwischen der Kamera und der Rakete größer ist als 30 Quants, wird die Rakete auf unpassierbar gesetzt; man kann nun recht sicher sein, dass sie nicht mehr mit dem Spieler kollidiert.

Trifft die Rakete auf etwas, läuft diese Funktion:

function remove_rocket()
{
if (you != null)
{
you._health -= 150;
}
vec_set (temp, my.pos);
ent_create(explosion_pcx, temp, sprite_explosion);
ent_remove (me);
}

Falls die Rakete eine Entity trifft, zieht sie 150 Punkte von der Gesundheit dieser Entity ab; wird ein Auto getroffen, hat dies keinen Effekt, aber ein Gegner wird dadurch getötet. Dann erstellen wir ein Explosionssprite an der Stelle, wo die Rakete auftraf und lassen die folgende Funktion ablaufen:

function sprite_explosion()
{
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);
}

Hier passiert nichts Aufregendes: die Explosion ist passable und bright. Wir lassen ein Geräusch ertönen, lassen die Animation laufen und entfernen das Sprite.