Darts

Wenn Ihre Hände nicht zittern, könnten Sie eine ganz gute Punktzahl erreichen, aber falls Sie sie nicht kontrollieren können, werden Sie mit Sicherheit den Highscore erzielen! Starten Sie das Projekt und sehen Sie selbst... als erstes schauen wir uns die Main Funktion an - sie tut eine Menge Dinge:

function main()
{
     camera.arc = 60;
     fps_max = 30;
     level_load (board_wmb);
     wait (2);
     mouse_map = mouse_pcx; // 2d dart picture
     mouse_mode = 2;
     mouse_pos.x = 372;
     mouse_pos.y = 272;

Wir setzen das Sichtfeld auf 60 Grad, begrenzen die Frame Rate auf 30 fps und laden das Level. Wir werden mouse_pcx als Mauszeiger nehmen (ein Bild eines Darts von hinten) und plazieren den Mauszeiger in die Mitte des Bildschirms; wir ziehen dabei jeweils 28 Pixel von x und y Richtung ab, da die Bitmap eine Größe von 56 x 56 Pixeln hat.

     while (1)
     {
          if (key_t == 1)
          {
               mouse_pos.x = pointer.x;
               mouse_pos.y = pointer.y;
          }
          else
          {
               counter += 1;
               if (counter == 1)
               {
                    random_x = 20 - int(random(40));
                    random_y = 20 - int(random(40));
               }
               if (mouse_pos.x < pointer.x + random_x) {mouse_pos.x += 1;}
               if (mouse_pos.x > pointer.x + random_x) {mouse_pos.x -= 1;}
               if (mouse_pos.y < pointer.y + random_y) {mouse_pos.y += 1;}
               if (mouse_pos.y > pointer.y + random_y) {mouse_pos.y -= 1;}
               if ((mouse_pos.x == pointer.x + random_x) && (mouse_pos.y == pointer.y + random_y))
               {
                    counter = 0;
               }
          }

Wenn wir die "T" Taste drücken und halten, kann der Mauszeiger ohne Mühe bewegt werden. Dieser Modus sollte allerdings nur für Kalibrierungszwecke genutzt werden, weil es wenig Spaß macht, den Mauszeiger einfach in die Mitte der Dartscheibe zu ziehen und fünf Mal ins Schwarze zu werfen. Wir müssen das Spiel etwas schwerer gestalten - und dies tut der Rest des Codes für uns.

Ich benutze eine einfache Variable namens "counter"; sie startet bei 0, aber das Kommando "counter += 1;" addiert etwas hinzu. Falls "counter == 1" generieren wir ein zufälliges Ziel +-20 Pixel von der gegenwärtigen Mausposition entfernt. Die Maus wird sich auf dieses zufällig gewählte Ziel zu bewegen und wenn es erreicht wurde, wird ein neues gesetzt und so weiter. Dies erzeugt sauberes, elegantes Zittern des Mauszeigers (bzw. Dartpfeils) auf dem Bildschirm und macht das Spiel schwerer und (natürlich) spannender. Counter wird zurückgesetzt, wenn der Mauszeiger das zufällige Ziel erreicht hat.

          if (mouse_left == 1 && number_of_darts < 5)
          {
               while (mouse_left == 1) {wait (1);}
               dart_coords.x = mouse_pos.x + 28;
               dart_coords.y = mouse_pos.y + 28;
               dart_coords.z = 100;
               vec_for_screen(dart_coords, camera);
               ent_create(dart_mdl, dart_coords, move_dart);
          }
          wait (1);
     }
}

Wenn die linke Maustaste gedrückt wird und noch nicht alle fünf Pfeile geworfen wurden, warten wir ab, bis die linke Maustaste losgelassen wird (kein Autofeuer!) und setzen eine Variable (dart_coords) auf die Koordinaten des Mauszeigers. Die Bitmap für den Mauszeiger ist 56 x 56 Pixel groß, also müssen jeweils 28 Pixel in x und y Richtung addiert werden, um die korrekten Koordinaten zu erhalten. Der 3D Dart (das eigentliche Model eines Dartpfeils) wird 100 Quants vor den Schirm plaziert und vec_for_screen erfüllt diesen Wunsch: es errechnet aus einer 2D Position auf dem Bildschirm die 3D Koordinaten der "echten" Welt. Die letzte Zeile erzeugt den Dartpfeil an dieser Position (die Position, die der Mauszeiger angibt und 100 Quants nach vorn - für die, die später zugeschaltet haben).

Nun da wir einen richtigen Pfeil in der Welt haben, was sollen wir damit tun? Schauen wir uns die Funktion move_dart an:

function move_dart()
{
     wait (1);
     mouse_map = null;
     my.enable_entity = on;
     my.enable_block = on;
     my.event = stop_dart;
     my.skill1 = 0;

Der Mauszeiger verschwindet (wir haben einen echten 3D Dart, also ist der Mauszeiger "geworfen"); der Pfeil selbst reagiert auf andere Entities und Level Blocks. Falls der Pfeil mit einer anderen Entity oder dem Level kollidiert, wird sein stop_dart Event ausgelöst. Ich setze skill1 für den Dart zurück, aber wenn er etwas trifft, wird skill1 auf 1 gesetzt.

Zeit, die Richtung zu erraten! Sie könnten glauben, dass ein Dartpfeil einfach nach vorne fliegt, aber diese gefährlichen Teile aus woraus auch immer sie bestehen müssen anders behandelt werden. Warum, fragen Sie? Stellen Sie sich folgendes Szenario vor: Sie sitzen an Ihrem Computer, trinken Cola und spielen dieses Dart Spiel. Es ist spät und Sie haben stundenlang gespielt und trotzdem erhalten Sie jedes Mal 250 von 250 Punkten. Warum? Falls Sie die Mitte um einige Pixel verfehlen - sagen wir um 3 Pixel - wird der Pfeil 3 Pixel von der Mitte der Dartscheibe entfernt ankommen, was an der Punktzahl nichts ändert, weil die Entity für die Mitte einen Durchmesser von 8 Pixeln hat. Sie müßten schon 80 Pixel danebenliegen, um die Scheibe komplett zu verfehlen - und das wird nicht geschehen.

Das ist das Problem - sehen wir uns die Lösung an. Der Pfeil fliegt wegen seiner dart_speed.x Komponente auf die Scheibe zu. Wenn die Mitte verfehlt wird, ändern wir die Geschwindigkeit in y und z Richtung abhängig vom Abstand zwischen der Mitte (perfekter Treffer) und dem Ziel. Das Bild sollte das noch besser erklären:
 


Das ist noch etwas unklar und verschwommen... ich werde einfach camera.fog = 0 in die Konsole tippen, damit wir weitermachen können (Acknex Humor). Die schlechte Version ist schlecht, weil selbst wenn der Spieler die Mitte der Scheibe verfehlt, der Pfeil trotzdem noch treffen kann. Die gute Version prüft, ob der Spieler die Mitte verfehlt hat und wenn ja, ändert sie die Geschwindigkeit des Pfeils in y und z Richtung, um den Pfeil von der Mitte des Schirms wegzubewegen.

     dart_speed.x = 100;
     dart_speed.y = (372 - mouse_pos.x) / 15;
     dart_speed.z = (272 - mouse_pos.y) / 15;
     dart_speed *= time;
     while (my.skill1 == 0)
     {
          move_mode = ignore_passable; // ignore passable entities
          ent_move (nullvector, dart_speed);
          wait (1);
     }
     // the dart has hit something here
     mouse_map = mouse_pcx;
}

Habe ich erwähnt, dass dart_speed.x die Hauptkomponente für die Geschwindigkeit des Pfeils ist? Dieser Wert bewegt den Pfeil auf die Scheibe zu. Mit den anderen beiden dart_speed Komponenten sieht es anders aus: dart_speed.y bewegt den Pfeil nach links oder rechts, während dart_speed.z den Pfeil nach oben oder unten bewegt. Wenn die Mitte des Mauszeigers in der Mitte des Bildschirms ist (400,300), wird die obere linke Ecke bei (372, 272) sein, da wir in jeder Richtung 28 abziehen müssen. Die Werte für dart_speed.y und dart_speed.z werden errechnet, indem die "echte" Mausposition von dieser virtuellen Bildschirmmitte bei (372, 272) abgezogen wird; der erhaltene Wert wird durch eine Zahl (15) dividiert, die zur Kalibrierung genutzt werden kann.

Erinnern Sie sich daran, dass die Maus aufhört zu zittern, wenn wir "T" drücken? Dies erlaubt es uns, das Spiel zu kalibrieren; drücken und halten Sie "T", schießen Sie einige Pfeile wie durch die roten Kreise im Bild unten gezeigt und sie sollten außen an der Scheibe auftreffen; treffen sie zu nah in die Mitte oder verfehlen das Brett ganz, ändern Sie den wert 15, bis alles funktioniert. Bedenken Sie, wenn Sie dart_speed.x abändern, müssen Sie erneut kalibrieren.

Dart_speed wird mit time multipliziert, um die Geschwindigkeit bei unterschiedlichen Frameraten konstant zu halten; kann Ihr PC die 30 fps nicht leisten wird Ihnen das sehr helfen. Der Dart wird mit Hilfe der While Schleife bewegt bis er auf etwas trifft; wenn dies geschieht, zeigen wir den Mauszeiger wieder.

Was aber geschieht, wenn er etwas trifft? Sehen wir uns die Funktion unten an:

function stop_dart()
{
     my.skill1 = 1;
     snd_play (hit_snd, 50, 0);
     my.passable = on;
     number_of_darts += 1;
     while (number_of_darts < 5) {wait (1);}
     while (camera.arc > 20) // zoom in
     {
          camera.arc -= 0.2 * time;
          wait (1);
     }
     while (key_any == 0) {wait (1);}
     ent_remove (me);
     while (camera.arc < 60) // zoom out
     {
          camera.arc += 0.2 * time;
          wait (1);
     }
     number_of_darts = 0;
     score = 0;
}

Als erstes setzen wir skill1 auf 1; dies stoppt die Bewegung in der Funktion mode_dart(). Wir spielen einen Sound und machen den Pfeil passierbar, weil ein anderer Pfeil an derselben Stelle treffen könnte. Wir erhöhen die Anzahl der Pfeile und warten bis alle 5 geworfen wurden. Warum sollten wir so etwas tun? Dies macht es leichter den Code zu schreiben, der das Spiel neu startet. Sind alle fünf Pfeile abgeschossen, zoomen wir heran, mit fünf Funktionen die zugleich den camera.arc verringern. Wir sind nun nah am Brett und können sie bewundern, was wir getroffen haben; sobald eine Taste gedrückt wird, werden die alten Pfeile entfernt, die Kamera zoomt wieder zurück, die Anzahl der Pfeile und die Punkte werden zurückgesetzt.

Ich habe Ihnen gesagt, nicht auf die Punktetafel zu schießen, richtig? Ok, tun Sie es und sehen Sie, was geschieht:

action score_board
{
     my.oriented = on;
     my.enable_impact = on;
     my.event = fall;
}

function fall()
{
     while (my.z > -500)
     {
          my.z -= 50 * time;
          wait (1);
     }
     snd_play (fall_snd, 50, 0);
}

Ich benutze einfach ein orientiertes Sprite, aber es reagiert auf impact; wird es von einem Pfeil getroffen, wird die Funktion fall() ausgelöst.
Die Punktetafel verringert ihre Höhe, bis es einen z-Wert kleiner als -500 erreicht. Der Spieler wird einen Sound hören, wenn die Tafel aufhört, zu fallen und wird denken, dass es auf den Boden gefallen ist... was Ok ist, da der Spieler den Boden nicht sehen kann. :)

Zeit für den Code für die Dartscheibe - der ist wirklich leicht:

action got_50
{
     my.skill1 = 50;
     my.enable_impact = on;
     my.event = compute_score;
}

action got_25
{
     my.skill1 = 25;
     my.enable_impact = on;
     my.event = compute_score;
}

action got_10
{
     my.skill1 = 10;
     my.enable_impact = on;
     my.event = compute_score;
}

function compute_score()
{
     score += my.skill1;
}

Diese Actions gehören zu den Entities, die über den großen Dartkreis gelegt werden. Die Punktzahl für jede Entity wird in skill1 gespeichert; trifft einer der Pfeile, wird dieser Wert auf die Gesamtpunktzahl addiert.
 
 

 
Robby der Roboter

Wer ist Robby? Er ist Ihr persönlicher Roboter! Er ist ein NPC, aber er kann zur Atmosphäre Ihres Spiels beisteuern. Was tut er? Nicht viel, aber das macht er gut. Er wartet einige Sekunden und läuft dann einige Sekunden... habe ich erwähnt, dass er jedes Hindernis spüren und vermeiden kann und darüberhinaus nicht feststecken kann?

Laden und starten Sie das Demo Level jetzt. Ich würde sagen, dass der Code recht beeindruckend ist, wenn man bedenkt, dass er nur aus einer Action und zwei winzigen Funktionen besteht, aber wer glaubt mir schon? Sehen wir uns die Action an, die zu Robby gehört:

action robby_the_robot
{
     my.enable_entity = on;
     my.enable_impact = on;
     my.enable_block = on;
     my.event = robby_event;
     ent_create(antenna_pcx, nullvector, check_front);

Der Roboter reagiert auf andere Entities, Impact und Level Blocks; tritt eines dieser Dinge ein, wird robot_event aufgerufen. Wir erstellen eine virtuelle "Antenne" - ich habe zwei ähnliche Antennen für die erste KI Demo in Aum4 benutzt.

     while(1)
     {
          standing_time = 3 + random(7);
          while (standing_time > 0)
          {
               standing_time -= time / 16;
               ent_cycle("stand", my.skill20); 
               my.skill20 += 2 * time;
               my.skill20 %= 100; // loop
               wait (1);
          }
 
Robby benutzt eine While(1) Schleife, also wird er den ganzen Tag mit dem weitermachen, was er tut. Wir definieren standing_time = 3 ... 10 Sekunden - dies ist die Zeit, die Robby verbringt, während er seine "Stand" Animarion abspielt. Haben Sie bemerkt, dass wir "time / 16" von standing_time pro Frame subtrahieren? Auf diese Weise wird pro Sekunde genau eine Sekunde von standing_time abgezogen. Das ist sehr nützlich, da wir mit "richtigen" Werten rechnen können, wir müssen nicht errechnen, wieviele ticks das wären und so weiter.

          walking_time = 10 + random(10); 
          walk_speed.x = 1;
          walk_speed.y = 0;
          walk_speed.z = 0;
          walk_speed *= time; 
          while (walking_time > 0)
          {
               if (content(front_coords) == content_solid)
               {
                    my.skill40 = my.pan;
                    my.skill41 = 30 + random(90);
                    while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
                    {
                         my.pan += 3 * time;
                         wait (1);
                    }
               }
 
Der Roboter ist fertig mit seiner ersten While Schleife, die für "stand" benutzt wurde, nun ist es Zeit für 10 ... 20 Sekunden zu laufen - das ist der Wert, auf den walking_time gesetzt wird und die Geschwindigkeit wird durch walk_speed gegeben. Wir setzen nur die x Koordinate, der Roboter wird sich also in die Richtung bewegen, die sein momentaner pan angibt. Erinnern Sie sich an die Antenne? Es ist das rote Sprite in der Demo, aber Sie können den Kommentar aus der WDL Datei entfernen und es wird verschwinden. Die Antenne wird 30 Quants vor den Roboter gesetzt (glauben Sie es mir im Moment, wie werden uns die Funktion gleich ansehen). Wenn die Antenne content_solid bemerkt (Wände, Map Entities, etc.), speichert der Roboter seinen momentanen pan in Skill40 und ändert diesen Wert, indem er einen zufälligen Wert (30...120) addiert.

               else
               {
                    walking_time -= time / 16;
                    ent_cycle("walk", my.skill20); 
                    my.skill20 += 4 * time;
                    my.skill20 %= 100; // loop
                    move_mode = ignore_passable; 
                    result = ent_move (walk_speed, nullvector);
                    if (result == 0) // got stuck!
                    {
                         walk_speed.x *= -1; // reversed movement
                         my.skill40 = my.pan;
                         my.skill41 = 30 + random(90);
                         while (my.pan < my.skill40 + my.skill41) // rotate 30..120 degrees
                         {
                              my.pan += 3 * time;
                              ent_cycle("walk", my.skill20); // play reversed "walk" animation
                              my.skill20 -= 4 * time;
                              my.skill20 %= 100; // loop
                              move_mode = ignore_passable; // ignore passable entities
                              ent_move (walk_speed, nullvector);
                              wait (1);
                         }
                         walk_speed.x *= -1; // restore walk_speed
                    }
               }
               wait (1);
          }
          wait (1);
     }
}
 
Wenn die Antenne keinen content_solid gefunden hat, sollte der Roboter sich nach vorn bewegen können, also spielt er seine "walk" Animation in einer Schleife und bewegt sich. Sehen Sie sich die folgende Zeile genau an:

                   result = ent_move (walk_speed, nullvector);

Sieht "normal" aus, oder? Die gute Nachricht ist, dass result die Distanz enthält, die Robby sich bewegt hat, also falls result gleich 0 ist, bedeutet das, Robby steckt fest und wir sollten etwas unternehmen. Also, was tun wir, wenn das geschieht? Wir drehen walk_speed um, indem wir den Wert mit -1 multiplizieren, ändern seinen pan um einen Zufallswert (30...120 Grad), spielen eine umgedrehte "walk" Animation (beachten Sie die Zeile mit -= 4 * time) und bewegen den Roboter rückwärts. Wenn result nicht 0 ist (Robby steckt nicht mehr fest), multiplizieren wir walk_speed wieder mit -1, um den alten Wert wiederherzustellen.

Nun ist es Zeit sich die zwei winzigen Funktionen anzusehen, von denen ich sprach:
 
function check_front()
{
     my.passable = on;
     my.transparent = on;
     while (1)
     {
          my.x = you.x + 30 * cos(you.pan); // the antenna is placed
          my.y = you.y + 30 * sin(you.pan); // 30 quants in front of the player
          my.z = you.z;
          vec_set (front_coords, my.pos);
          wait (1);
     }
}

Die Funktion check_front gehört zu der "Antenne"; sie macht das rote Sprite passable und transparent und setzt es die ganze Zeit über 30 Quants vor den Roboter. Aber diese Funktion macht noch etwas Wichtiges: sie setzt die Variable front_coords auf die Koordinaten des roten Sprites. Wenn Sie eben aufgepaßt haben, wissen Sie, dass wir den Inhalt von front_coords prüfen, um festzustellen, ob der Weg frei ist oder nicht.

Vielleicht fragen Sie sich: wenn wir diese tolle Antenne haben, warum die Mühe mit result == 0 und so weiter? Nun ja... das Leben ist nicht immer fair. Robby könnte in eine Situation wie in dem Bild unten geraten:
 

Wie Sie sehen denkt die Antenne, dass alles in Ordnung ist, aber Robby steckt an der Ecke fest.

function robby_event()
{
     if (event_type == event_entity)
     {
          snd_play (sorry_wav, 50, 0);
     }
}

Die letzte Funktion sagt Robby, ob er mit einer Entity zusammengestoßen ist oder nicht. Ich dachte, es wäre nett, wenn Robby "Sorry!" sagt, falls er in den Spieler läuft. Dieses simple event spielt einen Sound; der Code, der dafür sorgt, dass Robby andere Entities (inkl. Dem Spieler) vermeidet basiert auf result == 0 und wurde bereits besprochen.

Ich hoffe, dass Ihnen dieses Projekt Spaß gemacht hat - mir schon.