Deformierbare Terrains

Der folgende Code ist ein Remake einer meiner älteren Aum Artikel; diesmal ist der Code aber komplexer, flexibler und läuft schneller. Sie steuern einen Spieler aus der Egoperspektive, der mit einem Raketenwerfer in der Hand das Terrain nach Belieben verformen kann.

panel cross_pan
{
    bmap = cross_pcx;
    pos_x = 393;
    pos_y = 293;
    layer = 15;
    flags = overlay, refresh, visible;
}

function main()
{
   level_load (terrain1_wmb);
}

action terrain
{
   terrain1 = my; // used by vec_for_mesh and vec_to_mesh
   my.enable_impact = on; // triggers events if it is hit by rockets
   my.event = create_crate;
}

Ich benutze ein Fadenkreuz-Panel namens cross_pan; die Werte für pos_x und pos_y stellen sicher, dass es bei einer Auflösung von 800 x 600 Pixeln in der Bildschirmmitte angezeigt wird. Die main Funktion lädt das Level und die Action namens “terrain” läßt das Terrain auf Kollision mit anderen Entities (unseren Raketen) reagieren und wählt die erforderliche Event Funktion (create_crate).

function create_crate()
{
    if (you.skill40 != 12345) // not a rocket?
    {
       return; // then get out of here
    }
    vec_set (hit_coords, rocket1.x); // store rocket's coords before they get lost
    wait (1);
    my.skill1 = 1; // set the index to 1
    my.skill2 = 50000; // set a huge distance at first; skill2 will get smaller and smaller for the vertices that are closer and closer to the explosion
    while (my.skill1 < 1090) // go through all the vertices (1...1089) on the 33x33 grid (not using vertex_dist[0])
    {
       vec_for_mesh (temp, terrain1, my.skill1); // sets temp to vertex1, 2, 3...
       vertex_dist [my.skill1] = vec_dist (hit_coords.x, temp.x); // measures the distance between temp (vertex1, 2, 3...) and hit_coords
       if (vertex_dist[my.skill1] < my.skill2) // this vertex is closer than the others?
       {
          my.skill2 = vertex_dist[my.skill1]; // then it might be the one, so store the distance
          my.skill3 = my.skill1; // and its index as well
       }
       my.skill1 += 1; // move on to the following vertex
    }
    deform (my.skill3);
}

Die obige Funktion wird aufgerufen, wenn etwas (wie z.B. unsere Raketen) mit dem Terrain kollidiert. Ich habe den Skill40 der Raketen auf "“2345"”gesetzt, damit nichts geschieht, wenn eine andere Entity (der Spieler z.B.) mit dem Terrain kollidiert. Wenn eine Rakete für die Kollision verantwortlich war, speichern wir ihre Position in der hit_coords Variable, setzen einen Index auf 1 und speichern einen großen Wert (50.000 Quants) in Skill2.

Unser Terrain hat 33 x 33 = 1089 Vertizes; die “while” Schleife geht sie durch und prüft, welcher am nächsten an hit_coords liegt. Wenn die Schleife durchlaufen wurde, ist der kürzeste Abstand in Skill2 und der entsprechende Vertex in Skill3 gespeichert. Das Terrain wird mit den Koordinaten aus Skill3 verformt.

function deform(vertex_number)
{
    vec_for_mesh(temp, terrain1, vertex_number);
    vec_scale (temp, 0.7);
    vec_to_mesh (temp, terrain1, vertex_number);
    ent_fixnormals (my, my.frame); // recalculate the normal vectors, not really needed
}

Diese Funktion verformt das Terrain, indem die Höhe des entsprechenden Punktes um 70% verringert wird; ändern Sie diesen Wert, wenn Sie möchten. Die letzte Zeile berechnet die Normalenvektoren erneut und ist dafür zuständig, dass dynamische Lichter auch nach der Deformation korrekt angezeigt werden.

Der Spieler hat eine simple Action:

action player_1st
{
    my.invisible = on; // no need to see the player model in 1st person view
    while (1)
    {
       my.skill1 = 3 * (key_w - key_s) * time; // move with w / s
       my.skill2 = 2 * (key_a - key_d) * time; // strafe with a / d
       vec_set (temp, my.x);
       temp.z -= 1000;
       trace_mode = ignore_me + ignore_passable + use_box;
       my.skill3 = -trace (my.x, temp);
       my.pan -= 3 * mouse_force.x;
       my.tilt += 3 * mouse_force.y;
       if (mouse_left == 1)
       {
          players_bullet();
       }
       move_mode = ignore_passable + ignore_passents + glide;
       ent_move(my.skill1, nullvector);
       wait (1);
       camera.x = my.x;
       camera.y = my.y;
       camera.z = my.z + 30;
       camera.pan = my.pan;
       camera.tilt = my.tilt;
    }
}

Er kann mit den WASD-Tasten bewegt werden; der Code beinhaltet Schwerkraft und erlaubt es uns, die Kamera mit der Maus zu drehen. Wenn der Spieler die linke Maustaste drückt, wird die Funktion player_bullet() gestartet. Der Spieler ignoriert passierbare Entities bei seiner Bewegung und gleitet an Flächen entlang (das “glide”-Flag ist gesetzt), die im Weg sind; die Kamera hat dieselbe Position und Ausrichtung wie der Spieler und wird 30 Quants oberhalb seines Origins plaziert.

function players_bullet()
{
    proc_kill(4); // only one instance of this function should run
    if (rocket1 != null) {return;} // don't allow more than 1 rocket to be fired at a certain moment
    while (mouse_left == 1) {wait (1);} // disable auto fire
    snd_play (rocket_wav, 50, 0);
    rocket1 = ent_create (rocket_mdl, camera.x, move_players_bullet);
}

function move_players_bullet()
{
   var distance_covered = 0;
   my.pan = you.pan; // the bullet and the player have the same pan angle
   my.tilt = you.tilt; // and tilt angle
   my.enable_entity = on; // the bullet is sensitive to other entities
   my.enable_impact = on;
   my.enable_block = on; // and to level blocks
   my.event = remove_bullet;
   my.skill2 = 0;
   my.skill3 = 0;
   my.skill40 = 12345; // unique identifier for the rocket
   while (my.invisible == off)
   {
      my.skill1 = 30 * time; // bullet speed
      move_mode = ignore_you + ignore_passable;
      distance_covered += ent_move (my.skill1, nullvector);
      if (distance_covered > 100)
      {
         my.skill3 -= 1 * time;
      }
      wait (1);
   }
}

Die Funktion players_bullet() startet jedes Mal, wenn der Spieler die linke Maustaste drückt; wir stellen sicher, dass nur eine Instanz der Funktion läuft und warten dann, bis die zuvor abgefeuerte Rakete verschwunden ist. Die folgenden Zeilen verhindern automatisches Feuern, lassen ein Geräusch ertönen und erzeugen ein Raketenmodel namens rocket1, das die Action move_players_bullet() erhält.

Diese Funktion stellt sicher, dass die Rakete die Ausrichtung des Spielers übernimmt und läßt sie auf Kollisionen mit anderen Entities und Blocks reagieren; die Event Funktion heißt remove_bullet. Die Rakete wird mit Hilfe der Werte in Skill1 bis Skill3 bewegt, die wie eine einzelne Variable (ein Vektor) behandelt werden. Skill40 wird auf den seltsamen Wert 12345 gesetzt, damit die Rakete vom Terrain als solche identifiziert werden kann. Die Rakete bewegt sich bis sie unsichtbar gemacht wird. Die Geschwindigkeit wird durch Setzen von Skill1 in der Schleife bestimmt und die zurückgelegte Distanz der Rakete wird in der lokalen Variable distance_covered aufaddiert. Sobald dieser Wert 100 übersteigt (auch dieser Wert kann nach belieben angepasst werden), wird Skill3 verringert, was dazu führt, dass die Rakete nach unten abdriftet. Die Rakete wird zu oden stürzen und einen Krater hinterlassen.

function remove_bullet()
{
    my.invisible = on;
    my.passable = on;
    ent_playsound (my, shockwave_wav, 200);
    ent_create(explosion_pcx, my.x, explosion_sprite);
    sleep (1);
    ent_remove (my);
}

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

Was geschieht, wenn die Rakete auf den Boden fällt? Sie wird unsichtbar und passierbar gemacht und an der Aufschlagsposition ertönt ein entsprechendes Geräusch. Desweiteren wird ein Explosionssprite mit der explosion_sprite Action erstellt.

Die letzte Funktion skaliert die Explosion korrekt, macht das Sprite passierbar und bright und spielt die Animation einmal durch, bevor das Sprite entfernt wird. Es ist eine einfache Methode, aber sie funktioniert gut.

Serious grass

Soweit ich weiß war Serious Sam das erste Spiel mit diesem Feature: der Spieler bewegt sich durchs Level und die Gras Sprites bzw. Models werden vor seinem Auge erzeugt, während diejenigen, die weiter entfernt sind automatisch entfernt werden. Auf diese Weise ist es möglich, gut aussehende Pflanzen zu haben, die sich nicht allzu negativ auf die CPU auswirken.

starter generate_grass()
{
    var player_position1;
    var player_position2;
    while (player == null) {wait (1);}
    while (1)
    {
       vec_set (temp.x, player.x);
       temp.x += 500 - random(1000);
       vec_set (temp.y, player.y);
       temp.y += 500 - random(1000);
       temp.z = player.z + 200;
       // not too close to the player and the player has moved recently?
       if ((abs(temp.x - player.x) > 300) && (abs(temp.x - player.x) > 300))
       {
          // increase the nexus if you want to have more than 280 bushes at once
          if (((player_position1.x != player_position2.x) && (number_of_bushes < 280)) || (total_frames < 300))
          {
             if (random(1) > 0.1)
             {
                 ent_create (grass1_mdl, temp.x, place_grass);
             }
             else
            {
                ent_create (grass2_mdl, temp.x, place_grass);
            }
        }
     } 
     vec_set (player_position1.x, player.x);
     wait (1);
     vec_set (player_position2.x, player.x);
   }
}

Ich verwende eine starter Funktion, die wartet bis der Spieler existiert und dann ein Quadrat aus Gras erstellt, in dessen Zentrum der Spieler ist:

Wenn Sie sich die erste Bedingung ansehen, stellen Sie fest, dass die Pflanze nur erstellt wird, wenn die Distanz zum Spieler größer als 300 Quants ist; wir möchten ja nicht, dass ein Busch direkt vor dem Spieler auftaucht, nicht wahr? Die folgende Bedingung sorgt dafür, dass Pflanzen nur dann erstellt werden, wenn sich der Spieler bewegt; es macht nicht viel Sinn, die Models zu erzeugen, wenn der Spieler steht. Mit einem einfachen Trick kann ich ermitteln, ob sich der Spieler bewegt oder nicht; ich habe zwei Variablen namens player_position1 und player_position2, in denen die Position des Spielers vor und nach dem “wait(1)” gespeichert wird. Wenn sich der Spieler im letzten Frame bewegt hat, sind beide Variablen verschieden und das Grass wird erzeugt, falls number_of_bushes kleiner als 280 ist. Diese Variable zählt, wieviele Gras Models erzeugt wurden und begrenzt die Anzahl der Graspolygone im Level. Sie können diesen Wert erhöhen, aber dann müssen Sie auch den Nexus vergrößern (er sollte eine Größe von 100 für 1000 Pflanzen haben usw.)

Ich habe fast vergessen, Ihnen das zu sagen: es spielt während der ersten 300 Frames (total_frames < 300) keine Rolle, ob sich der Spieler bewegt oder die Anzahl der Büsche größer als 280 ist. Dies sorgt dafür, dass die Grasmodels am Anfang des Spiels in einer größeren Zahl erstellt werden, auch wenn sich der Spieler nicht bewegt und trotzdem realistische Pflanzen sehen möchte.

Ich verwende zwei verschiedene Pflanzenmodels namens grass1.mdl und grass2.mdl und beide werden mit der place_grass Funktion aufgerufen:

function place_grass()
{
    number_of_bushes += 1;
    my.passable = on;
    my.transparent = on;
    my.pan = random(360);
    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 grass model on the ground
    while (vec_dist(player.x, my.x) < 1000)
    {
       my.alpha = min (100, (10000 / (vec_dist(player.x, my.x))));
       wait (1);
    }
    ent_remove (my);
    number_of_bushes -= 1;
}

Meine Fans haben vielleicht bemerkt, dass jedes Model 200 Quants oberhalb des Origins vom Spieler erzueugt wird, number_of_bushes erhöht wird, das Model passierbar und transparent gemacht wird, eine zufällige Ausrichtung bekommt und schließlich mit Hilfe einer trace Anweisung korrekt am Boden plaziert wird. Jedes Model hat eine interne while Schleife, welche die Transparenz kontrolliert und es durchsichtig macht, wenn der Spieler sich entfernt; ist der Spieler weiter als 1000 Quants entfernt, wird das Model entfernt und number_of_bushes wird verringert, wodurch ein brandneues Model erzeugt werden kann.