Volumetrischer Nebel
 

Acknex benutzt ein simples System für Nebel, das von allen auf dem Markt erhältlichen 3D Karten unterstützt wird, auch von den älteren. Die Engine stellt den z Wert der Pixel mit Hilfe einer Funktion ein, die Farbe, Stärke etc. des Nebels kontrolliert. Das Ergebnis ist der gut aussehende Nebel, den wir alle kennen und mögen (vgl. AUM 25 für nähere Informationen). Aber was ist, wenn wir etwas Nebel nur in einem bestimmten Bereich haben möchten, z.B. auf dem Boden des schleimigen Verlieses? Dafür bräuchten für volumetrischen Nebel.

Ich bin sicher, dass einige von Ihnen diese Frage schon oft stellten: wie kann ich solchen Nebel in mein Level einfügen? Dieser Artikel zeigt Ihnen, wie es geht. Das Ergebnis sieht gut aus und wirkt sich praktisch nicht auf die Performance aus.

1) Erstellen Sie ein simples Model im MED, das so aussieht:

Arbeiten Sie mit einem Würfel in MED und ändern Sie seine Größe, bis er Ihren Bedürfnissen entspricht. Vergessen Sie nicht, ihn sehr dünn zu machen. Verwenden Sie eine einzelne Farbe für die Skin, ich habe eine weiße Textur der Größe 32 x 32 Pixel genommen.

2) Plazieren Sie das Model in Ihrem Level, naha dem Boden, passen Sie falls nötig die Größe an und geben Sie dem Model die Action mit Namen fog_layer.

3) Fügen Sie weitere Models mit der gleichen Action hinzu, alle auf einer verschiedenen Höhe. Das Bild unten zeigt 5 “Schichten” (5 Models), die über dem Boden eingefügt sind; mein Demo Level verwendet 10 Schichten.


 
Nun ist es Zeit, sich den Code anzusehen:

action fog_layer
{
     my.passable = on;
     my.transparent = on;
     while (1)
     {
          my.alpha = max (0, (camera.z - my.z) / 20);
          wait (1);
     }
}

Ich weiß, dass es unglaublich klingt, aber dies ist aller Code, der benötigt wird, um den Nebeleffekt zu erzeugen! Das Model ist transparent und passierbar, sein Alpha Wert ändert sich in Abhängigkeit des Höhenunterschiedes zwischen der Kamera und dem Model. Wenn die Kamera näher kommt, wird die Schicht immer transparenter, bis sie verschwindet.

Sie können den Faktor ändern, indem Sie die “20” durch einen anderen Wert ersetzen, Sie können die Farbe Ihres Models ändern, Sie können auch verschiedene Actions mit verschiedenen Namen und anderen Werten für die “20” nehmen, wenn Sie verschiedene Nebeleffekte im gleichen Level brauchen, Sie können mehr oder weniger Models nehmen, Sie dünner oder dicker machen, um die Qualität des Nebels zu ändern, Sie können... nun ja, Sie können eine Menge damit anstellen.
 
 

Split screen shooter

Wenn Sie einen Zwillingsbruder (oder eine Zwillingsschwester) haben, bereiten Sie sich auf den Kampf Ihres Lebens vor!

Als Erstes definieren wir zwei Views:

view camera1
{
     layer = 15;
     pos_x = 0;
     pos_y = 0;
     size_x = 800;
     size_y = 300;
     arc = 80;
     flags = visible;
}

view camera2
{
     layer = 15;
     pos_x = 0;
     pos_y = 300;
     size_x = 800;
     size_y = 300;
     arc = 80;
     flags = visible;
}

Diese Definitionen werden den Bildschirm in zwei gleich große Teile aufteilen, wie im Bild unten:

Mit einer einfachen Text Definition zeige ich einen String auf dem Bildschirm an:

text score_text
{
     pos_x = 0;
     pos_y = 0;
     font = verdana_font;
     string = "Player1's score:\n\n\n\n\n\n\n\n\n\n\n\n\nPlayer2's score:";
     flags = visible;
}

Ich bin sicher, Sie erinnern sich daran, was das “\n” tut: es wirkt wie ein Zeielnumbruch. Auf diese Weise kann ich die “Player 1’s Score” und “Player 2’s Score” in einem einzigen String unterbringen. Mit Hilfe einer Panel Definition und zwei Digits zeige ich die Punkte dann an:

panel score_panel
{
     pos_x = 0;
     pos_y = 0;
     digits = 460, 0, 2, verdana_font, 1, score1;
     digits = 460, 300, 2, verdana_font, 1, score2;
     flags = refresh, visible;
}

Dieses Projekt steht für sich allein, hat also eine eigene Main Funktion:

function main()
{
     fps_max = 40;
     level_load(split_wmb);
     wait (3);
     camera.visible = off;
}

Die einzige erwähnenswerte Zeile hier ist die Letzte: sie verbirgt die Default Sicht (camera), weil wir ja zwei eigene für unser Projekt haben (camera1 und camera2).

Ich habe den meisten Code für beide Spieler kopiert, also sind die Actions und Funktionen für Spieler 1 identisch mit denen für Spieler 2.

action player_one
{
     player1 = me;
     my.enable_entity = on;
     my.enable_impact = on;
     my.event = kill_player1;
     ent_create (handgun1_mdl, nullvector, weapon1);

Die Entity mit dieser Action wird zu Spieler 1 und wird auf andere Entities reagieren; wenn dies geschieht, wird das Event “kill_player1” laufen. Die letzte Zeile erstellt die Waffe für Spieler 1; sie wird kontrolliert von der Funktion weapon1().

     while (p1_alive == 1)
     {
          my.pan += 5 * (key_a - key_d) * time;
          player1_dist.x = 15 * (key_w - key_s) * time;
          player1_dist.y = 0;
          player1_dist.z = 0;
          move_mode = ignore_passable + glide;
          ent_move (player1_dist, nullvector);
          camera1.pan = my.pan;
          camera1.x = my.x;
          camera1.y = my.y;
          camera1.z = my.z + 30;

Solange Spieler 1 lebt (p1_alive wird bei Spielstart auf 1 gesetzt), kann er sich mit Hilfe der “WASD” Tasten bewegen. Spieler 1 ignoriert passierbare Entities und gleitet an Mauern entlang. Die View namens camera1 folgt Spieler 1 die ganze Zeit, indem sie seine x und y Position benutzt und 30 Quants über seinem Zentrum positioniert wird.

          if (key_w + key_s > 0) // if the player is moving
          {
               ent_cycle("run", my.skill46);
               my.skill46 += 10 * time;
               my.skill46 %= 100;
          }
          else // the player is standing or shooting
          {
               if (key_space == 1) // if this player is shooting
               {
                    ent_cycle("attack", 100); // display the last "attack" animation frame
                    if (bullet1 == null)
                    {
                         wait (3);
                         ent_create (bullet_mdl, bullet1_coords, move_bullet1);
                    }
               }
               else
               {
                    ent_cycle("idle", my.skill46); // play "stand" frames animation
                    my.skill46 += 2 * time; // "stand" animation speed
                    my.skill46 %= 100; // loop animation
               }
          }

          wait (1);
     }
}

Wenn der Spieler sich bewegt (key_w oder key_s sind gedrückt) spielt das Model seine “run” Animation in einer Schleife. Bewegt er sich nicht, gibt es zwei Möglichkeiten:
1) Der Spieler schießt (“Space” ist gedrückt). Wenn dies der Fall ist, wird der letzte Frame der “Attack” Animation gezeigt und eine Kugel wird abgefeuert, sobald die vorherige Kugel fort ist.
2) Der Spieler steht still (“Space” ist nicht gedrückt). In diesem Fall spielen wir die “idle” Animation des Spielers in einer Schleife.

Schauen wir uns nun die Funktion für die Waffe an:

function weapon1()
{
     proc_late();
     my.passable = on;
     while (1)
     {
          vec_for_vertex (bullet1_coords, my, 148); // get the vertex coords for the bullet
          my.x = you.x;
          my.y = you.y;
          my.z = you.z;
           my.pan = you.pan;
          my.tilt = you.tilt;
          my.roll = you.roll;
          my.frame = you.frame;
          my.next_frame = you.next_frame;
          wait (1);
     }
}

Die erste Zeile stellt sicher, dass die Waffe nicht einen Frame hinter dem Spieler hängt. Mit einer Schleife ermitteln wir die Vertex Koordinaten für die Kugel, damit diese jeweils an der gleichen Position, nämlich Vertex #148 des Waffenmodels, erscheinen kann. Die Waffe hat die ganze Zeit über die gleiche Position, den gleichen Winkel und den gleichen Frame wie die erzeugende Entity (player1).

Die “Space” Taste löst die folgende Funktion aus:

function move_bullet1()
{
     bullet1 = me;
     my.skill10 = 1;
     my.pan = you.pan;
     my.enable_entity = on;
     my.enable_impact = on;
     my.enable_block = on;
     my.event = remove_bullet;
     while (my != null)
     {
          my.skill1 = 15 * time; // bullet speed
          my.skill2 = 0;
          my.skill3 = 0;
          move_mode = ignore_you + ignore_passable;
          ent_move (my.skill1, nullvector);
          wait (1);
     }
}

Jede Kugel, die Spieler 1 abfeuert, heißt “bullet1”. Wir benutzen die Action vom 1. Spieler um sicherzugehen, dass nur eine einzelne Entity mit Namen “bullet1” existieren kann und verhindern auf diese Weise Dauerfeuer.

Skill10 wird auf 1 gesetzt, um der Kugel eine einzigartige ID zu geben und dann stellen wir sicher, dass Kugel und Spieler zum Zeitpunkt des Feuerns dieselbe Ausrichtung haben. Die Kugel reagiert auf Entities und Level Blocks; wenn sie mit etwas kollidiert, wird das Event remove_bullet aufgerufen.

Die Kugel bewegt sich innerhalb einer Schleife und ignoriert dabei die erstellende Entity (Spieler 1) und alle passierbaren Objekte. Wie Sie sicher erraten haben ist das Event für die Kugel sehr simpel:

function remove_bullet()
{
     snd_play (hit_wav, 100, 0);
     wait (1);
     ent_remove (me);
}

Die Funktion läßt ein Geräusch ertönen und entfernt die Kugel. Schauen wir uns nun den Code für die Event Funktion von Spieler 1 an:

function kill_player1()
{
     if (you.skill10 != 1) {return;}
     my.event = null;
     p1_alive = 0;
     score2 += 1;
     my.skill46 = 0;
     snd_play (scream_wav, 100, 0);
     while (my.skill46 < 80)
     {
          ent_cycle("death", my.skill46);
          my.skill46 += 3 * time;
          wait (1);
     }
}

Die erste Zeile prüft, ob Spieler 1 wirklich von einer Kugel getroffen wurde, es wäre nicht nett, den Gegner töten zu können, einfach indem man in ihn läuft! Die zweite Zeile sagt dem Spieler, dass er von nun an nicht mehr auf Events reagieren soll – er ist bereits tot. Wir setzen p1_alive auf 0 (dies stoppt die Animationen), erhöhen “score2” (Spieler 1 ist gestorben), lassen ein Geräusch ertönen und spielen die “death” Animation einmal ab.

Die letzte Funktion, die wir uns ansehen sollten, ist für den Neustart des Spiels verantwortlich:

function play_again()
{
     snd_play (restart_wav, 100, 0);
     p1_alive = 1;
     p2_alive = 1;
     wait (2);
     main();
}

on_p play_again;
 
Diese Funktion läuft, wenn die “P” Taste gedrückt wird. Ein Geräusch ertönt, beide Spieler werden wiederbelebt (auch wenn einer von ihnen tot ist) und main() wird erneut aufgerufen. Wie Sie sehen, werden score1 und score2 nicht zurückgesetzt, Sie können also den ganzen Tag spielen, ohne die Punktzahl zu verlieren.