Script-System

  • Das Script-System von LOTUS, mit dem die Fahrzeuge zum Leben erweckt werden.

    1 Allgemeines

    Die Syntax der Script-System folgt der Programmiersprache Pascal. Die Scriptsprache ist nicht objektorientiert.

    2 Bugs & Schwächen

    An dieser Stelle seien bekannte Bugs der Scriptsprache genannt, auf die wir leider keinen Einfluss haben, sowie Schwächen, die nicht ohne weiteres kompensiert werden können:

    • Ganzzahl-Division aus Versehen: Wird eine Division mit "/" durchgeführt, so erwartet man bei Pascal auf jeden Fall eine "echte" Division. 3/2 ergibt somit 0,5. Wird dies jedoch genau so in Pascal Script notiert, so werden die beiden Zahlen als Integer-Werte interpretiert (was soweit ok ist), dann aber auch eine Ganzzahl-Division durchgeführt, also das, was man normalerweise mit "div" macht, in diesem Beispiel ist das Ergebnis 1. Die Lösung ist die Schreibweise 3.0/2.0, welche erzwingt, dass der Compiler diese Konstanten als Single interpretiert, was wiederum zur Folge hat, dass das "/" ordnungsgemäß durchgeführt wird.
    • Apostrophe in Kommentaren: Es gibt generell Probleme, wenn sich in Kommentaren Apostrophe (') befinden, da sich hier der Script-Precompiler (wegen der String-Erkennung) verhustet. Daher sollten Apostrophe auch in Kommentaren nur für String-Darstellungen verwendet werden (z.B. wenn etwas auskommentiert wird), also insbesondere nicht "einzeln" auftreten.

    3 Aufbau

    Ein Objekt-Script kann entweder komplett in eine Hauptdatei geschrieben werden - oder es werden beliebig viele Variablen, Prozeduren oder Typendeklarationen in Unterdateien ausgelagert.


    Das Scriptsystem kennt dafür zwar keine uses-Klausel, aber dafür die folgende Compiler-Anweisung: {$I zusatzdatei.pas}. Somit sieht die Hauptdatei eines Scripts (mit Unterdateien) wie folgt aus:



    Die erste Sektion dient der Deklaration von extern ansteuerbaren, sogenannten öffentlichen Variablen. Die zweite Sektion definiert die im Script zur Anwendung kommenden Tastatur/Gamecontroller/Maus-Events. Dann folgt der Abschnitt der einzufügenden, weiteren Pascal-Dateien. Schließlich folgen vier Prozeduren, die in unterschiedlichen Situationen aufgerufen werden:


    • SimStep wird immer einmal pro Berechnungs-Zyklus aufgerufen. Die Dauer des letzten Berechnungszyklus' kann über die externe Variable Timegap ausgelesen werden.
    • Initialize wird einmalig nach dem Laden des Objektes auf der Karte durchlaufen. In ihm können z.B. die gewünschten Schalterstellungen gesetzt werden, die dem User präsentiert werden sollen, nachdem er das Fahrzeug platziert hat.
    • OnButton wird immer dann aufgerufen, wenn der User mit der Maus ein Element angeklickt hat, eine Tastatur- oder eine Gamecontroller-Taste gedrückt hat, der ein Ereignis zugeordnet ist. Sie verfügt über drei Parameter:
      • id gibt an, welches Event ausgelöst wurde, z.B. Throttle
      • value ist true, wenn die Taste gedrückt wurde, und false, wenn die Taste losgelassen wurde
      • cockpitIndex gibt an, in welchem Fahrerstand sich der User gerade befindet; auf diese Weise wird verhindert, dass der User in allen Fahrerständen gleichzeitig einen bestimmten Taster drückt.
    • OnFloatInput wird immer dann aufgerufen, wenn der User eine Gamecontroller-Achse betätigt, die einem Ereignis zugeordnet ist. Sie verfügt über drei Parameter:
      • id gibt an, welches Event ausgelöst wurde, z.B. ThrottleAxis
      • valuegibt den Wert der Achse an im Bereich 0..1
      • cockpitIndex gibt an, in welchem Fahrerstand sich der User gerade befindet; auf diese Weise wird verhindert, dass der User in allen Fahrerständen gleichzeitig einen bestimmten Hebel o.Ä. betätigt.

    4 Grundregeln zum Aufbau

    • Das einzige zwingend nötige Element ist das end. am Ende der Datei. Alle anderen hier vorgestellten Elemente sind optional.
    • Die Precompiler-Anweisung {$I zusatzdatei.pas} ist nicht mit der Uses-Klausel zu verwechseln: Es handelt sich lediglich um eine Copy-Paste-Anweisung. Genausogut könnte die Zusatzdatei an dieser Stelle statt dieser Anweisung per Copy-Paste eingefügt werden! Die Reihenfolge der Precompiler-Anweisungen untereinander und in Bezug zur Hauptdatei ist somit sehr relevant, da der Zugriff auf Methoden und Variablen immer nur in der Reihenfolge der Deklarationen geschehen kann.
    • Die PUBLIC_VARS- und die PUBLIC_BUTTONS-Sektionen dürfen nur in der Hauptdatei stehen! Derartige Sektionen in Zusatzdateien werden ignoriert.

    5 Beschreibungen der Sektionen

    5.1 Öffentliche Variablen / PUBLIC-VARS-Sektion

    Öffentliche Variablen dienen der Kommunikation des Scripts mit der "Außenwelt", d.h. den physikalischen Variablen wie Raddrehzahlen, der Animationssteuerung, den Sounds, Lichtern, Partikelsystemen usw.


    Im Gegensatz zu den "normalen" lokalen und globalen Variablen, die wie üblich in Pascal in entsprechend eingerichteten "var"-Sektionen deklariert werden, müssen diese öffentlichen Variablen immer und ausschließlich in der "PUBLIC-VARS"-Sektion deklariert werden. Werden sie zusätzlich über "var"-Deklarationen deklariert, kommt es zu Compiler-Fehlern.


    Diese Sektion muss immer mit {PUBLIC_VARS beginnen und die Variablen darin müssen alle mit Semikolons getrennt werden. Die folgende Vereinfachung ist jedoch nicht erlaubt: varA, varB, varC: single;.

    5.1.1 Spezielle Variablen

    Es gibt eine ganze Reihe von speziellen System-Variablen, die je nach Objekttyp mit entsprechenden Variablen des Hauptprogramms verknüpft werden. Sie müssen lediglich ganz "normal" in der PUBLIC-VARS-Sektion deklariert werden, dann werden sie automatisch verknüpft.


    Hier befindet sich eine Liste der System-Variablen mit den entsprechenden Angaben: System-Scriptvariablen und -Events .

    5.2 Input-Events / PUBLIC-BUTTONS-Sektion

    Wenn der User eine Taste auf der Tastatur oder am Gamecontroller drückt oder auf ein Fahrzeug-Element klickt, welches mit einer Event-ID verknüpft ist, dann wird die Script-Prozedur OnButton aufgerufen. Diese übermittelt u.A. die Event-ID, die mit der Taste oder dem Fahrzeug-Element verbunden ist. Auf diese Weise kann der Script-Programmierer den einzelnen Event-IDs Abläufe zuordnen.


    Zusätzlich müssen alle Event-IDs, die das Fahrzeug verarbeiten kann, in der PUBLIC_BUTTONS-Sektion aufgeführt sein. Diese Information wird genutzt, um dem Fahrzeug-Ersteller bzw. dem User eine Liste der verfügbaren Event-IDs zur Verfügung zu stellen, wenn er im Content-Tool die klickbaren Fahrzeug-Elemente konfiguriert bzw. seine Tastatur- und Gamecontroller-Einstellungen bearbeitet.


    Dafür gibt es die Sektion PUBLIC_BUTTONS, in der einfach alle verwendeten Input-Events nacheinander und durch Semikolons getrennt aufgelistet werden.

    5.3 Float-Events / PUBLIC-AXIS-Sektion

    Wenn der User eine Achse am Gamecontroller bewegt, welche mit einer Event-ID verknüpft ist, dann wird die Script-Prozedur OnFloatInput aufgerufen. Diese übermittelt u.A. die Event-ID, die mit der Taste oder dem Fahrzeug-Element verbunden ist. Auf diese Weise kann der Script-Programmierer den einzelnen Event-IDs Abläufe zuordnen.


    Zusätzlich müssen alle Event-IDs, die das Fahrzeug verarbeiten kann, in der PUBLIC_AXIS-Sektion aufgeführt sein. Diese Information wird genutzt, um dem Fahrzeug-Ersteller bzw. dem User eine Liste der verfügbaren Event-IDs zur Verfügung zu stellen, damit er im Content-Tool und im Simulator die Gamecontroller-Achsen zuweist.

    5.4 SimStep-Prozedur

    Bis auf ganz besondere Ausnahmen sollte jedes Script eine Prozedur mit der Deklaration procedure SimStep; enthalten.


    Die Simulationsberechnungen, wie die Berechnung der dreidimensionalen Grafik, der Physik, der KI und z.B. eben auch der Fahrzeugsysteme, müssen in möglichst hoher Frequenz ständig wiederholt werden, damit der User den Eindruck eines kontinuierlichen Ablaufes bekommt. Die meisten Zustandsvariablen verändern sich dabei von Bild zu Bild meistens nur ein wenig, da bei einer hohen Frequenz das entsprechende Zeitintervall, was der Berechnung zugrunde liegt, sehr klein ist. Bei 50 FPS sind es z.B. nur 20 Millisekunden.


    Alle Script-Prozesse, die sich ebenfalls ständig wiederholen müssen - z.B. die Simulation von pneumatischen oder elektrischen Systemen, Verzögerungs-Schaltern, Blinkrelais usw. - müssen deshalb ebenfalls vom Simulationsprozess ständig aufgerufen werden.


    Hierfür wird die SimStep-Prozedur genutzt: Sie wird immer wieder vom Simulationsprozess aufgerufen, mindestens einmal pro Bild, d.h. mindestens mit der Bildwiederholfrequenz (FPS, Frames Per Second). Für eine präzise Berechnung der Systeme, beispielsweise für Timer und Blinkrelais, wird die Systemvariable Timegap: single; zur Verfügung gestellt. Diese enthält die Dauer der Zeit, die seit dem letzten Aufruf von SimStep vergangen ist.

    5.5 SimStep_RC-Prozedur

    Für Fahrzeuge wird zusätzlich eine Prozedur mit der Deklaration procedure SimStep_RC; vorgesehen. Diese arbeitet genauso wie SimStep, wird jedoch alternativ aufgerufen, wenn sich das Fahrzeug im RC-Modus befindet, d.h. von der KI oder (über den Multiplayer) einem anderen User ferngesteuert wird. In diesem Fall soll das Script aus Performance-Gründen wesentlich simpler aufgebaut sein, da sich auf der Karte ja nur ein User-Zug befindet, jedoch zahlreiche KI-/MP-Züge befinden können.


    Verfügt das Fahrzeug über keine SimStep_RC-Prozedur, dann bleibt das Fahrzeug scriptmäßig komplett passiv.

    5.6 SimStep_LOD-Prozedur

    Standardmäßig stoppt das Script eines Objektes komplett, wenn es aufgrund der Entfernung nicht mehr dargestellt wird. Um dies zu verhindern, kann im Objekt die Option "Script läuft weiter, falls Obj. entfern.bedingt ausgeblendet wird" eingeschaltet werden. Dann wird grundsätzlich, wenn das Objekt nicht sichtbar ist, das LOD-Script ausgeführt.


    Handelt es sich dabei beispielsweise um eine Ampel oder ein Signal, dann kann man die Sichtbarkeit zumindest der Lichteffekt mit einer Mindestentfernung (Mindestsichtbarkeitsentfernung der Lichteffekte) versehen, sodass sie selbst dann noch aktiv sind, wenn das Objekt schon entfernungsbedingt ausgeblendet wird. Damit die Lichteffekte - sofern dynamisch - weiterlaufen können, gibt es die Möglichkeit, auch hierfür eine SimStep_LOD-Prozedur anzulegen. Diese kann dann entsprechend so einfach wie nur irgendwie möglich programmiert werden und wird LOTUS-seitig auch nur noch alle 10 Frames abgearbeitet.

    5.7 Initialize

    Die Prozedur mit der Deklaration procedure Initialize; wird dagegen nur genau einmal aufgerufen, nämlich unmittelbar nach dem Laden des Objektes und noch bevor das erste Mal die Prozedur SimStep aufgerufen wurde. Sie kann dafür genutzt werden, dass bestimmte Variablen einen bestimmten Zustand haben, wenn die Simulation startet. Soll beispielsweise der Batterie-Hauptschalter bereits eingelegt sein, wenn der User das Fahrzeug platziert, dann kann die entsprechende Zuordnung, z.B. battery_main := true;, dort erfolgen.

    5.8 InitializeAfterConstSet

    Diese Prozedur wird – nur bei Fahrzeugen und so wie Initialize – beim Laden des Fahrzeuges genau einmal aufgerufen, aber erst, nachdem die Fahrzeugkonstanten, die Nummer und das Kennzeichen in die jeweiligen Variablen geschrieben wurden.

    5.9 Finalize

    In speziellen Fällen kann es sinnvoll sein, auch nach dem Simulationsprozess bestimmte Dinge im Script zu erledigen. Für diesen eher seltenen Fall kann auch eine Prozedur procedure Finalize; angelegt werden.

    5.10 OnButton

    Eine sehr wichtige Prozedur ist außerdem noch die procedure OnButton(id: string; value: boolean; cockpitIndex: byte);. Diese wird immer dann aufgerufen, wenn der User ein Input-Ereignis auslöst (siehe oben).


    Abgesehen von der Event-ID, die als String übergeben wird, enthält die Prozedur außerdem den Parameter value - die Prozedur wird nämlich nicht nur beim Drücken einer Taste, sondern auch nach dem Loslassen aufgerufen. Je nachdem hat value den Wert true (soeben gedrückt) oder false (soeben losgelassen).


    Schließlich - für Fahrzeuge mit mehr als einem Fahrerstand - gibt der Parameter cockpitIndex an, aus welchem Fahrerstand heraus das Ereignis ausgelöst wurde. Im Falle dessen, dass das Ereignis mit einer Tastatur- oder Gamecontroller-Taste ausgelöst wurde, wird hierfür seitens der Simulationslogik geprüft, wo sich der Spieler gerade aufhielt. Im Gegensatz dazu wird beim Anklicken eines Elementes im Fahrzeug über dessen Konfiguration geprüft, zu welchem Fahrerstand das Element gehört. Es ist also bei Fahrzeugen mit mehrerern Fahrerständen immer darauf zu achten, dass die entsprechenden anklickbaren Fahrerstands-Elemente auch hinsichtlich der Maus-Konfiguration dem richtigen Fahrerstand zugeordnet werden.

    5.11 OnFloatInput

    Das Äquivalent zu OnButton für Gamecontroller-Achsen ist die procedure OnFloatInput(id: string; value: single; cockpitIndex: integer);. Diese wird immer dann aufgerufen, wenn der User eine Gamecontroller-Achse mit zugeordnetem Event betätigt (siehe oben).


    Abgesehen von der Event-ID, die als String übergeben wird, enthält die Prozedur außerdem den Parameter value - diese enthält den neuen Wert, der sich zwischen 0 und 1 bewegt.


    Der cockpitIndex hat hier dieselbe Funktion wie bei OnButton.

    6 Spezielle Prozeduren

    Neben den oben explizit aufgelisteten Prozeduren gibt es zusätzlich weitere vordefinierte Hilfsfunktionen.

    6.1 Mathematische Funktionen

    • Min(X, Y): Gibt den kleineren Wert von X und Y zurück (Variablentyp Single)
    • Max(X, Y): Gibt den größeren Wert von X und Y zurück (Variablentyp Single)
    • IntMin(A, B): Gibt den kleineren Wert von A und B zurück (Variablentyp Integer)
    • IntMax(A, B): Gibt den größeren Wert von A und B zurück (Variablentyp Integer)
    • Abs(X): Absolutwert von X (falls X kleiner 0 ist, wird der zugehörige positive Wert zurückgegeben)
    • Sign(X): Signum-Funktion: Falls X positiv, wird 1 zurückgegeben, falls negativ, dann -1, sonst 0.
    • Sqr(X): Quadrat von X
    • Sqrt(X): Wurzel aus X
    • Exp(X): Exponentialfunktion zur Basis E
    • Ln(X): Natürlicher Logarithmus (zur Basis E)
    • Power(base, exponent): Potenzfunktion, gibt "base hoch exponent" zurück
    • Sin(X): Sinus
    • Cos(X): Cosinus
    • Tan(X): Tangens
    • ArcSin: Arcus-Sinus (Umkehrfunktion vom Sinus)
    • ArcCos: Arcus-Cosinus (Umkehrfunktion vom Cosinus)
    • ArcTan: Arcus-Tangens (Umkehrfunktion vom Tangens)
    • Random: Zufallszahl zwischen (inklusive) 0 und (exklusive) 1

    6.2 Strings

    • UChar(id: word): Wandelt einen Unicode-Wert in das zugehörige Zeichen um. Mit einem oder mehreren dieser Zeichen lassen sich Strings mit Unicode-Zeichen erzeugen (siehe auch Text-Texturen)
    • RemSpacesBeginEnd(text: string): Entfernt Leerzeichen, Anführungszeichen ("), Zeilensprünge und Tabstop-Befehle vor und nach dem eigentlichen String
    • StrGrThan(A, B: string): Ist true, wenn "B" alphabetisch nach "A" folgt
    • StrSmThan(A, B: string): Ist true, wenn "A" alphabetisch nach "B" folgt
    • StrCutBegin(text: string; n: integer): Entfernt "n" Zeichen am Anfang des Strings
    • StrCutEnd(text: string; n: integer): Entfernt "n" Zeichen am Ende des Strings
    • StrSetLenL(text: string; n: integer): Setzt die Länge des Strings linksbündig auf "n" Zeichen. Ist er länger als "n" Zeichen, wird er rechts abgeschnitten, ist er kürzer, dann werden rechts Leerzeichen angefügt.
    • StrSetLenC(text: string; n: integer): Setzt die Länge des Strings zentriert auf "n" Zeichen. Ist er länger als "n" Zeichen, wird er beidseitig symmetrisch abgeschnitten, ist er kürzer, dann werden beidseitig symmetrisch Leerzeichen angefügt.
    • StrSetLenR(text: string; n: integer): Setzt die Länge des Strings rechtsbündig auf "n" Zeichen. Ist er länger als "n" Zeichen, wird er links abgeschnitten, ist er kürzer, dann werden links Leerzeichen angefügt.
    • text[i]: Wenn an den Variablennamen in eckigen Klammern eine Integer-Variable angehängt wird, erhält man das i. Zeichen des Strings zurück. Achtung: Diese Funktion ist 1-basiert, d.h. das erste Zeichen des Strings erhält man mit text[1]. Und ganz wichtig: Das funktioniert nicht mit Public-Variablen! Ggf. muss die Public-Variable in eine temporäre, lokale Stringvariable kopiert werden, an die dann wieder die eckigen Klammern angehängt werden dürfen.
    • RegEx(Input: string; Pattern: string): Prüft, ob der String "Input" dem Schema "Pattern" folgt. Die Funktion ruft dabei die Delphi-interne Funktion "TRegEx.IsMatch" auf.
    • RegExReplace(Input: string; Pattern: string; Replacement: string): Ersetzt alle durch "Pattern" definierten Schemata im "Input"-String durch den String "Replacement". Die Funktion ruft dabei die Delphi-interne Funktion "TRegEx.Replace" auf.

    6.3 Farben

    Farben werden intern als eine Cardinal-Zahl behandelt, in der die Komponenten Rot, Grün, Blau und Alpha codiert sind. Daher gibt es entsprechende Umwandlungsfunktionen:

    • Color(r, g, b, a): Codiert eine Cardinal-Zahl aus den einzelnen Komponenten
    • Red(col), Green(col), Blue(col), Alpha(col): Decodiert eine der Farbkomponenten aus einer Cardinal-Zahl

    6.4 Typenkonvertierungen

    Um zwischen verschiedenen Datentypen zu wechseln, gibt es folgende Konvertierungsfunktionen (Liste bisher nicht vollständig):

    • Trunc: Konvertiert einen Single-Wert in einen Integer-Wert, indem die Nachkommastellen abgeschnitten werden. Achtung: Das bedeutet, dass das Ergebnis bei positiven Zahlen kleiner als die Originalzahl ist, bei negativen Zahlen jedoch größer!
    • Round: Konvertiert einen Single-Wert in einen Integer-Wert, indem auf die nächstliegende Zahl gerundet wird.
    • IntToStr: Konvertiert einen Integer-Wert in einen String
    • StrToInt: Konvertiert einen String in einen Integer-Wert
    • FloatToStr: Konvertiert einen Single-Wert in einen String
    • StrToFloat: Konvertiert einen String in einen Single-Wert
    • IntToStrEnh(value: integer; n: integer; fill: string): Wandelt die Zahl "value" in einen String um mit der festen Länge "n". Hat die Zahl mehr als "n" Stellen, dann werden die rechten Stellen so abgeschnitten und ein "#" ergänzt, dass der String dann genau "n" Zeichen hat. Hat die Zahl weniger als "n" Stellen, dann wird die Differenz links unter Verwendung von "fill" aufgefüllt.
      Beispiel:
      • IntToStrEnh(12, 4, '0') ergibt "0012"
      • IntToStrEnh(2456, 3, '0') ergibt "24#"
    • function DecodeDateFullyGetLeap(const DateTime: single; var Year, Month, Day, DOW: Word): Diese Funktion dekodiert eine durch "DateTime" übergebene Datum+Zeit-Information in das Jahr, den Monat, den kalendarischen Tag und den Wochentag (DOW). Als boolean gibt die Funktion dabei zurück, ob es sich um ein Schaltjahr handelt.

    6.5 Stückweise lineare Funktionen

    Für besondere Zwecke können spezielle Funktionen definiert werden: Stückweise lineare Funktionen in Scripts

    6.6 Texturen

    Um die Texturen bestimmter Materialien scriptseitig einzustellen, muss eine Integer-Variable angelegt werden. Diese wird im entsprechenden Texturfeld des Materials ganz rechts in das Variablen-Feld eingetragen. LOTUS weist dem Material auf dem entsprechenden Kanal die Textur zu, die unter dem durch die Variable angegebenen, internen Index abgelegt ist. Da dieser im Allgemeinen unbekannt ist, muss für die Integer-Variable mit der folgenden Funktion der interne Index ermittelt werden, wobei hier ggf. die Textur auch geladen wird, sofern sie es nicht bereits ist.

    • GetTextureIndex(userID, contentSubID: integer), wobei userID und contentSubID die Textur identifizieren.

    6.7 Script-Textur-Zeichenbefehle

    Siehe hierzu bitte Text-Texturen.

    6.8 Module setzen

    Jedes Fahrzeug und jedes Szenerieobjekt kann mit Modul-"Slots" ausgerüstet werden, welche über verschiedene Wege mit separat entwickelten Modul-Objekten ausgerüstet werden können.


    Einer dieser Wege ist das Ausrüsten mit dem folgenden Script-Befehl:


    SetModule(self: integer; slotindex: integer; userID, contentSubID: integer)


    Mit diesem Befehl wird das Modul "userID:contentSubID" auf den Slot mit dem Index "slotindex" gesetzt.


    Mit GetModuleSet(self: integer; slotindex: integer): boolean kann geprüft werden, ob der Slot frei ist oder ob ein Modul eingestellt wurde.

    6.9 Force Feedback

    Das Gamecontroller-Force-Feedback wird per Script gesteuert. Hierfür gibt es drei Prozeduren, jeweils eine für jeden (unterstützten) Effekt. Mit dem Parameter axisID wird eingestellt, welche Achse gesteuert werden soll und je nach Effekt-Typ müssen ein oder bis zu drei weitere Parameter gesetzt werden – diese müssen sich im Allgemeinen in einem Bereich zwischen 0 und 1, ggf. auch -1 bis 0 bewegen.

    • procedure SetForceFeedbackCenterforce(axisID: string; coeficient, saturation, offset: single): Hiermit wird die Rückstellkraft gesteuert. Je nachdem, wie coeficient und saturation eingestellt werden, verändert sich die Charakteristik dieser Kraft. Wahlweise kann der "Mittelpunkt" auch mit offset verschoben werden (-1 = ganz links, 0 = Mitte, 1 = ganz rechts).
    • procedure SetForceFeedbackFriction(axisID: string; coeficient: single): Hiermit wird die Reibung gesteuert. Je höher coeficient eingestellt wird (maximal 1), desto schwergänger ist der Gamecontroller zu bewegen.
    • procedure SetForceFeedbackVibration(axisID: string; magnitude, period: single): Hiermit kann ein Vibrationseffekt hinzugefügt werden. magnitude steuert die Intensität, period die Frequenz der Vibration.