Genaues Fadenkreuz

Eines der Probleme mit den Templatewaffen ist, dass die Kugeln nicht ins Zentrum des Fadenkreuzes treffen (drücken Sie K, um das Fadenkreuz anzuzeigen oder zu verbergen, wenn Sie die Tampltes benutzen). Das Problem wird klar, wenn Sie nahe an einer Wand stehen und eine Waffe benutzen, die ein hit_hole in ihrer Definition hat; weapon_mg_animated ist ein gutes Beispiel. Der Grund ist einfach: die Waffe ist nicht in der Mitte des Bildschirms (sie ist normalerweise auf der rechten Seite) also kann die Mitte gar nicht getroffen werden, die durch das Fadenkreuz angezeigt wird. Darüber hinaus werden Sie kleine Gegenstände auf große Distanzen verfehlen - etwa ein Seil, welches eine Brücke absenkt.

Nun, da Sie das Problem kennen, werden Sie erfreut sein zu hören, dass Sie es mit nur 11 Code Zeilen lösen können. Wir müssen weapons.wdl modifizieren, aber keine Angst - ich habe den modifizierten Code bereits beigefügt.

Die Idee ist leicht: wir erstellen zwei Positionen (Vektoren) vor dem Spieler: eine in 20 Quants Entfernung und eine in 10000 Quants Entfernung und dann tracen wir zwischen diesen. Beide Positionen werden in der Bildschirmmitte erzeugt und daher trifft jede Kugel auch genau das Zentrum des Fadenkreuzes - in einer geraden Linie!

Wenn wir diesen Trick nicht benutzen würden, müßten wir alle Waffen in die Bildschirmmitte setzen, wie im Bild:

Schauen wir uns diesen tollen Code an - er wird in die Funktion gun_fire() der weapons.wdl geschrieben:

 damage = MY._DAMAGE;
 fire_mode = MY._FIREMODE;

 my.skill40 = (screen_size.x + bmap_width(cross_bmp)) / 2;
 my.skill41 = (screen_size.y + bmap_height(cross_bmp)) / 2;
 my.skill42 = 20;
 vec_set (my.skill43, my.skill40);
 my.skill45 = 10000;
 vec_for_screen (my.skill40, camera);
 vec_for_screen (my.skill43, camera);
 trace_mode = ignore_me + ignore_passable;
 trace (my.skill40, my.skill43);
 vec_set (gun_target, target);
 vec_diff (shot_speed, gun_target, gun_muzzle);

 // if player is not already passable
 if(player.passable == OFF)
 {
      player.passable = ON;   // make it so the gun doesn't hit the player
      gun_shot();       // fire the shot
       player.passable = OFF;
 }
 else
 {
      gun_shot();
 }

Der alte Templatecode ist rot, der neue gelb.

Ich habe skill40 bis skill42, sowie skill43 bis skill45 als zwei Vektorvariablen benutzt. Als erstes holen wir uns die Koordinaten der Bildschirmmitte. Wenn Sie nicht geschlafen haben, werden Sie wissen, dass die Bildschirmmitte so definiert ist:

 my.skill40 = (screen_size.x - bmap_width(cross_bmp)) / 2; // with "-" not "+"
 my.skill41 = (screen_size.y - bmap_height(cross_bmp)) / 2; // with "-" not "+"
 
Sie haben recht: es ist ein kleiner Bug in der weapons.wdl; das Fadenkreuz Panel ist nicht genau in der Mitte des Bildschirms, sondern nur seine obere linke Ecke, also müssen wir die Hälfte des Bitmaps addieren, um zum Zentrum zu kommen. Weiter: skill42 wird 20 Quants vor den Screen gesetzt, weil wir nicht wollen, dass trace den Spieler oder seine Waffe trifft und das Ergebnis verfälscht.

Nun kopieren wir skill40 in skill43; da sie als Vektoren angesehen werden, werden auch skill44 und skill45 automatisch auf skill41 bzw. skill42 gesetzt. Nun muß skill45 nachträglich noch 10000 Quants vom Spieler entfernt werden, also ändern wir den Wert. Die folgenden vec_for_screen Anweisungen liefern die virtuellen Positionen in der 3D Welt, die wir haben möchten. Wir tracen zwischen diesen und setzen das Ergebnis (target, vgl. Handbuch) auf gun_target und  holen uns die Richtung (shot_speed) von gun_muzzle nach gun_target.

Diese Zeile mußte ich an den Anfang von weapons.wdl setzen:

BMAP cross_bmp, <cross.pcx>;

Denn ich brauchte cross_bmp in meinem Code und das war zuvor nicht definiert.

 
 
Thanks

Das folgende Projekt sollte Ihnen zu einem guten Start verhelfen, wenn Sie ein Panzerspiel planen.

Die main Funktion schaltet die "D" Taste ab, weil mit ihr der Spieler bewegt wird (WASD) und lädt das Level:

function main()
{
     on_d = null; // disable the standard "debug" key
     level_load (level1_wmb);
}

Der Panzer, den der Spieler kontrolliert, besteht aus 3 Teilen: Rumpf, Turm und Kanonenrohr. Der Turm und das Rohr werden mit 2 ent_create Anweisungen in der action player_tank am Rumpf angebracht:

ent_create(challenger_turret, nullvector, attach_player_turret);
ent_create(challenger_barrel, nullvector, attach_player_barrel);

Diese Funktion fügt die Teile an den Rumpf an:

function attach_player_turret()
{
     my.passable = on;
     while(you) // as long as the creator exists
     {
          vec_set(my.x, you.x);
          my.pan = you.pan + turret_offset;
          my.frame = you.frame;
          my.next_frame = you.next_frame;
          wait(1);
     }
    ent_remove(my);
}

Der Turm ist passable und existiert nur solange der Rumpf existiert. Er hat dieselbe Position und dieselbe Animation wie der Rumpf. Diese Zeile ist interessant:

          my.pan = you.pan + turret_offset;

Der Rumpf und der Turm können in verschiedenen Winkeln zueinander stehen, wie im Bild unten. Turret_offset ist die Differenz ihrer pan Winkel zu jedem Zeitpunkt.
 

In meinem Beispiel ist turret_offset = 60 Grad. Die Funktion attach_player_barrel() ist ähnlich:

function attach_player_barrel()
{
     my.passable = on;
     while(you) // as long as the creator exists
     {
          vec_set(my.x,you.x);
          my.pan = you.pan + turret_offset;
          my.tilt = you.tilt + barrel_offset;
          my.frame = you.frame;
          my.next_frame = you.next_frame;
          vec_for_vertex(rocket_coords, my, 33); // vertex for player's rocket
          if (mouse_left == 1 && procket_ptr == null && player.shield > 0)
         {
              ent_create (tank_rocket, rocket_coords, player_rocket);
              snd_play (tankrocket_snd, 100, 0);
         }
         wait(1);
    }
    ent_remove(my);
}

Das Rohr hat denselben Winkel wie der Turm, also benutzt es dieselbe turret_offset Variable, aber es kann einen anderen Tilt Winkel haben und dieser Wert ist in barrel_offset gespeichert. Der Turm wird die Raketen abschießen, also benutzen wir seinen 33. Vertex als Ausgangspunkt für die Raketen, die der Spieler abschießt.

Ich wollte Autofire abschalten, weil es weder fair noch realistisch wäre, mehrere Rakten pro Sekunde schießen zu können; ich habe das dadurch erreicht, dass ich der Rakete einen Zeiger (procket_ptr) zuweise, wenn sie erstellt wird. Wenn die Rakete aufschlägt und explodiert, ist procket_ptr wieder null und eine weitere Rakete kann abgefeuert werden. Wenn die linke Maustaste gedrückt wird und die Rakete davor nicht mehr existiert und der Spieler noch lebt, erstellen wir eine Rakete und spielen einen Sound.

Hier ist die Action für den Spieler:

action player_tank
{
     player = me;
     my.enable_impact = on;
     my.enable_entity = on;
     my.event = tank_damaged;
     ent_create(challenger_turret, nullvector, attach_player_turret);
     ent_create(challenger_barrel, nullvector, attach_player_barrel);
     my.shield = 250; // 250 shield points for player's tank

Ich bin der Spieler und ich reagiere auf Kollisionen mit anderen Entities oder Wänden. Wenn eines dieser Events eintritt, läuft die Funktion tank_damage(). Ich habe 250 Shield Punkte; shield ist nur ein anderer Name für skill20. Der Turm und das Rohr wurden oben diskutiert.

     while (my.shield > 0) // the 10th hit will destroy player's tank
     {
          camera.pan = my.pan;
          camera.x = my.x - 500 * cos (my.pan);
          camera.y = my.y - 500 * sin (my.pan);
          camera.z = my.z + 300;
          camera.tilt = -20; // the camera looks down

          if (mouse_force.x != 0) // if we move the mouse on x
          {
               if (turret_handle == 0)
               {
                    turret_handle = snd_play (turret_snd, 70, 0);
               }
               turret_offset -= 2 * mouse_force.x * time;
          }
          else
          {
               turret_handle = 0;
          }
          if (mouse_force.y != 0) // if we move the mouse on y
          {
               barrel_offset += 2 * mouse_force.y * time;
          }

Solange mein Shield Wert größer als 0 ist, folgt mir die Kamera in einem Abstand von 500 Quants nach hinten und 300 Quants nach oben; sie schaut auf mich hinunter.

Wenn die Maus auf der X-Achse bewegt wird und der Turmsound nicht bereits abgespielt wird, spielen wir ihn ab und ändern turret_offset - dies ändert den Winkel von Turm und Rohr zugleich. Die Y-Achse der Maus ist für barrel_offset zuständig, den tilt Winkel für das Rohr.

          if (player_speed.x != 0) //
          {
               my.pan -= 0.1 * (key_d - key_a) * player_speed.x * time;
               if (engine_handle == 0)
               {
                    engine_handle = snd_loop (tank_snd, 50, 0);
                    snd_tune (engine_handle, 25, 100, 0); // original frequency (100%)
                    my.skill10 = 100;
               }
          }
          else
         {
               my.skill10 -= 5 * time;
               snd_tune (engine_handle, 25, my.skill10, 0);
               if (my.skill10 < 20) // below 20% of the original frequency
               {
                    snd_stop (engine_handle);
                    engine_handle = 0;
               }
          }

Der Panzer selbst ändert seinen pan, wenn "A" oder "D" gedrückt werden. Wie Sie sehen kann der Panzer sich nur dann drehen, wenn er sich bewegt, wie (fast) jedes Fahrzeug mit Rädern. Wenn der Motor Sound nicht läuft, starten wir ihn mit der Originalfrequenz. Wenn der Panzer sich nicht bewegt ("W" oder "S" sind nicht gedrückt), senken wir die Frequenz mit skill10 und snd_tune, bis sie mehr als 20% unter der ursprünglichen Frequenz ist und dann schalten wir sie ab.

          player_speed.x = 1.5 * (key_w - key_s) * time + max (1 - time * 0.15, 0) * player_speed.x;
          player_speed.y = 0;
          player_speed.z = 0;
          move_mode = ignore_you + ignore_passable;
          ent_move(player_speed, nullvector);
          wait (1);
     }
}

Der Spieler bewegt sich mit den "W" und "S" Tasten mit Hilfe einer simplen Formel, die schon in früheren AUM Ausgaben diskutiert wurde; ändern Sie 1.5, um die Geschwindigkeit anzugleichen oder 0.15 für Beschleunigung.

Wenn der Spieler die linke Maustaste drückt, wird eine Rakete erstellt:

function player_rocket()
{
     procket_ptr = me;
     my.skill40 = 1; // it is a rocket
     my.pan = you.pan;
     my.tilt = you.tilt;
     my.enable_impact = on;
     my.enable_block = on;
     my.enable_entity = on;
     my.event = explode_rocket;
     my.skill5 = 0; // used for vertical movement
     procket_speed.x = 100;
     procket_speed.y = 0;
     procket_speed.z = 0;
     procket_speed *= time;
     while (my != null)
     {
          my.skill5 += time;
          if (my.skill5 > 20)
          {
               procket_speed.z -= fall_speed * time;
          }
          move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
          ent_move (procket_speed, nullvector);
          wait (1);
     }
}

Jede Rakete erhält den procket_ptr Zeiger, um Autofire auszuschließen, erinnern Sie sich? Indem skill40 auf 1 gesetzt wird, können wir prüfen, ob eine bestimmte Entity eine Rakete ist oder nicht - wir werden dieses Feature später nutzen. Die Rakete hat dieselbe Orientierung (pan und tilt) wie das Rohr und reagiert auf impact, blocks und andere Entities. Wenn eines dieser Events eintritt, läuft die Funktion explode_rocket() ab. Pro Frame bewegt sich die Rakete 100 * time Quants.

Solange die nicht explodiert, wird skill5 in einer Schleife erhöht und die Rakete wird bewegt. Steigt skill5 über 20, beginnt die Rakete an Höhe zu verlieren (Gravitation) und wird irgendwann den Boden treffen, wenn sie vorher auf nichts anderes trifft. Spielen Sie mit den Werten fall_speed und dem Wert 20 für verschiedene Trajektorien.
 

Trifft die Rakete auf den Boden, eine Wand oder eine andere Entity explodiert sie mit folgender Funktion:

function explode_rocket()
{
     my.skill10 = vec_dist (player.x, my.x);
     if (my.skill10 < 1000) {my.skill10 = 1000;}
     my.skill10 = 100000 / my.skill10;
     snd_play (tankexplo_snd, my.skill10, 0);

     vec_set (temp, my.pos);
     temp.z += 50; // create the explosion sprite a little higher
     ent_create(explosion_pcx, temp, animate_explosion);
     ent_remove (me);
}

Ich habe mich entschieden, die ent_playsound Funktion durch eine zu ersetzen, die in diesem Fall besser funktioniert. Die Rakete ermittelt die Distanz zwischen sich und dem Spieler und speichert das in skill10. Ist diese Distanz geringer als 1000 Quants wird das Explosionsgeräusch mit maximaler Lautstärke gespielt, sonst verringern wir die Lautstärke je nach dem Wert von skill10.

Wir müssen noch eine Explosion am Aufschlagpunkt erzeugen, richtig? Wir erreichen dies mit einem Explosions Sprite und der Funktion animate_explosion(); beachten Sie, dass das Sprite 50 Quants oberhalb des Aufschlagpunktes erstellt wird - dadurch sehen wir es ganz:

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

Das Sprite ist passable und bright und läuft durch seine Frames; es wird entfernt, wenn es den letzten Frame erreicht hat.

Was geschieht, wenn einer der Panzer getroffen wird?

function tank_damaged()
{
     if (you.skill40 != 1) {return;}
     wait (1);
     my.shield -= 25;
     if (my.shield <= 0) // player's tank was hit 10 times or the enemy tank was hit twice
     {
          while (1)
          {
               effect (tank_smoke, 5, my.pos, normal);
               sleep (0.2);
          }
     }
}

Der Panzer könnte auch mit einer anderen Entity zusammenstoßen, z.B. einem Brunnen, einem anderen Panzer, etc. Wenn diese andere Entity ihren skill40 nicht auf 1 hat, verlassen wir die Funktion wieder. Jeder Raketentreffer zieht 25 Schildpunkte ab, also sollte der Spieler nach 10 Treffern zerstört sein (player.shield = 250; erinnern Sie sich?)

Wenn der Spieler 10 Mal oder ein Feind zweimal getroffen wurde (ihre Schilde = 50), beginnen sie zu rauchen:

function tank_smoke()
{
     temp.x = random(2) - 1;
     temp.y = random(2) - 1;
     temp.z = random(2) + 1;
     vec_add (my.vel_x, temp);
     my.alpha = 30 + random(30);
     my.bmap = tanksmoke_pcx;
     my.size = 100;
     my.flare = on;
     my.move = on;
     my.lifespan = 100;
     my.function = fade_smoke;
}

function fade_smoke()
{
     my.alpha -= 0.5 * time;
     if (my.alpha < 0)
     {
          my.lifespan = 0;
     }
}

Der Rauch hat zufällige Geschwindigkeit in x und y Richtung und eine positive Geschwindigkeit in z Richtung, er bewegt sich also nach oben. Die Rauchpartikel verringern ihren alpha Wert und werden entfernt, wenn sie nicht mehr zu sehen sind.

Nun wird es Zeit, sich die gegnerischen Panzer anzusehen. Ich habe ein Model benutzt, welches den Turm und das Rohr direkt an den Rumpf angebracht hat, um die Dinge zu vereinfachen.

action enemy_tank
{
     my.enable_impact = on;
     my.enable_entity = on;
     my.event = tank_damaged;
     my.shield = 50;

Der Panzer reagiert auf impact und andere Entities und teilt sich die tank_damaged Funktion mit dem Spieler. Zwei Raketen des Spielers zerstören ihn.

     while (my.shield > 0)
     {
          vec_set (temp.x, player.x);
          vec_sub (temp.x, my.x);
          vec_to_angle (my.skill4, temp); // turn towards the player

Solange der Feind "lebt", richtet er sich in Richtung des Spielers aus und versucht, zu feuern. Wir wollen keine plötzliche Drehung, sondern eine weiche, also drehen wir nicht den Panzer direkt zum Spieler, sondern skill4. Nun wissen wir, dass in skill4 der Winkel zum Spieler steht und müssen den Panzer nun langsam dahin ausrichten, wie im richtigen Leben.

          if (abs(my.skill4 - my.pan) < 1)
          {
               vec_for_vertex(my.skill1, my, 41); // vertex for the enemy rocket
               ent_create (tank_rocket, my.skill1, enemy_rocket);
               sleep (3);
          }
          else
          {
               my.pan += ang(my.skill4 - my.pan) * 0.03 * time; // smooth rotation code
               enemy_speed.x = 5 * time;
               enemy_speed.y = 0;
               enemy_speed.z = 0;
               move_mode = ignore_you + ignore_passable;
               ent_move(enemy_speed, nullvector);
          }
          wait (1);
     }
}

Wenn die Differenz zwischen dem Winkel in skill4 und dem aktuellen pan kleiner ist als 1 Grad, setzen wir den Startpunkt der gegnerischen Rakete zum 41. Vertex des Panzers und feuern alle 3 Sekunden. Ist die Differenz größer als 1 Grad, dreht der Panzer sich langsam und bewegt sich dabei, wie ein Fahrzeug auf Rädern.

Die Funktion für die gegnerischen Raketen ist anders, weil der Gegner alle 3 Sekunden schießen kann:

function enemy_rocket()
{
     my.skill40 = 1; // it is a rocket
     my.pan = you.pan;
     my.tilt = you.tilt;
     my.enable_impact = on;
     my.enable_block = on;
     my.enable_entity = on;
     my.event = explode_rocket;
     erocket_speed.x = 100;
     erocket_speed.y = 0;
     erocket_speed.z = 0;
     erocket_speed *= time;
     while (my != null)
     {
          move_mode = ignore_you + ignore_passable; // ignores the barrel -> can't collide with it
          ent_move (erocket_speed, nullvector);
          wait (1);
     }
}

Wir setzen skill40 auf 1, auf diese Weise wird die Entity als Rakete erkannt. Die gegnerische Rakete hat denselben pan und tilt wie der Panzer, reagiert auf impacts, Blocks und andere Entites und hat dasselbe Event wie die Raketen des Spielers. Beachten Sie, dass diese Rakete in einer geraden Linie fliegt, ohne die Gravitation, also wird Ihr Panzer vermutlich oft getroffen.

Bevor Sie also diese Rohversion eines Spiels in einen verkaufsfähigen Hit machen können, sollten Sie Gravitation und eine bessere AI einbauen. Vergessen Sie nicht, mir eine kostenlose Kopie zu übersenden. :)