Autor Thema: [Scripting for Dummies] Missionen in bereits laufende Missionen laden - Teil 3  (Gelesen 4026 mal)

0 Mitglieder und 1 Gast betrachten dieses Thema.

Offline FG28_Kodiak

  • Blasenteetrinker
  • *****
  • Beiträge: 1.893
Nachdem wir in den letzten Zwei Lektionen zum Thema, die Missionen gleich zu Beginn in unsere laufende Mission geladen haben, werden wir dies in der heutigen zeitgesteuert machen. Zu diesem Zweck stellt uns das Spiel die Methode public virtual void OnTickGame() zur Verfügung, auch diese lässt sich für unsere Zwecke überschreiben.

Also
public override void OnTickGame()
Dieses OnTickGame wird vom Spiel ca. 34mal pro Sekunde aufgerufen, je nach Rechnerauslastung kanns mal mehr oder weniger sein.
Beispiel:
using System;
using maddox.game;
using maddox.game.world;

public class Mission : AMission
{
    public override void OnTickGame()
    {
        double WievielSekunden = 0;
       
        if (Time.tickCounter() == 334)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Meldung nach: {0} Ticks das sind {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
        if (Time.tickCounter() == 667)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Meldung nach: {0} Ticks das sind {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
        if (Time.tickCounter() == 1000)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Meldung nach: {0} Ticks das sind {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
   }
}
Das Beispiel macht nichts anderes als nach ca. 10, 20 und 30 sek eine kurze Meldung in die Chatleiste zu schreiben.
Time.tickCounter() enthält die Anzahl aller Ticks seit die Mission gestartet wurde und mit Hilfe von Time.TicksToSecs(Anzahl der Ticks), kann man diese Anzahl in Sekunden umrechnen.
GamePlay.gpLogServer (null, "Meldung nach: {0} Ticks das sind {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
führt dann zu folgender Ausgabe:


Man kann OnTickGame() auch verwenden, wenn man Zyklisch etwas ausgeben möchte.
Beispiel:
using System;
using maddox.game;
using maddox.game.world;

public class Mission : AMission
{
    public override void OnTickGame()
    {
        double WievielSekunden = 0;
       
        if (Time.tickCounter() % 334 == 0)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Erste Meldung: {0} Ticks = {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
        if (Time.tickCounter() % 509 == 0)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Zweite Meldung: {0} Ticks = {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
    }
}

Um unsere Meldungen zyklisch ausgeben zu können, verwenden wir hier den Modulo Operator aus C# also %. Der Modulo gibt den ganzzahligen Rest einer Division zurück. 3%3 wäre demnach 0, 3%2 wäre 1, 5%3 wäre 2 (näheres in http://en.wikipedia.org/wiki/Modulo_operation)
Die Abfrage Time.tickCounter() % 334 == 0 wird also immer Wahr wenn der Wert in tickCounter() ohne Rest Teilbar ist und gibt dann eine Meldung aus (ca alle 10sek.). Die 2te Abfrage gibt dann nach ca. 15sek (Na ja so ungefähr ;)) eine Meldung aus. Wobei beide gleich zum Start erscheinen da 0 durch irgendwas keinen Rest hat.


Möchte man keine Meldung (oder sonstiges) zu Beginn der Mission gibt man statt Null einen Wert an der nicht größer oder gleich dem Divisors ist.
Schlecht wäre z.B. if (Time.tickCounter() % 334 == 334) diese Abfrage könnte niemals WAHR werden. Genausowenig wie if (Time.tickCounter() % 334 == 335). Da es nie einen Rest geben kann der diesen Wert erreicht.
Beispiel wie sein sollte:
using System;
using maddox.game;
using maddox.game.world;

public class Mission : AMission
{
    public override void OnTickGame()
    {
        double WievielSekunden = 0;
       
        if (Time.tickCounter() % 334 == 333)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Erste Meldung: {0} Ticks = {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
        if (Time.tickCounter() % 509 == 508)
        {
            WievielSekunden = Time.TicksToSecs(Time.tickCounter());
            GamePlay.gpLogServer (null, "Zweite Meldung: {0} Ticks = {1} sek.\n", new object [] {Time.tickCounter(), WievielSekunden});
        }
    }
}

Ergebnis:


Wie jetzt heute nichts zum Kaputt machen? - Doch ;)

Die Hauptmission modifizieren wir durch hinzufügen von zwei Tarnnetzen, diese sollen nur das auftauchen und verschwinden der Fahrzeuge kaschieren, sind aber eigentlich nicht nötig.


Diesmal benötigen wir nur eine SubMission, diese beinhaltet ein einzelnes Fahrzeug das vom einem Ende der Landebahn zum anderen fährt.


Die Submission soll zyklisch geladen werden, sagen wir alle 30sek. dadurch entsteht der Eindruck das die Fahrzeuge in einigem Abstand hintereinander herfahren. Gerade bei Onlineservern entsteht aber mit Laufe der Zeit das Problem, das immer mehr Fahr- und Flugzeuge die Karte bevölkern, was dann zu Performance Problemen führt oder führen kann. In unserer Mission lassen wir das Fahrzeug daher nach 75sek verschwinden, das ist die Zeit die das Fahrzeug benötigt um von einem Tarnzelt zum anderen zu kommen. Der Spieler soll versuchen soviele Fahrzeuge wie möglich zu zerstören. Die Anzahl der Fahrzeuge (oder besser die Anzahl der Submissionen die geladen werden sollen) setzen wir mal auf 10 fest, wir könnten sie auch unendlich weiterlaufen lassen. Wir werden diesmal keine Trigger verwenden, sondern unsere Siegbedingung und das zählen der zerstörten Fahrzeuge im Code unterbringen.

Der Code mit Testmeldungen sieht folgendermassen aus:
using System;
using maddox.game;
using maddox.game.world;
using System.Collections.Generic;

public class Mission : AMission
{
   
    AiAircraft PlayerPlane;
    const int MaxAnzahlWellen = 10;
    int AnzahlWellen = 0;
    int ZerstoerteZiele = 0;
   
    public override void OnBattleStarted()
    {
        base.OnBattleStarted();
        MissionNumberListener = -1;
        PlayerPlane = (AiAircraft)GamePlay.gpPlayer().Place();
    }


    private void serverMessage(string msg)
    {
        GamePlay.gpLogServer (null, msg, new object [] {msg});
    }
   
   
    public override void OnTickGame()
    {

        if (Time.tickCounter() % 1000 == 0 && (AnzahlWellen < MaxAnzahlWellen))  //ca. alle 30sek die Karte laden
        {
            GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen5Sub1.mis");
            AnzahlWellen++;
            GamePlay.gpLogServer (null, "{0} nach {1} sek.\n", new object [] {AnzahlWellen, Time.TicksToSecs(Time.tickCounter())});//Testmeldung
        }
       
        if (Time.tickCounter() % 11500 == 0)     
        {
                    GamePlay.gpLogServer (null, "{0} nach {1} sek.\n", new object [] {AnzahlWellen, Time.TicksToSecs(Time.tickCounter())});//Testmeldung
                    GamePlay.gpHUDLogCenter("Sie haben "+ ZerstoerteZiele.ToString() + " von " + AnzahlWellen.ToString() + " Fahrzeugen zerstört" );
        }
    }
   
    public override void OnActorCreated(int missionNumber, string shortName, AiActor actor)
    {
        base.OnActorCreated(missionNumber, shortName, actor);

        if (actor is AiGroundActor)
        {
            Timeout(75, () => {
                if (actor != null)
                {
                    (actor as AiGroundActor).Destroy();
                }
            });
        }
    }
   
    public override void OnActorDead(int missionNumber, string shortName, AiActor actor, System.Collections.Generic.List<DamagerScore> damages)
    {
    base.OnActorDead(missionNumber, shortName, actor, damages);
       
        string KilledName;
     
      KilledName = missionNumber.ToString()+ ":0_Chief";
     
      if ((damages.Count != 0) && (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) && KilledName.Equals(actor.Name()))
      {
          ZerstoerteZiele++;
          serverMessage(ZerstoerteZiele.ToString());
        }
    }
}

So schauen wir den Code näher an
Als erstes fällt die neue using Anweisung auf:
using System.Collections.Generic; diese benötigen wir im Code um Zugriff auf die List Klasse von C# zu bekommen, die List-Klasse stellt Methoden zur Verfügung die das einfügen und auslesen (und einiges mehr) von Objekten in eine Liste ermöglichen. Dazu zu gegebenen Zeitpunkt mehr.

diesmal benötigen wir folgende Variablen die im Gesamten Kontext Gültigkeit besitzen:
AiAircraft PlayerPlane; Die Spiel Engine stellt uns die Klasse AiAircraft zur Verfügung, alle Flugzeuge im Spiel, außer die Statischen sind sozusagen Mitglieder dieser Gruppe.
const int MaxAnzahlWellen = 10; Diese Konstante legt die Anzahl der SubMissions Durchläufe fest.
int AnzahlWellen = 0; AnzahlWellen wird im Verlauf als Counter verwendet der die aktuelle Anzahl der Durchläufe speichert.
int ZerstoerteZiele; Diese Variable enthält die Anzahl der bereits zerstörten Ziele.

public override void OnBattleStarted()
{
    base.OnBattleStarted();
    MissionNumberListener = -1;
    PlayerPlane = (AiAircraft)GamePlay.gpPlayer().Place();
}

In OnBattleStarted legen wir mit MissionNumberListener = -1 fest, das alle Ereignisse aus den Missionen beachtet werden. Und mit Hilfe von:
PlayerPlane = (AiAircraft)GamePlay.gpPlayer().Place(); weisen wir der Variablen PlayerPlane das vom Spieler besetzte Flugzeug zu. Da in dieser Mission nur ein Flugzeug vorkommt indem der Spieler sitzen kann, genügt eine Zuweisung zu Beginn der Mission, sollten mehrere Flugzeuge im Spiel sein ist es sinnvoll vor wichtigen den Spieler betreffenden Ereignissen zu überprüfen ob dieser noch im Anfangsflugzeug sitzt und wenn nicht neu zuzuweisen. Aber sowas benötigen wir sicher in einer der späteren Lektionen. Zur Zuweisung stellt uns die IGamePlay-Klasse die Methode gpPlayer() zu Verfügung durch das (AiAircraft) teilen wir dem Spiel mit das wir in diesem Fall die Klasse AiAircraft meinen, da es mehrere abgeleitete Klassen gibt ist es notwendig dem System mitzuteilen, welche wir denn meinen (Der Player ist z.B. Mitglied der Klassen AiActor, AiCart, AiAircraft..). Place() gehört zur Playerklasse und teilt mit wo sich der Spieler gerade befindet. Zu beachten ist auch das man gpPlayer nur in Singlemissionen benutzen kann, in Multiplayerspielen kommt gpRemotePlayers zur Anwendung.
Wichtig ist momentan nur das man mit Hilfe von (AiAircraft)GamePlay.gpPlayer().Place() in einem Singleplayerspiel das Flugzeug des Spielers als Rückgabe bekommt.

public override void OnTickGame()
{
    if (Time.tickCounter() % 1000 == 0 && (AnzahlWellen < MaxAnzahlWellen))  //ca. alle 30sek die Karte laden
    {
        GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen5Sub1.mis");
        AnzahlWellen++;
        GamePlay.gpLogServer (null, "{0} nach {1} sek.\n", new object [] {AnzahlWellen, Time.TicksToSecs(Time.tickCounter())});//Testmeldung
    }
       
    if (Time.tickCounter() == 11500)     
    {
        GamePlay.gpLogServer (null, "{0} nach {1} sek.\n", new object [] {AnzahlWellen, Time.TicksToSecs(Time.tickCounter())});//Testmeldung
        GamePlay.gpHUDLogCenter("Sie haben "+ ZerstoerteZiele.ToString() + " von " + AnzahlWellen.ToString() + " Fahrzeugen zerstört" );
        MissionAbgeschlossen = true;
    }
}


OnTickGame()
if (Time.tickCounter() % 1000 == 0 && (AnzahlWellen < MaxAnzahlWellen)) Die Abfrage lädt die Karte jeweils alle 30 sek. und das solange die AnzahlWellen < MaxAnzahlWellen ist, in unserem Fall halt 10. ohne diesen "&& (AnzahlWellen < MaxAnzahlWellen)" würde die SubMission unendlich oft geladen werden, oder halt bis der Spieler keine Lust mehr hat. Wobei wenn ich mir überlege, waren die Namen AnzahlWellen und MaxAnzahlWellen bei einem Fahrzeug doch etwas übertrieben ;). Ist die Bedingung Wahr wird unsere SubMission geladen und dadurch das Fahrzeug auf den Weg gebracht. Dann noch die AnzahlWellen um eins erhöht und eine Kontrollausgabe in die Chatleiste ausgegeben, damit kann man evtl. die Zeiten noch anpassen, wie schon gesagt die Anzahl der Ticks pro Sekunde kann je nach Auslastung variieren. Sollte natürlich in der Endfassung entfernt werden.
if (Time.tickCounter() == 11500) ist unsere Abruchbedingung die 11500 Ticks sollten natürlich so gewählt sein das auch das letzte Fahrzeug die Strecke absolvieren kann. Diese Bedingung macht die Verwendung eines Zeittriggers überflüssig. Ist die Zeit also abgelaufen, wird eine Kontrollmeldung ausgegeben welche die Anzahl der Wellen und die dafür benötigte Zeit ausgibt, auch diese Meldung in der Endversion entfernen.
Es wird dann noch eine Meldung am Bildschirm ausgegeben:



public override void OnActorCreated(int missionNumber, string shortName, AiActor actor)
{
    base.OnActorCreated(missionNumber, shortName, actor);

    if (actor is AiGroundActor)
    {
        Timeout(75, () => {
            if (actor != null)
            {
                (actor as AiGroundActor).Destroy();
            }
        });
    }
}

Die Methode OnActorCreated(int missionNumber, string shortName, AiActor actor) wird jedesmal aufgerufen wenn ein neuer Actor erzeugt wird, ein Actor in Cliffs of Dover ist alles was sich entweder bewegen oder eine Funktion (z.B. Artillerie) hat, etwas was nur dumm in der Gegend steht gehört nicht dazu (also Gebäude, statische LKWs, statische Flugzeuge usw.).
Auch hier wird erstmal die base aufgerufen um sicher zu stellen das alles ordnungsgemäß initialisiert wird.
if (actor is AiGroundActor) hier wird geprüft ob der aktuelle actor zu der AiGroundActor Klasse gehört, in dieser Klasse finden sich alle Objekte die am Boden unterwegs sind, also fahrende LKWs, Artillerie, Schiffe etc. Ist die Abfrage WAHR wird als nächstes
Timeout(75, () => { Anweisungen }); ausgeführt. Bei Timeout wird zuerst die Zeit angegeben, nach der etwas ausgeführt werden soll, hier also 75sek. in den geschweiften Klammern befinden sich dann die auszuführenden Anweisungen, warum die Schreibweise mit () => etwas merkwürdig ist, würde den Rahmen dieser Lektion sprengen, so sagen wir halt ist halt so ;). Die Timeout Anweisung führt dann nach 75sek die If Abfrage aus, in dieser wird Abgefragt ob der Aktuelle actor auch vorhanden ist (er könnte ja schon an anderer Stelle entfernt worden sein), wenn nicht passiert nichts, wenn doch (actor as AiGroundActor) legen wir hiermit fest das der actor als AiGroundActor zu behandeln ist (wenn wir dies nicht machen bekommen wir eine Fehlermeldung da das System nicht weiß welchen Actor wir meinen) mit .Destroy() wird dieser dann aus der Mission restlos entfernt, verschwindet also einfach. Dies macht man um eine "Überbevölkerung" der Karte zu verhindern, also im wesentlichen bei Multiplayer Karten (da ist die DeSpawn-Zeit natürlich wesentlich größer) die etliche Zeiten laufen. Aber hier passt es auch ganz gut rein ;).

public override void OnActorDead(int missionNumber, string shortName, AiActor actor, System.Collections.Generic.List<DamagerScore> damages)
{
    base.OnActorDead(missionNumber, shortName, actor, damages);
       
    string KilledName;
           
    KilledName = missionNumber.ToString()+ ":0_Chief";
           
    if ((damages.Count != 0) && (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) && KilledName.Equals(actor.Name()))
    {
        ZerstoerteZiele++;
        serverMessage(ZerstoerteZiele.ToString()); //Testmeldung
    }
}

Um zu registrieren ob wir ein Ziel zerstört haben, überladen wir die Methode OnActorDead(..)
public override void OnActorDead(int missionNumber, string shortName, AiActor actor, System.Collections.Generic.List<DamagerScore> damages)
Diese Methode wird vom System jedesmal aufgerufen wenn ein Actor zerstört wird (oder besser Handlungsunfähig ist) und zwar geschieht das zweimal einmal für das Fahrzeug und dann für den imaginären Insassen, hier merkt man  das im Spiel auch vorgesehen ist, das Bodenfahrzeuge auch von menschlichen Spielern gesteuert werden können. Da wir nicht möchten das unser "DeadCounter" zweimal um eins erhöht wird führen wir die Variable KilledName vom Typ String ein. KilledName setzt dann den zu beachtenden Namen zusammen, hier muss man sich entscheiden denn actor.Name() (also der Name des zerstörten Ziels) hat einmal den Wert "1:0_Chief" und beim zweiten Aufruf "1:0_Chief0" (die eins variiert je nach Missionsnummer), die Nummer nach Chief nach dem Insassen der draufgegangen ist. Den Grundnamen findet man in der Mis-Datei z.B. bei unserer SubMission hier:
[Chiefs]
  0_Chief Vehicle.Austin_10_Tilly gb
Man kann natürlich die Mis-Datei editieren und dort einen eigenen einsetzen.
Ich habe mich für "Missionnummer:0_Chief" entschieden, also dem Fahrzeug selbst.
if ((damages.Count != 0) && (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) && KilledName.Equals(actor.Name()))
So die Erklärung für die If-Anweisung wird wieder etwas umfangreicher ;)
damages.Count != 0 damages ist vom Typ System.Collections.Generic.List<DamagerScore>, na ja eigentlich kein Typ sondern eine Liste, diese Liste wird uns von C# durch (using System.Collections.Generic;) zur Verfügung gestellt. Diese Liste enthält Elemente vom Typ DamagerScore, in dieser Liste werden vom Spiel alle Aktoren abgelegt die an der Zerstörung dieses Objekts (Actor) beteiligt waren. Das können mehrere Spieler als auch KIs gewesen sein. Mit Hilfe von .Count kann man die Anzahl der Elemente in der Liste abfragen, ist dieser Wert Null ist niemand beteiligt gewesen, die Abfrage ist hier nötig da auch die Methode .Destroy() die OnActorDead- Methode auslöst, da uns aber nur die vom Spieler zerstörten Fahrzeuge interessieren, ignorieren wir die vom System beseitigten einfach.
&& (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) hiermit stellen wir sicher das auch der Spieler derjenige ist der das Fahrzeug zerstört hat PlayerPlane.Name() enthält den Namen des Piloten (in meinem Falle also Kodiak) und damages[0].initiator.Actor.Name() den Namen desjenigen der als letztes das Fahrzeug (oder Flugzeug) beschossen hat. damages[0] gibt an das das erste Element in der Liste damages gemeint ist, in unserem Fall genügt das da nur ein Flugzeug und ein Spieler unterwegs waren. Beides wird miteinander verglichen und wenn übereinstimmung wird dieser Teil Wahr. Mit && KilledName.Equals(actor.Name() wird dann noch abgefragt ob das Ziel mit unserem gewünschten übereinstimmt. Ist also die komplette If-Abfrage Wahr, wird der ZerstoerteZiele Counter um eins hochgezählt und eine kurze Testmeldung auf dem Bildschirm ausgegeben.

Aber keine Angst in den nächsten Lektionen wird das ganze noch vertieft.
Wie immer bin ich für Kritik und Anregungen dankbar.

Das Attachment enthält die heutigen Beispielmissionen.



[gelöscht durch Administrator]
« Letzte Änderung: 07.Juni.2011, 11:41 von Deichwart »

Offline Hyde

  • Flugschüler
  • ***
  • Beiträge: 134
Geil Kodiak  ;D

Ich glaub ich muss das noch 10 mal Lesen bis ich es versteh, .....wenn ich es je Verstehe.


Offline FG28_Kodiak

  • Blasenteetrinker
  • *****
  • Beiträge: 1.893
Dat werden wir noch bis zum erbrechen durchexerzieren, keine Angst.
Werde es zumindest in der nächsten Lektion mit ein paar Beispielen vertiefen.
Hab die Missionen noch hochgeladen, ist einfach zu Schwül heute hatte ich glatt vergessen.
Die Rechtschreibfehler korrigier ich auch noch  ;D

Offline III./JG27_Culli

  • Hallenfeger
  • **
  • Beiträge: 39
Zitat
Die Rechtschreibfehler korrigier ich auch noch

Das passt schon. Da hab ich schon wesentlich schlimmeres hier gesehen  ;D

Offline tbag

  • Hallenfeger
  • **
  • Beiträge: 12
Vielen Dank fuer Deine genialen Tutorials Kodiak. Endlich mal wieder mal eine gute Moeglichkeit zum Programmieren.

Offline LoHan

  • Blasenteetrinker
  • *****
  • Beiträge: 2.301
    • Zerstörergeschwader 1
Jo goil...wie zu C64iger Zeiten....wo iss nur meine Datasette :D

Offline FG28_Kodiak

  • Blasenteetrinker
  • *****
  • Beiträge: 1.893
Ich muss dich enttäuschen, auf PEEK und POKE bin ich bislang noch nicht gestoßen   ;D