Stratego 2

Wenn Sie Stratego aus AUM 2 mochten, dann werden Sie Stratego 2 lieben, weil es folgende neue Features enthält:
- Echte 3D - Levels
- mit der Maus auswählbare Einheiten
- Mehrere gleichzeitig ausgewählte Einheiten (man zieht einen Rahmen um die Einheiten)
- Verschiedene Sounds für jede Einheit

Vorrausgesetzt wird, dass Sie sich mit dem Code von Stratego auskennen, sodass nicht geklärt werden muss, wie man Gebäude, Einheiten usw. erstellt. All dies ist in AUM 2 erklärt, hier konzentrieren wir uns nur auf das Wesentliche.

Stratego 2 ist ein Standalone-Projekt, sodass es seine eigene Hauptfunktion besitzt:

function main()
{
     level_load (stratego2_wmb);
     wait (2); // wait for the level to be loaded
     clip_size = 0; // show all the triangles for all the models
     fps_max = 40; // lock the frame rate
     camera.x = 0;
     camera.y = 0;
     camera.z = camera_height;
     camera.tilt = -90;
     camera.pan += 90;
     mouse_mode = 2;
     mouse_map = pointer_map;
     mouse_pos.x = screen_size.x / 2;
     mouse_pos.y = screen_size.y / 2;
     move_camera();
     multiple_selection();
     while (index < 9)
     {
          temp.x = start_pos[index];
          temp.y = start_pos[index + 1];
          temp.z = start_pos[index + 2];
          ent_create (guard_mdl, temp, init_unit);
          index += 3;
     }
}

Die Kamera schaut deshalb runter, weil der tilt = -90; ist (Vogelperspektive) und der pan ist deshalb mit 90° gesetzt, weil die Kamera somit in die richtige, gewünschte Richtung zeigt. Der Mauszeiger ist sichtbar und im Zentrum des Bildschirmes platziert und dann werden noch ein paar Funktionen initialisiert, über die wir noch reden werden. Drei Einheiten (guard.mdl) werden erstellt; die Startpositionen bei Spielbeginn werden hier gelesen:

var start_pos[9] = 200, 200, 500, -200, 200, 500, 0, -200, 500;

Der erste Guard ist bei xyz = 200, 200, 500, der zweite bei -200, 200, 500 (usw.) positioniert. Jeder Guard ruft die Funktion init_unit(); auf:

function init_unit()
{
     wait (1);
     my.enable_click = on;
     my.enable_entity = on;
     my.enable_block = on;
     my.event = move_unit;
     vec_set (temp, my.pos);
     temp.z -= 3000;
     trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
     my.z -= trace (my.pos, temp); // place the unit on the ground (it has been spawned in the air)
     my.target_x = 100000;
     while (1)
     {
          while (my.target_x < 50000) {wait (1);}
          if ((my.x > upleft_coords.x) && (my.x < upright_coords.x) && (my.y < upleft_coords.y) && (my.y > downleft_coords.y) && (my.unit_selected == 0))
          {
               my.unit_selected = 1;
               move_unit();
          }
          else
          {
               if (my.unit_selected == 1)
               {
                    vec_set (upleft_coords, nullvector); // reset selection coords
                    vec_set (upright_coords, nullvector);
                    vec_set (downleft_coords, nullvector);
                    vec_set (downright_coords, nullvector);
               }
          }
          wait (1);
     }
}
 
Die Einheit reagiert auf die Maus, auf Kollisionen in der Level-Geometrie und mit anderen Objekten. Der Guard wird in der Luft erstellt (der Z Wert war im Array auf 500 gestellt, nicht?), sodass er nur noch auf den Boden bewegt werden muss. Wir ziehen dafür das Ergebnis eines 3000 Quants - Trace (abwärts!) vom ursprünglichen Z-Wert ab. My.target_x = 100000 zeigt nur an, dass die Einheit kein Ziel hat; der Wert 100000 ist einfach nur ein Indikator, der dies anzeigt („außer Reichweite”).

Sie finden in Stratego 2 viel Code bezüglich my.target_x, my_unit_selected und weiteren, aber kein Grund zum Verzweifeln: dies sind Skills, die am Anfang der stratego.wdl definiert sind: z.B. ist my.target_x gleich skill12, es sieht halt besser aus.

Jede Einheit hat eine while(1) Schleife, die auch weiter läuft, wenn die Einheit etwas anderes macht; wenn der target_x einer Einheit größer als 50000 ist (100000 bedeutet „kein Ziel”, nicht?) und sie ist ausgewählt (nur sie oder mit anderen) durch ziehen eines Rahmens, dann wird die Einheit ausgewählt (my.unit_selected = 1) und sie bewegt sich in Richtung des Ziels, was jetzt ausgewählt ist. Die Koordinaten für das Auswählen werden dann zurückgesetzt, wenn die Einheit markiert wurde. Wenden wir uns nun etwas diesen Koordinaten zu:

function multiple_selection()
{
     while (1)
     {
          if (mouse_left == 1)
          {
               upper_left.visible = on;
               upper_right.visible = on;
               lower_left.visible = on;
               lower_right.visible = on;
               if (first_click == 0) // make sure that this "if" branch is executed only once
               {
                    first_click = 1;
                    upper_left.pos_x = pointer.x; // store panel's position
                    upper_left.pos_y = pointer.y;
               }
               lower_right.pos_x = pointer.x; // store panel's position
               lower_right.pos_y = pointer.y;

               lower_left.pos_x = upper_left.pos_x; // store panel's position
               lower_left.pos_y = lower_right.pos_y;

               upper_right.pos_x = lower_right.pos_x; // store panel's position
               upper_right.pos_y = upper_left.pos_y;

          }
          else // finished multiple selection
          {
               upper_left.visible = off;
               upper_right.visible = off;
               lower_left.visible = off;
               lower_right.visible = off;
               first_click = 0;

               upleft_coords.x = upper_left.pos_x; // project upper_left's panel coords on the map
               upleft_coords.y = upper_left.pos_y;
               upleft_coords.z = camera_height;
               vec_for_screen (upleft_coords, camera);

               upright_coords.x = upper_right.pos_x; // project upper_left's panel coords on the map
               upright_coords.y = upper_right.pos_y;
               upright_coords.z = camera_height;
               vec_for_screen (upright_coords, camera);

               downleft_coords.x = lower_left.pos_x; // project upper_left's panel coords on the map
               downleft_coords.y = lower_left.pos_y;
               downleft_coords.z = camera_height;
               vec_for_screen (downleft_coords, camera);

               downright_coords.x = lower_right.pos_x; // project upper_left's panel coords on the map
               downright_coords.y = lower_right.pos_y;
               downright_coords.z = camera_height;
               vec_for_screen (downright_coords, camera);
          }
          wait (1);
     }
}

Die Funktion multiple_selection() setzt die Koordinaten, die gebraucht werden, damit jede Einheit erkennt, ob sie ausgewählt ist oder nicht, indem ein Rahmen erstellt wird.


Wenn man auf die Karte klickt (mouse_left = 1) erscheinen vier panels (upper_left, upper_right, lower_left, lower_right). Diese da sind die kleinen grünen auf dem Bild unten. Das erste Panel (upper_left) erscheint an der Stelle des Mauszeigers und bleibt dort; die übrigen positionieren sich je nach Maus - Bewegung um einen Rahmen beliebiger Breite/Höhe zu zeichnen. Dafür benutzen wir ein „entity”-panel (quadratisch) und ändern dessen Ausmaße über scale_x und scale_y.

Wenn die Maustasten nicht mehr aktiv sind und der Rahmen gezeichnet wurde, setzt der Code bei dem „else” fort: die Panels verschwinden und die Koordinaten der vier Ecken werden in 3D-Vektoren mithilfe von vec_for_screen, wie im Vild zu sehen, umgesetzt.

Hier haben wir einen virtuellen Rahmen erstellt, der die gleiche Größe wie der auf dem Bildschirm hat; der Code in init_unit prüft, ob sich die zugehörige Einheit in dem Rahmen befindet:

if ((my.x > upleft_coords.x) && (my.x < upright_coords.x) && (my.y < upleft_coords.y) && (my.y > downleft_coords.y) && (my.unit_selected == 0))
{
..............
}
 


In meinem Beispiel sind alle Einheiten in diesem Rahmen platziert, sodass alle markiert sind.

Hier ist nun die Funktion move_unit():

function move_unit()
{
     if (event_type == event_block || event_type == event_entity)  // collision with other entities or level geometry
     {
          if (abs(my.x - my.target_x) + abs(my.y - my.target_y) < 100) // close to the target but other entities are already there
          {
               my.target_reached = 1; // the unit has reached the target
               my.unit_selected = 0;
               my.target_x += 100000; // move the target far away to get out of this "if" branch
          }
          else
          {
               // caveman's path finding code - read more about it in Aum2
               my.pan += 90 - random(180);
               waitt (10); // wait a little
               temp.x = my.target_x; // stored mouse pointer coordinates
               temp.y = my.target_y;
               temp.z = 0;
               vec_sub (temp, my.x);
               vec_to_angle (my.pan, temp); // rotate unit towards the target again
               my.tilt = 0; // stand tall :)
          }
     }
     if (event_type == event_click || my.unit_selected == 1)
     {
          my.destination = 0;
          my.target_reached = 0; // target not available yet
          ent_create (selected_pcx, my.pos, selected_unit);
          my.selected_handle = snd_play (selected_snd, 70, 0);
          snd_tune (my.selected_handle, 70, 80 + random(20), 0); // different "huh?" voices
          while (mouse_left == 1) {wait(1);} // wait for the mouse release
          while (my.destination == 0) // as long as the target hasn't been set
          {
               if (mouse_left == 1)
               {
                    mouse_map = pointerhigh_map;
                    my.ok_handle = snd_play (ok_snd, 70, 0); // store the sound handle in a skill, no need to use a separate var
                    snd_tune (my.ok_handle, 70, 80 + random(50), 0); // different "ok" voices
                    waitt (4); // show the cross for 0.25 seconds
                    mouse_map = pointer_map;
                    my.destination = 1;
                    temp.x = mouse_pos.x;
                    temp.y = mouse_pos.y;
                    temp.z = camera_height;
                    vec_for_screen (temp, camera); // temp holds pointer's coords now
                    my.target_x = temp.x; // store pointer's coords before they get lost
                    my.target_y = temp.y;
                    vec_sub (temp, my.x);
                    vec_to_angle (my.pan, temp); // rotate the unit towards the target
                    my.tilt = 0; // we only need the correct "pan" angle, not tilt
                    my.fuel = 300; // maximum path length (fuel)
                    while (abs(my.x - my.target_x) + abs(my.y - my.target_y) > 3 && my.fuel > 0 && my.target_reached != 1) // stop near the target
                    {
                         my.unit_selected = 0;
                         vec_set (temp, my.x);
                         temp.z -= 3000;
                         trace_mode = ignore_me + ignore_sprites + ignore_models + use_box;
                         unit_speed.z = -trace (my.x, temp);

                         move_mode = ignore_you + ignore_passable;
                         ent_move (unit_speed, nullvector);
                         ent_cycle("walk", my.animation_frame); // play walk frames animation
                         my.animation_frame += 5 * time; // "walk" animation speed
                         if (my.animation_frame > 100) {my.animation_frame = 0;} // loop animation
                         my.fuel -= time; // burn fuel
                         wait (1);
                    }
                    my.target_reached = 1; // the unit has reached the target
                    my.target_x += 100000;
                    ent_cycle("stand", 0); // the unit stands still now
               }
              wait (1);
          }
     }
}

Diese Funktion wird aufgerufen, wenn auf die Einheit geklickt wurde (event_click ausgelöst) oder wenn my.unit_selected =1 ist (Rahmen-Auswahl). Wenn etwas davon passiert, wird ein grüner Sprite erstellt, der die Einheit markiert, ein Sound wie „ja?” wird mit einer zufälligen Frequenz gespielt (deshalb die Benutzung von snd_tune und random) und dann wird bis zum loslassen der linken Maustaste gewartet. Jetzt wird das Ziel angegeben: wenn wir irgendwo auf die Karte klicken, wechselt der Pointer nach pointerhigh_map (ein Kreuz), ein „ok!” Sound ertönt, und der Mauszeiger ist auf normal gesetzt. Eine weiterer vec_for_screen Befehl setzt die Maus Koordinaten in eine Levelposition um; dieser Vektor ist das Ziel der Einheiten! Die Guards drehen sich Richtung Ziel und bewegen sich solange dorthin bis die Distanz dahin < 3 ist. Wenn das Ziel unerreichbar für eine Einheit ist, stoppt sie nach einer bestimmten Zeit.

Hier wird nun die Benutzung von my.fuel erklärt: die Einheit startet mit fuel = 300 und verbrennt das Benzin während sie sich durch den Level bewegt. Wenn fuel < 0 ist, stoppt die Einheit abprupt.

Es wird weiterhin Trace benutzt, da die Einheiten die ganze Zeit auf dem Boden bleiben sollen. Die Einheiten tracen nur, wenn sie sich bewegen. Wenn die Einheit das Ziel erreicht hat, oder fuel < 0 ist, wird der target_x der Einheit auf eine Variable von 100000 gesetzt, was heißt, dass die Einheit kein Ziel hat, und das Flag target_reached wird gesetzt.

     if (event_type == event_block || event_type == event_entity)  // collision with other entities or level geometry
     {
          if (abs(my.x - my.target_x) + abs(my.y - my.target_y) < 100) // close to the target but other entities are already there
          {
               my.target_reached = 1; // the unit has reached the target
               my.unit_selected = 0;
               my.target_x += 100000; // move the target far away to get out of this "if" branch
          }
          else
          {
               // caveman's path finding code - read more about it in Aum2
               my.pan += 90 - random(180);
               waitt (10); // wait a little
               temp.x = my.target_x; // stored mouse pointer coordinates
               temp.y = my.target_y;
               temp.z = 0;
               vec_sub (temp, my.x);
               vec_to_angle (my.pan, temp); // rotate unit towards the target again
               my.tilt = 0; // stand tall :)
          }
     }

Die Zeilen im Code der Funktion move_unit sind sehr nützlich, da sie auf eine Kollision mit anderen Einheiten oder des Levels reagieren. Wenn das Ziel sehr nahe ist und es eine Kollision gibt, ist es nahezu sicher, dass eine andere Einheit das Ziel schon erreicht hat; alle Einheiten haben dasselbe Ziel. Wenn nun eine zweite, dritte, etc. Einheit in der Nähe des Ziels ist und etwas berührt (z.B. eine andere Einheit!), dann bleibt sie stehen. Wenn die Einheit kollidiert ist, aber weit weg vom Ziel ist, wird die pan geändert, die Einheit geht etwas vorwärts, und es wird nochmal versucht, das Ziel zu finden.

function selected_unit()
{
     my.unlit = on; // shouldn't be affected by the lights in the level
     my.ambient = 100; // make it bright
     my.oriented = on;
     my.passable = on;
     my.tilt = 90;
     while (you.target_reached == 0) // as long as the target hasn't been reached
     {
          vec_set (my.pos, you.pos); // move with the unit
          wait (1);
     }
     ent_remove (me); // the target has been reached so the square has to disappear
}

Die Funktion selected_unit bewegt den grünen Sprite, der die ausgewählte Einheit umgibt, solange, bis die Einheit das Ziel erreicht hat.

Die letzte Funktion ist aus AUM2:

function move_camera()
{
     waitt (4); // wait for the level to load
     level_marginx = 1000; // the map has 2000 quants on x (-1000...+1000)
     level_marginy = 1000; // and on y (-1000...+1000)
     while (1)
     {
          mouse_pos.x = pointer.x;
          mouse_pos.y = pointer.y;
          if (mouse_pos.x < 1 && camera.x > level_marginx * (-1)) {camera.x -= 10 * time;}
          if (mouse_pos.x > screen_size.x - 2 && camera.x < level_marginx) {camera.x += 10 * time;}
          if (mouse_pos.y > screen_size.y - 2 && camera.y > level_marginy * (-1)) {camera.y -= 10 * time;}
          if (mouse_pos.y < 1 && camera.y < level_marginy) {camera.y += 10 * time;}
          wait (1);
     }
}

Sie können die Level Ausdehnung mit level_marginx und level_marginy setzen und die Scroll-Geschwindigkeit anpassen, indem sie den Wert 10 ersetzen.

Stratego 1 und - 2 kann sicher dazu benutzt werden, um ein gutes Strategie-Spiel zu entwickeln, das einzige was noch nicht richtig behandelt wurde ist z.B. KI (Pathfinding) and Nahkampf. Starten Sie ihr Strategiespiel noch heute - diese zusätzlichen Funktionen werde ich bestimmt noch in den nächsten Monaten hinzufügen, keine Sorge! :)
 

 
Geschütztürme

Diese bösartigen Türme können Sie innerhalb von ein oder zwei Sekunden töten, da sie darauf trainiert worden sind, seit sie 5 Jahre alt sind. Sehen Sie sich die ihnen zugewiesene Aktion an:

action turret
{
     if (my.skill1 == 0) {my.skill1 = 1000;} // set the range with skill1, default = 1000 quants
     if (my.skill2 == 0) {my.skill2 = 180;} // default viewing angle in front of the turret = 180
     my.enable_impact = on;
     my.event = destroy_turret;
     while (player == null) {wait (1);}
     while (my != null && player != null)
     {
          my.skill10 = abs(ang(player.pan) - ang(my.pan));
          if (vec_dist (my.x, player.x) < my.skill1)
          {
               if ((my.skill10 > 180 - my.skill2 / 2) && (my.skill10 < 180 + my.skill2 / 2))
               {
                    vec_set (temp.x, player.x);
                    vec_sub (temp.x, my.x);
                    vec_to_angle (my.pan, temp);
                    ent_create (bullet_mdl, my.pos, move_bullet);
               }
          }
          waitt (8); // fires 2 bullets a second
     }
}

Die Türme haben eine einstellbare Reichweite und einen Sichtradius; Wenn sie vergessen skill1 und skill2 im Wed einzutragen, werden die Türme ihre Standardwerte verwenden. Wenn die Einheiten von einer Kugel getroffen werden, werden sie zerstört. Wir werden später darauf zurückkommen. Solange die Türme und der Spieler „leben”, berechnet der Turm den Abstand zum Player, dreht sich zu ihm und feuert 2 Kugeln pro Sekunde ab.

my.skill10 = abs(ang(player.pan) - ang(my.pan));
................................
if ((my.skill10 > 180 - my.skill2 / 2) && (my.skill10 < 180 + my.skill2 / 2))

Diese zwei Zeilen stellen sicher, dass der Turm und der Spieler sich „sehen” können. Ich hätte ebensogut eine scan_entity Anweisung verwenden können. Die Türme können den Spiele „sehen”, wenn der Winkel zwischen dem Spieler und dem Turm im Bereich von 180 - skill2 / 2 .... 180 + skill2 / 2 liegt. Wenn wir die Standardwerte für skill2 verwenden, beträgt der Winkel, in dem der der Turm den Spieler bemerken wird, zwischen 90 und 270 Grad.

function destroy_turret()
{
     wait (1);
     if (you == player) {return;} // can't be destroyed by running into it
     snd_play (explode_snd, 70, 0);
     ent_remove (me);
}

Wenn der Turm mit etwas zusammenstößt, wird er zerstört. Wir wollen dem Spieler nicht erlauben, den turm zu zerstören indem er einfach gegen ihn rennt, also passiert bei (you == player) nichts. Die Funktion, die die Kugel des Turmes bewegt, ist typisch und ich habe ihre Funktionsweise einige Male in voherigen Aums besprochen:

function move_bullet()
{
     wait (1);
     my.enable_entity = on;
     my.enable_block = on;
     my.event = remove_bullet;
     my.passable = on;
     my.pan = you.pan;
     bullet_speed.x = 100;
     bullet_speed.y = 0;
     bullet_speed.z = 0;
     bullet_speed *= time;
     while (my != null)
     {
          if (you == null) {return;}
          if (vec_dist (my.x, you.x) < 100) // don't collide with the turret
          {
               my.passable = on;
          }
          else
          {
               my.passable = off;
          }
          ent_move (bullet_speed, nullvector);
          wait (1);
     }
}

Hier ist die fertige Funktion:

function remove_bullet()
{
     wait (1);
     if (you == player) {player._health -= 10;}
     ent_remove (me);
}

Sobald die Kugel den Spieler trifft, werden dem Spieler 10 Lebenspunkte abgezogen und sie verschwindet. Wenn die Kugel eine Wand trifft, wird sie verschwinden ohne irgendwelchen Schaden anzurichten.