Planet Survivors

Ob Sie es glauben oder nicht, die Serie ist vorbei! Es hat einige Monate gedauert, aber jetzt haben wir ein Projekt, das vier verschiedene Spiele vereint. In diesem Monat habe ich einige Bugs behoben und ein System zum Speichern und Laden von Spielen eingebaut; das wollen wir uns näher ansehen.

string name1_str = "                                       "; // up to 39 characters, feel free to increase the limit
string name2_str = "                                       "; // up to 39 characters, feel free to increase the limit
string name3_str = "                                       "; // up to 39 characters, feel free to increase the limit
string name4_str = "                                       "; // up to 39 characters, feel free to increase the limit

text slot1_txt
{
  pos_x = 205;
  pos_y = 180;
  layer = 150;
  font = system_font;
  string = name1_str;
}

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

text slot4_txt
{
  pos_x = 205;
  pos_y = 480;
  layer = 150;
  font = system_font;
  string = name4_str;
}

Ich habe mich dafür entschieden, dem Spieler vier Slots zum Speichern zu geben; die Namen, die der Spieler den Spielständen gibt werden in name1_str, name2_str usw. gespeichert. Ich habe auch vier Text-Objekte erstellt, die diese Namen auf den “Load Game” und “Save Game” Panels anzeigen.

panel blackpanel_pan
{
  bmap = blackpanel_pcx;
  layer = 2;
  pos_x = 0;
  pos_y = 0;
  flags = overlay, refresh;
}

panel savegame_pan
{
  bmap = savegame_pcx;
  layer = 100;
  pos_x = 0;
  pos_y = 0;
  button = 198, 158, slot2_pcx, slot1_pcx, slot2_pcx, ps_save1, null, mouse_over;
  button = 198, 258, slot2_pcx, slot1_pcx, slot2_pcx, ps_save2, null, mouse_over;
  button = 198, 358, slot2_pcx, slot1_pcx, slot2_pcx, ps_save3, null, mouse_over;
  button = 198, 458, slot2_pcx, slot1_pcx, slot2_pcx, ps_save4, null, mouse_over;
  flags = overlay, refresh;
  on_click = game_was_saved;
}

panel loadgame_pan
{
  bmap = loadgame_pcx;
  layer = 100;
  pos_x = 0;
  pos_y = 0;
  button = 198, 158, slot2_pcx, slot1_pcx, slot2_pcx, ps_load1, null, mouse_over;
  button = 198, 258, slot2_pcx, slot1_pcx, slot2_pcx, ps_load2, null, mouse_over;
  button = 198, 358, slot2_pcx, slot1_pcx, slot2_pcx, ps_load3, null, mouse_over;
  button = 198, 458, slot2_pcx, slot1_pcx, slot2_pcx, ps_load4, null, mouse_over;
  flags = overlay, refresh;
  on_click = cancel_loading;
}

Ich habe ein Panel mit Hilfe einer schwarzen Bitmap definiert; damit überdecke ich das Spiel, wenn der Spieler Esc drückt, um das Hauptmenü anzuzeigen. Die folgenden Panels werden für das Speichern und Laden genutzt; jedes hat vier Knöpfe, die den Speicherslots entsprechen. Wann immer das savegame_pan Panel angeklickt wird, dann läuft die Funktion game_was_saved() ab; das gleiche geschieht mit dem loadgame_pan und der Funktion cancel_loading().

Was geschieht nun, wenn während des Spiels die Esc-Taste gedrückt wird? Bislang wurde dadurch das Spiel sofort beendet, aber jetzt spielt die Taste eine andere Rolle – folgende Funktion wird gestartet:

function return_to_main()
{
  if (main_pan.visible == on) {return;}
  freeze_mode = 1;
  blackpanel_pan.visible = on;
  main_pan.visible = on;
  mouse_mode = 2;
  while (freeze_mode == 1)
  {
    mouse_map = pointer1_tga;
    mouse_pos.x = pointer.x;
    mouse_pos.y = pointer.y;
    wait (1);
  }
}

Ist das Main Panel bereits sichtbar, geschieht nichts; andernfalls werden alle Actions im Level gestoppt und das schwarze Panel wird angezeigt. Wir zeigen auch das Main-Panel – da dieses einen höheren Layer-Wert als das schwarze Panel hat, wird es darüber angezeigt. Der Mauszeiger wird angezeigt und wir erlauben die Bewegung der Maus solange das Spiel angehalten ist.

function save_game()
{
  loadgame_pan.visible = off;
  savegame_pan.visible = on;
  save_handle = file_open_read("save1.dat"); // open the file
  if (save_handle != 0) // if the file exists
  {
    file_str_read(save_handle, name1_str);
    slot1_txt.string = name1_str;
    slot1_txt.pos_x = 205 + (39 - str_len(name1_str)) * 5; // center the text, allow up to 39 characters, 5 = 10 / 2 (character width / 2)
    slot1_txt.visible = on;
    file_close(save_handle);
  }
  .....................................................
  save_handle = file_open_read("save4.dat"); // open the file
  if (save_handle != 0) // if the file exists
  {
    file_str_read(save_handle, name4_str);
    slot4_txt.string = name4_str;
    slot4_txt.pos_x = 205 + (39 - str_len(name4_str)) * 5; // center the text, allow up to 39 characters, 5 = 10 / 2 (character width / 2)
    slot4_txt.visible = on;
    file_close(save_handle);
  }
}

Die obige Funktion läuft, wenn der Spieler den “Save”-Knopf im Main Panel drückt. Sie verbirgt das “Load Game” Panel sofern das sichtbar war und zeigt statt dessen das “Save Game” Panel an. Wir öffnen die Datei mit Namen save1.dat (falls diese existiert) und lesen den Namen des Slots aus der Datei, indem wir slot1_txt den entsprechenden String zuweisen. Der Text wird zentriert und angezeigt. Schließlich wird die Datei noch geschlossen und der Prozess wiederholt sich für die anderen Slots.

Falls Sie sich nun fragen, wofür man diese save1.dat ... save4.dat Dateien braucht, dann sind Sie nicht allein; ich habe mich das auch gefragt... warten Sie, jetzt weiß ich es wieder: ich mußte den Namen für das gespeicherte Spiel separat abspeichern, weil ich die Namen auf den “Load Game” bzw. “Save Game” Panels anzeigen wollte, bevor tatsächlich ein Spiel geladen oder gespeichert wird. In meiner Demo gibt es bereits gespeicherte Spiele mit Namen level1 bis level4 (raten Sie mal, warum die so heißen...), aber Sie können jeden Namen verwenden, solange er weniger als 39 Zeichen lang ist. Wie ich auf diesen Wert gekommen bin? Ein Zeichen ist in der verwendeten Schriftart 10 Pixel breit und der Knopf ist 400 Pixel breit, also kann ich 39 * 10 = 390 Pixel bequem für die Beschriftung verwenden, ohne den Grenzen zu nahe zu kommen.

function load_game()
{
  media_stop(media_handle); // stop the soundtrack
  savegame_pan.visible = off;
  loadgame_pan.visible = on;
  save_handle = file_open_read("save1.dat"); // open the file
  if (save_handle != 0) // if the file exists
  {
    file_str_read(save_handle, name1_str);
    slot1_txt.string = name1_str;
    slot1_txt.pos_x = 205 + (39 - str_len(name1_str)) * 5; // center the text, allow up to 39 characters, 5 = 10 / 2 (character width / 2)
    slot1_txt.visible = on;
    file_close(save_handle);
    save1_exists = 1;
  }
  else
  {
    save1_exists = 0;
 
}

  ...................................................
  save_handle = file_open_read("save4.dat"); // open the file
  if (save_handle != 0) // if the file exists
  {
   file_str_read(save_handle, name4_str);
   slot4_txt.string = name4_str;
   slot4_txt.pos_x = 205 + (39 - str_len(name4_str)) * 5; // center the text, allow up to 39 characters, 5 = 10 / 2 (character width / 2)
   slot4_txt.visible = on;
   file_close(save_handle);
   save4_exists = 1;
  }
  else
  {
   save4_exists = 0;
  }
}

Die Funktion load_game() wird jedes Mal aufgerufen, wenn der “Load”-Knopf im Hauptmenü angeklickt wird. Jeder Soundtrack wird gestoppt und das “Save Game” Panel wird verborgen; dafür wird das “Load Game” Panel angezeigt und die Daten aus den Dateien save1.dat bis save4.dat werden gelesen und in die Texte geschrieben. Wenn die erste Datei existiert, wird save1_exists auf 1 gesetzt, sonst auf 0 und so weiter. Wir werden diese Variable später verwenden; schließlich wollen wir vermeiden, dass der Spieler versuchen kann, ein Spiel eines Slots zu laden, der leer ist.

function ps_save1()
{
  if (inkey_active == on) {return;}
  if (players_shield <= 0) {return;}
  slot1_txt.visible = on;
  str_cpy(name1_str, "");
  while (str_cmpi(name1_str, "") == 1)
  {
    inkey(name1_str);
    wait (1);
  }
  slot1_txt.string = name1_str;
  slot1_txt.pos_x = 205 + (39 - str_len(name1_str)) * 5;
  save_handle = file_open_write("save1.dat");
  file_str_write (save_handle, name1_str);
  file_close(save_handle); // close the file
  game_save("save", 1, sv_all - sv_info);
  game_was_saved();
}

Diese Funktion läuft, wenn der Spieler den ersten Knopf auf dem “Save Game” Panel anklickt; Sie können mir glauben, dass die anderen Funktionen nicht viel anders aussehen. Die erste Zeile verläßt die Funktion, wenn inkey bereits aktiv ist (dann hat der Spieler bereits einen Knopf gedrückt). Die Funktion wird ebenfalls für ungültig erklärt, wenn der Spieler tot ist. Der Text, der den Namen des Spiels enthalten wird, wird angezeigt, name1_str wird gelöscht und dann warten wir in der Schleife bis der Spieler einen Namen eingegeben hat – diese Eingabe wird in name1_str gesichert.

Außerhalb der Schleife zentrieren wir den Text auf dem Bildschirm und erzeugen die save1.dat Datei (bzw. überschreiben sie) und schreiben den Namen hinein. Schließlich wird das Spiel mit der “game_save”-Anweisung der Engine gespeichert. Die letzte Zeile startet die game_was_saved() Funktion, die einige Panels verbirgt, Sandwiches macht, usw.

function ps_load1()
{
  if (save1_exists == 0) // the slot is empty?
  {
    return; // get out of here!
  }
  game_load("save", 1);
  game_was_loaded();
  if (level_number == 1)
  {
    media_play ("track1.wav", null, 50); // play the first soundtrack (3D space + logical level)
  }
  if (level_number == 3)
  {
    media_loop("track2.wav", null, 100); // play the second soundtrack (shooter leve)
  }
}

Die obige Funktion (und ihre drei Schwestern) wird aufgerufen, wenn ein Knopf auf dem “Load Game” Panel angeklickt wird. Sie prüft, ob das Spiel existiert; und falls dies der Fall ist, werden die Daten aus save1.sav geladen und die game_was_loaded() Funktion wird aufgerufen, die ebenfalls etwas aufräumt. Falls der Spielstand aus dem ersten Level kam, wird der erste Soundtrack gespielt, andernfalls, falls der Spielstand aus dem dritten (dem Shooter) Level stammte, wird der zweite Track gespielt. Warum machen wir das? Nun, die Sounds wurden gestoppt und können nicht so einfach and er Stelle wiederaufgenommen werden (solange wir keine kleineren Dateien aneinanderreihen und komplexeren Code dafür verwenden), also beginnen wir einfach von vorn, wenn wir im ersten oder dritten Level sind.

function game_was_saved()
{
  if (players_shield <= 0)
  {
    savegame_pan.visible = off;
    return;
  }
  else
  {
    sleep (2);
    slot1_txt.visible = off;
    slot2_txt.visible = off;
    slot3_txt.visible = off;
    slot4_txt.visible = off;
    savegame_pan.visible = off;
    blackpanel_pan.visible = off;
    main_pan.visible = off;
    if (level_number == 1)
    {
       if (human == null)
      {
        mouse_mode = 0;
        mouse_map = pointer1_tga;
      }
      else
     {
       mouse_mode = 2;
       mouse_map = logical_pcx;
      }
    }
    if (level_number == 2)
    {
      mouse_mode = 0;
    }
    if (level_number == 3)
    {
     mouse_mode = 0;
    }
    sleep (1);
    freeze_mode = 0;
  }
}

Diese Funktion läuft am Ende des Speicherprozesses und nimmt einige Änderungen vor, je nach der Levelzahl. Falls der Spieler tot ist, wird das “Save Game” Panel verborgen und die Funktion wird verlassen; andernfalls wartet sie 2 Sekunden und verbirgt dann den Text für die gespeicherten Spiele und das Panel. Wenn wir uns im ersten Level befinden und der “human”-Zeiger auf null steht, dann sind wir noch im “3D Weltraum”-Teil des ersten Levels, also wird der Mauszeiger verborgen (der Schiffscode wird ihn anzeigen) und wieder auf pointer1_tga zurückgesetzt. Falls “human” nicht null ist, sind wir bei dem Logikspiel, denn “human” ist der kleine Mensch, der aus dem Schiff steigt und der erzeugt wird, sobald das Schiff landet. In diesem Fall wird der Mauszeiger auf logical_pcx gesetzt.

Im zweiten Level wird kein Mauszeiger benötigt, im dritten ebensowenig. Wir geben dem Spieler eine Sekunde Zeit, um sich zu erinnern an welcher Stelle abgebrochen wurde und starten das Spiel erneut.

function cancel_loading()
{
  loadgame_pan.visible = off;
  slot1_txt.visible = off;
  slot2_txt.visible = off;
  slot3_txt.visible = off;
  slot4_txt.visible = off;
}

Diese Funktion wird gestartet, wenn der Spieler auf das “Load Game” Panel klickt. Ich habe auf das Panel geschrieben “Klicken Sie unten auf das Panel, wenn Sie unbeabsichtigt hier sind”, aber in Wahrheit können Sie irgendwo auf das Panel klicken (außer auf die Knöpfe!), wenn Sie im Hauptmenü aus Versehen auf “Load” geklickt haben. Sollte das der Fall sein, wird das Panel und die entsprechenden Texte verborgen.

function game_was_loaded()
{
  loadgame_pan.visible = off;
  savegame_pan.visible = off;
  slot1_txt.visible = off;
  slot2_txt.visible = off;
  slot3_txt.visible = off;
  slot4_txt.visible = off;
  main_pan.visible = off;
  sleep (1);
  freeze_mode = 0;
}

Die letzte Funktion räumt nach dem Laden eines gespeicherten Spieles auf; einige Panels und Texte werden verborgen, es wird eine Sekunde gewartet und das Spiel startet wieder.
Erlauben Sie mir die Serie mit einigen Ratschlägen zu beenden, die nützlich sein können, wenn Sie planen, ein großes Spiel zu schreiben:
- Vermeiden Sie “starter”-Funktionen; diese machen Ihnen am Ende nur das Leben schwer.
- Benutzen Sie so viele lokale Variablen wie möglich und vermeiden Sie globale Variablen nach Möglichkeit.
- Initialisieren Sie so viel wie möglich in der Main Funktion; wenn Sie nicht gerade eine seltsame, komplizierte Methode verwenden, wird diese ohnehin am Anfang jedes Spiels ausgeführt.

 

Springbrunnen

Dieser Springbrunnen verwendet Partikel, die nicht durch eine WMB-Entity oder einen Levelblock hindurch fallen und einen netten Nebeleffekt erzeugen, wenn sie auf Wasser treffen.

action fountain
{
    my.invisible = on;
    my.passable = on;
    while(1)
    {
       effect(particle_fountain, 15 * time, my.x, nullvector);
       wait(1);
    }
}

Weisen Sie die obige Action einem kleinen Model zu; dieses wird unsichtbar und passierbar gemacht und generiert dann Partikel mit Hilfe der particle_fountain Funktion. Die Anzahl der erzeugten Partikel ist an die Framerate angepaßt.

function particle_fountain()
{
    temp.x = random(6) - 3;
    temp.y = random(6) - 3;
    temp.z = random(10) + 10; // play with this value
    vec_set(my.vel_x,temp);
    my.bmap = drop_tga;
    my.size = 1.5;
    my.flare = on;
    my.bright = on;
    my.move = on;
    my.streak = on; // if your engine version supports it
    my.gravity = 2;
    my.alpha = 80;
    my.lifespan = 50;
    my.function = fade_particles;
}

Die Partikel haben eine zufällige Geschwindigkeit in x und y-Richtung (zwischen –3 und 3) und eine zufällige positive Geschwindigkeit in z-Richtung (zwischen 10 und 20). Beachten Sie, dass ich das “streak”-Flag verwende, wodurch die Partikel viel besser aussehen, aber eventuell nicht in Ihrer Version der Engine verfügbar ist; verwenden Sie gegebenenfalls eine andere Bitmap für das Partikel. Die Partikel haben eine gravity von 2 und eine lifespan von 50; die Funktion, die sie lenkt heißt fade_particles.

function fade_particles()
{
    my.alpha -= 2 * time;
    if(content(my.x) == content_solid) // the particle is inside a solid?
    {
       effect(fountain_mist, 1, my.x, nullvector); // generate mist
       my.lifespan = 0; // don't allow the particles to continue their movement if they have hit something solid
    }
    if(my.alpha < 0)
    {
       my.lifespan = 0;
    }
}

Die erste Codezeile macht die Partikel transparenter. Wenn ein Partikel ein Frame in einem “soliden” Block (also in einer WMB-Entity oder einem Levelblock) beendet, wird ein Nebelpartikel mit der fountain_mist Funktion erzeugt und das Partikel verschwindet. Für bessere Resultate sollten Sie einen Brunnen mit dicken Wänden benutzen, weil die Wassertropfen sehr schnell sind und durch die Wände rutschen könnten, wenn diese zu dünn sind. Die letzten Zeilen zerstören das Partikel, wenn sein Transparenzfaktor unter 0 sinkt.

function fountain_mist()
{
    temp.x = random(2) - 1;
    temp.y = random(2) - 1;
    temp.z = random(1) + 1; // play with this value
    vec_set(my.vel_x,temp);
    my.bmap = smoke123_tga;
    my.size = 5;
    my.lifespan = 50;
    my.flare = on;
    my.bright = on;
    my.move = on;
    my.gravity = 0;
    my.alpha = 70;
    my.function = fade_mist;
}

Der Nebel hat eine zufällige Geschwindigkeit in x und y-Richtung (zwischen –1 und 1) und eine positive Geschwindigkeit in z-Richtung (zwischen 1 und 2). Die entsprechende Funktion heißt fade_mist:

function fade_mist()
{
    my.alpha -= 3 * time;
    my.size += 0.5 * time;
    if(my.alpha < 0)
    {
       my.lifespan = 0;
    }
}

Diese Funktion verringert die Transparenz des Nebels und vergrößert ihn dabei. Sobald der Alpha unter 0 sinkt, wird das Partikel entfernt. Sie können mit dem Wert von temp.z in der fountain_mist() Funktion spielen, ebenso mit dem alpha und der Größenänderung in fade_mist(), bis Sie einen Nebel bekommen, der Ihnen gefällt. Ich bin kein Fan von exzessivem Partikelgebrauch, aber hier ist ein Beispiel, das man mit leicht abgewandelten Werten erhält.