Voriges: Bones

Zauberer im Duell. Kollisionen. Zustandsmaschinen

Wenn Sie Ihre Hausaufgaben am Ende des Animationsworkshops gemacht haben, wissen Sie bereits, wie man eine Entity zum Gehen bringt. In diesem Workshop packen wir unser ganzes Wissen zusammen und erstellen die Grundlage eines Shooterspiels mit einem Player, einem Feind und einer Dritte-Person-Kamera. Deshalb ist dieser Workshop etwas länger - tatsächlich ist er ein Meilenstein in der Spieleentwicklung! Bitte beachten Sie, dass hierzu die Version A7 7.82 oder höher benötigt wird - mit älteren Versionen funktioniert es nicht.

Öffnen Sie shooter.c in workshop24, starten Sie es und lassen Sie den roten Zauberer mithilfe der Pfeiltasten herumgehen (oder eher -hinken):

Wir haben drei Entity-Aktionen im Skript, also unterteilen wir das Ganze. Schauen Sie zunächst nur auf die Aktion wizard_walk. Sie ist irgendwo in der Mitte des Skripts:

action wizard_walk()
{ 
  camera_follow(me);
 
  VECTOR vFeet;
  vec_for_min(vFeet,me); // vFeet.z = distance from player origin to lowest vertex
  
  my.STATE = 1;
  while (1)
  {
// state 1: walking ////////////////////////////////////////////
    if (my.STATE == 1)
    {
// rotate the entity with the arrow keys     
      my.pan += (key_cul-key_cur)*5*time_step;   

// move the entity forward/backward with the arrow keys
      var distance = (key_cuu-key_cud)*5*time_step;
      c_move(me, vector(distance,0,0), NULL, GLIDE);

// animate the entity    
      my.ANIMATION += 2*distance;
      ent_animate(me,"walk",my.ANIMATION,ANM_CYCLE);

// adjust entity to the ground height, using a downwards trace
      c_trace(my.x,vector(my.x,my.y,my.z-1000),IGNORE_ME | IGNORE_PASSABLE);
      my.z = hit.z - vFeet.z; // always place player's feet on the ground

      if (key_space) { // key pressed -> go to state 2
        my.ANIMATION = 0;
        my.STATE = 2;
      }
    }
    
// state 2: casting a spell /////////////////////////////////// 
    if (my.STATE == 2) 
    {
      my.ANIMATION += 8*time_step;
      ent_animate(me,"attack",my.ANIMATION,0);
      
      if (my.ANIMATION > 100) { // create the spell and go to state 3
        ent_create("spell.dds",vector(my.x,my.y,my.z+20),spell_fly);
        my.ANIMATION = 0;
        my.STATE = 3;
      }
    }  
    
// state 3: waiting for key release /////////////////////////// 
    if (my.STATE == 3) {
      if (!key_space) my.STATE = 1; // when key was released, back to state 1
    }
    
    wait(1);
  }
}

Am Anfang rufen wir camera_follow auf, damit die Kamera dem Player über die Schulter schaut - dazu kommen wir später. Dann machen wir einiges an Initialisierungskram.

VECTOR vFeet;
vec_for_min(vFeet,me);

Einen VECTORen kennen wir bereits aus früheren Workshops. Um die Position der Füsse des Players zu kriegen - wir müssen sie auf dem Boden platzieren, während er geht - verwenden wir den Vektoren vFeet. vec_for_mit setzt den Vektoren vFeet auf die Minimalkoordinaten des Modellmeshs. Da die Füsse für gewöhnlich der tiefste Teil des Körpers sind, enthält der Wert vFeet.z die Position der Füsse oder genauer der Sohle - in Relation zum Mittelpunkt des Modells. Wenn der Player z. B. 100 Einheiten groß ist und sein Mittelpunkt - der Punkt 0,0,0 seines Koordinatensystems- liegt präzise in seinem Zentrum, wird vFeet.z den Wert -50 haben, denn die Füsse befinden sich 50 Einheiten unterhalb des Zentrums.

Sie haben bemerkt, dass wir den Vektor direkt innerhalb der Funktion definiert haben - er ist lokal. Wir haben möglicherweise viele verschiedene Actors mit derselben Aktion, daher werden lokale Variablen zum Speichern der Koordinaten gebraucht.

Wofür brauchen wir nun die folgende Zeile?

my.STATE = 1;

Was ist my.STATE? Ein Entity-Parameter STATE ist nirgendwo im Handbuch aufgeführt! Aber wir haben ihn selbst definiert - schauen Sie an den Anfang des Skripts:

#define STATE  skill1
#define ANIMATION  skill2

Auf diese Weise können wir die Entity-Variable skill1, die für allgemeine Zwecke zur Verfügung steht, unter dem Namen "STATE" und skill2 unter dem Namen "ANIMATION" ansprechen. Auf gleiche Art können wir jede andere Skill-Variable einer Entity umbenennen in HEALTH, STRENGTH oder so ähnlich, einfach um zu verdeutlichen, wofür wir sie benutzen. Weil wir wollen, dass andere Funktionen darauf zugreifen können, benutzen wir einen Entity-Skill anstatt einer lokalen Variablen. Glauben Sie es oder nicht, diese harmlose Variable my.STATE ist in fast allen Computer-Games das Kernstück der künstlichen Intelligenz eines Actors!

Zustandsmaschinen

Der Ausdruck Künstliche Intelligenz (kurz "KI" oder aus dem Englischen "AI") wurde als erstes von Patrick H. Winston in seinem Buch "Artificial Intelligence" definiert und zwar als: "die Computerberechnungen, die es ermöglichen zu erkennen, schlußzufolgern und zu agieren." Akademische Studien über Künstliche Intelligenz sind faszinierend, Spieleprogrammierer interessieren sich aber i. d. R. eher für die Ergebnisse. Darum überlassen wir die Debatte ob eine Maschine tatsächlich Intelligenz besitzen kann, den Behaviouristen, Mechanisten und Dualisten und kümmern uns um das Schreiben von Code, der dafür sorgt, dass unsere Entities innerhalb unserer Spielewelt "erkennen, schlußfolgern und agieren".

Eine populäre und auch die in den meisten Games verwendete Technik zum Modellieren von AI ist als sogenannte state machine (Zusandsmaschine) bekannt. Schaut man sich die Struktur einer Zustandsmaschine, wie sie in dem Diagramm unten grafisch dargestellt ist, an, sieht man, dass sie aus weiter nichts besteht als einer Reihe von Zuständen (durch die Kugeln repräsentiert), die durch eine Reihe von Übergängen (durch die Pfeile repräsentiert) miteinander verbunden sind. Jede Entity startet in einem bestimmten Zustand und, je nach Eingabe, die sie erhält, wird sie von ihrem gegenwärtigen Zustand in einen anderen Zustand in der Maschine übergehen. Ein Gegner in einem Shooter-Spiel könnte beispielsweise die folgenden Verhaltenszustände haben: Warten (Wait), Angreifen (Attack), Flüchten (Escape), Sterben (Die) und Tot-Sein (Dead).


Solche Übergänge lassen sich durch Events wie Kollisionen oder das Drücken von Tasten oder Knöpfen auslösen. Beim Erstellen einer Entity-Aktion ist es sehr sinvoll, zunächst einmal ein Zustandsdiagramm wie das Obige zu zeichnen und die verschiedenen Zustände dann separat zu programmieren und zu testen. Wenn wir uns die Haupt-while()-Schleife unserer Zauberer-Aktion oben anschauen, können wir drei verschiedene Zustände und ihre Übergänge erkennen. Die Statements if(...) sorgen dafür, dass, in Abhängigkeit der Variablen my.STATE (mein Zustand), unterschiedliche Teile der Schleife ausgeführt werden.


Initialisiere die Entity-Einstellungen.

 State 1: Walk

(Zustand 1: Gehen )

Drehe und gehe durch die Pfeiltasten gesteuert.

[Leertaste] gedrückt => gehe über in Zustand 2.

 State 2: Attack

(Zustand 2: Angreifen)

Gehe durch die Zauberanimation. Erzeuge am Ende den Zauberspruch.

Animation beendet => gehe über in Zustand 3.

 State 3: Wait

(Zustand 3: Warten)

Tu nichts solange die Taste gedrückt bleibt.

[Leertaste] losgelassen => gehe zurück zu Zustand 1.

Das einzelne Statement wait(1) am Ende der Schleife wird jedes Mal und von jedem Zustand ausgeführt. Dadurch brauchen wir keine eigenen wait-Aufrufe in die Zustände selber zu setzen.

Werfen wir nun einen genaueren Blick in die einzelnen Zustände. Mit dem Geh-Zustand 1 fangen wir an.

Gehen & Kollisionen

if (my.STATE == 1)
{
  my.pan += (key_cul-key_cur)*5*time_step;
  var distance = (key_cuu-key_cud)*5*time_step;
  c_move(me, vector(distance,0,0), NULL, GLIDE);

Rechte und linke Pfeiltaste verringern oder erhöhen den pan-Wert des Players, was den Player nach rechts und links dreht. Die Pfeiltasten Auf und Ab werden zum Berechnen einer Vorwärtsstrecke, die in der Variablen distance gespeichert ist, benutzt. Auch diese ist, genau wie unser Vektor vFeet, eine direkt definierte, lokale Variable. Es ist sehr angenehm, lokale Variablen in einer Funktion und genau dort, wo man sie braucht, definieren zu können.

Wir verwenden die Variable distance zum Vorwärtsbewegen des Players und ausserdem, um ihn, seiner zurückgelegten Wegstrecke entsprechend, zu animieren.

  my.ANIMATION += 2*distance;
  ent_animate(me,"walk",my.ANIMATION,ANM_CYCLE);

ANIMATION ist ein weiterer dieser Entity-Skills für allgemeine Zwecke. Wir haben ihn, genauso wie STATE, am Anfang des Skripts mit einem Namen versehen. Wir benutzen ihn als einen Prozentwert zum Animieren. Wird der Animationsprozentsatz durch die Wegstrecke erhöht, wird die Entity ihrer Gehgeschwindigkeit entsprechend animiert. Der Faktor 2 läßt sich anpassen, so dass er auf andere Modelle passt, die eine längere oder kürzere Wegstrecke pro Animationszyklus zurücklegen.

  c_trace(my.x,vector(my.x,my.y,my.z-1000),IGNORE_ME | IGNORE_PASSABLE);
  my.z = hit.z - vFeet.z;

Diese beiden Zeilen sind zum Anpassen des Players an die Höhe des Terrains da. c_trace ist sowas wie ein Laser-Abstands-Sensor: es schickt einen Strahl zwischen zwei Positionen und stellt den Punkt fest an dem der Strahl auf ein Hindernis trifft. In unserem Fall schicken wir einen Strahl nach unten vom Positionsvektor des Players - gegeben von my.x, was als der Vektor (my.x,my.y,my.z) dient - an eine Position von 1000 Einheiten darunter - gegeben von vector(my.x,my.y,my.z-1000). Der Strahl trifft auf den Grund des Terrains und die Position, an der er auftrifft, wird im Vektor (hit.x,hit.y,hit.z) gespeichert. Wir addieren die relative Position der Füsse des Players (vFeet.z) und bekommen die Höhe auf der wir den Player platzieren müssen (my.z).

Ein Beispiel: wenn vFeet.z -50 ist und hit.z ist 100, müssen wir den Player vertikal auf 100 - 100 - (-50) = 150 platzieren, also 50 Einheiten über dem Grund.

Warum müssen wir aber überhaupt die Höhe des Players setzen wo doch die Kollisionserkennung c_move sowieso verhindern würde, dass er in den Boden einsinkt? Das stimmt, allerdings wäre das nicht billig. Jede Kollision verbraucht wertvolle Prozessor-Zeit. Würden wir einen Player mit permanenten Kollisionen über den Boden schleifen, hätte dies, bei gleichzeitig 1000 Playern und Feinden in einem Game, einen schlechten Einfluss auf die Framerate. Daher halten wir seine Kollisions-Box auf einer sicheren Höhe und vermeiden so jedwede Kollision solange er normal läuft.

Eine Kollisionsbox ist die Hülle um ein sich bewegendes Objekt und bestimmt dessen Reichweite bei der Kollisionserkennung. Sie wird von c_move beim Berechnen ob der Player mit etwas kollidiert oder nicht verwendet. In Wirklichkeit ist es keine Box sondern ein Ellipsoid, aber das braucht uns hier nicht zu kümmern. Drücken Sie zweimal auf [F11] und machen Sie so die Kollisionsbox sichtbar - es ist die hellblaue Drahtbox in obigem Screenshot.

Die Default-'narrow'-Box hat eine Größe von 32x32x32 Quants und wird allen Modellen mittlerer Größe beim Spielstart zugewiesen. Wie Sie sehen, ist sie um die Hüfte des Players herum zentriert. Normalerweise ist sie kleiner als der Player. Der Grund dafür ist der, dass sämtliche Player durch dieselben Türen passen und gleich nahe an Wände herangehen können sollen. Auch hat die Box ein wenig Abstand zum Untergrund, damit der Player gehen kann, ohne ständig zu kollidieren. Natürlich können wir die Kollisionsbox auch anders einstellen und ihr die tatsächliche Größe der Entity geben - das machen wir bald, weiter unten.

Angriff!

Als nächstes schauen wir uns den Angriffsmodus an, die Zustände 2 und 3. Drücken Sie die [Leertaste] und beobachten Sie was passiert:


if (key_space) {
  my.ANIMATION = 0;
  my.STATE = 2;
}

Die Taste wirft die Entity aus dem Zustand 1 heraus und direkt in den Zustand 2, der mit einer Zauberspruch-Animation startet.

if (my.STATE == 2)
{
  
my.ANIMATION += 8*time_step;
  ent_animate(me,"attack",my.ANIMATION,0);
  if (my.ANIMATION > 100) {
    ent_create("spell.dds",vector(my.x,my.y,my.z+20),spell_fly);
    my.ANIMATION = 0;
    my.STATE = 3;
  }
}

Sobald die Animation beendet ist (> 100%), wird die Zauberspruch-Entity erstellt und der Zauberer geht über in Zustand 3. Dieser letzte Zustand wartet einfach nur bis der Player die [Leertaste] losgelassen hat. Ansonsten würde der Zauberer wie ein Maschinengeweht Zaubersprüche abfeuern.

if (my.STATE == 3) {
  if (!key_space) my.STATE = 1;
}

Wird die [Leertaste] nicht erneut gedrückt, geht der Zauberer wieder in den Zustand 1 über. Das '!' ist die "nicht"-Operation in einer Programmiersprache und kehrt den folgenden Wert einfach um.

Ich habe nicht viel Erfahrung mit Zaubersprüchen - darum habe ich angenommen, so ein Spruch sieht so aus wie eine Art fliegende Energiekugel. Falls ich mich hier irre und ein echter Zauberer liest dies, korrigieren Sie mich bitte! Die Zauberkugel in Zustand 2 ist eine Sprite-Entity - in einem der vorangegangenen Workshops haben Sie was über Sprites gelernt - und, Sie haben es erraten, auch das ist eine Zustandsmaschine.

action spell_fly()
{
  my.ambient = 50;  // medium bright
  my.lightrange = 300; // activate dynamic light
  vec_set(my.blue,vector(255,50,50)); // bluish light color
  set(me,BRIGHT);   // additive blending

  vec_scale(my.scale_x,0.15); // small size
  c_setminmax(me);   // set my bounding box to my real size
  my.pan = your.pan; // face same direction as player
  my.STATE = 1;
  
  while(1)
  {
// state 1: flying ///////////////////////////////////////////  
    if (my.STATE == 1) 
    {
      my.roll += 20*time_step; 
      c_move(me,vector(40*time_step,0,0),NULL,IGNORE_YOU);
      if (HIT_TARGET)  // collided? 
        my.STATE = 2;  // go to state 2
    }

// state 2: exploding ////////////////////////////////////////  
    if (my.STATE == 2) 
    {
      set(me,ZNEAR);  // appear in front of close objects
      my.roll = random(360);
      my.lightrange *= 1+0.5*time_step; // increase light range
      vec_scale(my.scale_x,1+0.5*time_step); // inflate size
      if (my.scale_x > 1) { // explosion finished? 
        ent_remove(me);
        return; // terminate function to prevent furter access to removed entity
      }
    } 
    
    wait(1);  
  }
}

Und das ist unsere Zustandstabelle - etwas weniger kompliziert als die des Zauberers:


Initialisiere die Entity-Einstellungen.
 State 1: Fly
Fliege in die Richtung, in die der Zauberer geschaut hat, und drehe dich dabei.

Kollision mit einem Hindernis => gehe über zu Zustand 2.
 State 2: Explode
Werde schnell größer und erhöhe die Reichweite des Lichts.

Endgröße erreicht => entferne Entity.

Die ersten Zeilen der Aktion dienen nur der Show - sie setzen Helligkeit und dynamisches Licht.

action spell_fly()
{
  my.ambient = 50;
  my.lightrange = 300;
  vec_set(my.blue,vector(255,50,50));

  set(me,BRIGHT);

Die nächsten Zeilen sind wichtig. Ohne sie würde die Zauberkugel überhaupt nicht korrekt fliegen!

  vec_scale(my.scale_x,0.15);
  c_setminmax(me);
  my.pan = your.pan;
  my.STATE = 1;

Wir verwenden die Funktion vec_scale, um die Originalskalierung der Entity mit 0.15 zu multiplizieren, was uns die Größe einer kleinen Kugel gibt. Der folgende Aufruf von c_setminmax setzt die Kollisionsbox der Entity auf diese reduzierte Größe. Ohne c_setminmax würde die Entity auch die Default-Boxgröße, wie Sie sie in obigem Screenshot gesehen haben, bekommen. Die ist aber viel zu groß für die klein-skalierte Kugel und würde sie zu früh im Flug mit dem Terrain kollidieren lassen.

Der Flug geschieht in Zustand 1:

if (my.STATE == 1)
{
  my.roll += 20*time_step;
  c_move(me,vector(40*time_step,0,0),NULL,IGNORE_YOU);
  if (HIT_TARGET)
    my.STATE = 2;
}

Zum Drehen des Sprites wird der roll-Winkel erhöht, dann wird das Sprite nach vorne bewegt. IGNORE_YOU sorgt dafür, dass das Sprite nicht schon mit seinem Erzeuger, dem roten Zauberer kollidiert. Dieser ist am Beginn der Aktion seiner erzeugten Entities auf den Entity-Pointer you gesetzt. Seien Sie sich im Klaren darüber, dass der you-Pointer auch von vielen anderen Engine-Funktionen geändert wird. In einer komplizierteren Aktion müssen Sie ihn daher normalerweise in einer lokalen Variablen oder einem Entity-Skill speichern. Nach einem c_move-Aufruf, evaluiert das Makro HIT_TARGET auf ungleich Null, wenn die Entity mit etwas kollidiert ist. Dies löst den nächsten Zustand aus:

if (my.STATE == 2)
{
  set(me,ZNEAR);
  my.roll = random(360);
  my.lightrange *= 1+0.5*time_step;
  vec_scale(my.scale_x,1+0.5*time_step);
  if (my.scale_x > 1)
    ent_remove(me);
    return;
  }

}

Die Explosion ist nichts Besonderes - wir erhöhen lediglich schnell Größe und Lichtreichweite der Entity. Das Flag ZNEAR von Sprites sorgt dafür, dass das Sprite über nahegelegenen Objekten gerendert wird. Andernfalls würde die Explosion des Sprites vom Boden teilweise verdeckt werden und das sähe weniger realistisch aus. Der roll-Winkel wird jeden Frame auf einen Zufallswert zwischen 0 und 360 Grad gesetzt und gibt dem Sprite so einen Effekt von Flackern. Sobald die Originalgröße erreicht ist, wird das Sprite entfernt und die Funktion beendet. Würden wir die Funktion hier nicht per return zurückstellen, würde sie in der nächsten while-Schleife aufgrund eines Zugreifens auf eine nicht-existente Entity abstürzen.

Der böse Zauberer

Noch sind wir mit diesem Workshop nicht fertig. Lassen Sie den Zauberer den Pfad zur Linken entlangschlurfen. Bald wird er auf einen Kollegen treffen:

Nun müssen wir wissen, dass grüne Zauberer stets die Erzfeinde von roten Zauberern sind. Also belegen wir ihn mit unserem Zauberspruch!

Treffer! Wenn Sie gut gezielt haben, trifft die Kugel den grünen Zauberer genau in die Brust und haut ihn bewusstlos (wenn nicht schlimmer). Werfen wir einen Blick in den einfachen Code, der das bewerkstelligt.

function wizard_hit()
{
  my.STATE = 2;
}


action wizard_stand()
{
  my.event = wizard_hit;
  my.emask |= ENABLE_IMPACT;   
  my.STATE = 1;
  while (1)
  {  
// state 1: do absolutely nothing ////////////////////////////// 
    
// state 2: death animation //////////////////////////////////// 
    if(my.STATE == 2) 
    {
      my.ANIMATION += 3*time_step; 
      ent_animate(me,"death",my.ANIMATION,0); // animate the entity
      if (my.ANIMATION > 70) 
        return;
    }

    wait(1);
  }
}

In einem früheren Workshop haben wir bereits etwas über Maus-Events gelernt. Hier setzen wir einen Kollisionsevent ein. Wir erinnern uns, dass wir zwei Dinge brauchen: eine Event-Funktion - wizard_hit - die auf den Event-Parameter der Entity gesetzt ist und einen Freigabe-Flag, der auf den emask-Parameter der Entity gesetzt ist (der Operator '|=' setzt einen Flag). Der Flag ENABLE_IMPACT startet die Event-Funktion wenn eine andere Entity - in diesem Fall die Kugel - auf die Entity trifft. Es gibt viele andere Freigabe (enable)-Flags, die Event-Funktionen starten: wenn die Entity selbst mit etwas kollidiert, beschossen wird, entdeckt wird oder etwas in der Nähe explodiert oder so ähnlich. Hier setzt die Event-Funktion lediglich den Zustand, STATE, auf 2 und leitet die Sterbeanimation des Zauberers ein. Der Zustand 1 in der while-Schleife ist nicht-existent, denn er tut nichts anderes als warten.

Übrigens, darum haben wir für STATE einen Entity-Skill anstatt einer lokalen Variablen genommen: auf diese Weise kann von anderen Funktionen, wie hier von der Event-Funktion darauf zugegriffen werden.

Die Dritte-Person-Kamera

Das letzte Code-Stück ist das Kürzeste. Nehmen wir an, Sie haben immer noch nicht genug von Mathematik, und schauen sich die Funktion camera_follow an:

function camera_follow(ENTITY* ent)
{
   while(1) {
     vec_set(camera.x,vector(-150,10,25));
     vec_rotate(camera.x,ent.pan);
     vec_add(camera.x,ent.x);
     vec_set(camera.pan,vector(ent.pan,-10,0));
     wait(1);
   }
}

Das ist die einfachste Dritte-Person-Kamera. Die Funktion nimmt den Entity-Pointer als Argument und berechnet daraus die Kameraposition so, dass sie sich immer hinter der Entity befindet. Um das zu verstehen, berechnen wir mal ein Beispiel:

Angenommen, die Entity steht an der Position (100,200,300) und schaut Richtung Norden (pan = 90°). Zuerst setzen wir die Kameraposition relativ zum Player auf (-150,10,20) - das sind 150 Einheiten hinter ihm, sowie leicht nach rechts und nach oben verschoben, so, dass die Kamera über seine rechte Schulter schaut.

Camera position: (-150,10,20)

Dreht sich der Player, sollte die Kameraposition um ihn herumschwenken - also nehmen wir die Funktion vec_rotate, um die Kamera um den Playerwinkel zu drehen. Der Winkel ist in unserem Berechnungsbeispiel 90 Grad, also dreht sich die Position 90 Grad im Gegenuhrzeigersinn um den Koordinatenursprung. Um die neuen x- und y-Werte zu sehen, können Sie ein Diagramm zeichnen.

Camera position: (-10,-150,20)

Als nächstes addieren wir die Playerposition (100,200,300) mittels vec_add, und bestimmen so die tatsächliche Kameraposition, nicht relativ zum Player, sondern in Welt-Koordinaten. Wir bekommen:

Camera position: (90,50,320)

Die Kameraposition ist nun südlich des Players und leicht nach rechts oben verschoben, genauso wie wir das brauchen. Zum Schluß lassen wir die Kamera ebenso nach Norden schauen indem wir ihren pan-Winkel auf den Playerwinkel setzen, ihren tilt aber auf -10, so, dass sie leicht nach unten schaut. Wenn es Sie interessiert, finden Sie eine detailliertere Einweisung in die Verktorenmathematik im Kapitel Vektoren des Handbuchs.


Hausaufgabe: Verändern Sie den Code dahingehend, dass es ein faires Duell gibt. Der grüne Zauberer sollte erkennen, wenn der Rote sich ihm nähert oder eine Kugel geworfen wird und dann sollte er mit Kugeln zurückschiessen. Sie können sämtliche Engine-Kommandos benutzen, die Sie im Handbuch finden - c_scan beispielsweise ist prima zum Erkennen von Entities in der näheren Umgebung. Die Lösung finden Sie dann in shooter_2.c.

Weiterlesen: Multiplayer


Zum Weiterlesen : Gamestudio-Handbuch ► c_move, c_trace, vec_for_min, vectors.