Guten Tag,
in diesem Beitrag soll es um das Thema Scripting gehen. Die Grundlagen dazu wurden schon an verschiedenen anderen Stellen hier im Forum beschrieben. Daher möchte ich mich auf die Dinge beschränken, die mir in den Jahren so aufgefallen sind.
Bevor wir beginnen, müssen wir uns aber ein paar Dinge in Erinnerung rufen.
1. Bereiche des Skriptes
Auch wenn das gesamte Skript "nur" aus Funktionen (functions) besteht, lässt sich das Skript grob in zwei Bereiche einteilen. Einen Bereich der die im Szenario ausgelösten Ereignisse (events) verarbeitet und einen Bereich, der Bedingungen testen kann (testconditions).
2. Gültigkeit von Variablen
Variablen sind immer jeweils in dem Bereich gültig, in dem sie deklariert werden. Wird eine Variable also innerhalb eines Events deklariert, ist diese nur innerhalb des Events gültig. Auf den Wert dieser Variablen kann also nicht in anderen Funktionen zugegriffen werden.
GVAR1 = "ich gelte im gesamten Skript";
-- diese Variable gilt immer und überall
OnEvent = function(event)
if event == "Test1" then
VAR2 = "ich gelte nur im Event Test1";
-- diese Variable gilt nur in diesem Event
end
end
function TestCondition(condition)
if condition == "CheckHLL" then
currentBPP = SysCall("PlayerEngine:GetControlValue", "HLL", 0);
-- die Variable 'currentBPP' gilt nur in dieser TestCondition
if currentBPP >= 5.3 then
SysCall("ScenarioManager:TriggerDeferredEvent", "PressureReached",0);
end
end
end
Alles anzeigen
Was an dieser Stelle auch noch erwähnenswert ist, ist der Umstand, dass die innerhalb von Funktionen deklarierten Variablen nach dem Durchlauf der jeweiligen Funktion "vergessen" werden. Sie sind also nur zur Laufzeit innerhalb der Funktion vorhanden und können auch nur dann Werte aufnahmen bzw. Werte ausgeben.
3. Laufzeit
Ganz wichtiges Thema. Hier muss man wissen, dass der Bereich der Events und der TestConditions bei jedem Frame komplett durchlaufen werden. Ihr habt 30 FPS im Spiel? Super, dann wird das Skript 30 Mal pro Sekunde durchlaufen. Bei 50 FPS wäre das dann eben 50 Mal.
Warum das wichtig ist? Einmal sollte man daran denken, TestConditions auch wieder zu beenden. Es kann sonst nämlich zu unerwünschten Ergebnissen kommen. Zum Anderen sollte man die Anzahl der Durchläufe nicht für die Berechnung von Zeiten benutzen. Warum das weniger klug ist, soll folgendes Beispiel zeigen:
-- Start- und Randbedingungen
-- - es wird das Event 'Start' ausgelöst
-- - 10 Sekunden danach soll ein zweites Event 'Weiter' ausgelöst werden
-- - der TS läuft mit 30 FPS
if event == "Start" then
-- Überlegung: 30 Durchläufe pro Sekunde -> 300 Durchläufe in Sekunden
for i = 0, 300, 1
do
if i == 300 then
event = "Weiter";
end
end
end
if event == "Weiter" then
-- do some nice things
end
Alles anzeigen
Das sollte so zwar funktionieren, aber eben nur bei den Start- und Randbedingungen. Bei 60 FPS ist die Bedingung schon nach 5 Sekunden erfüllt.
Die Anforderung so zu lösen ergibt also keinen Sinn, da es dafür einen eigenen Aufruf für den TS gibt.
-- Start- und Randbedingungen
-- - es wird das Event 'Start' ausgelöst
-- - 10 Sekunden danach soll ein zweites Event 'Weiter' ausgelöst werden
-- - der TS läuft mit 30 FPS
if event == "Start" then
-- Aufruf "TriggerDeferendEvent"
SysCall("ScenarioManager:TriggerDeferendEvent", "Weiter", 10);
end
if event == "Weiter" then
-- do some nice things
end
Alles anzeigen
Das ist weniger Aufwand und führt sicher zum gewünschten Ergebnis.
Soweit, so gut. Wir wollen aber spannende Dinge mit dem Skript tun. Das Beispiel mit den Ansagen werde ich daher hier nicht wiederholen.
Achso, kurzer Einschub an dieser Stelle: Das was ich hier niederschreibe ist sicherlich nicht der einzige Weg um an Ziel zu kommen. Es gibt mit Sicherheit auch einfacherer oder elegantere Wege. Aber so wie ich das hier wiedergebe funktioniert es für mich.
Die große Frage, die sich stellt, ist dann natürlich: Woher soll ich denn wissen, wie die Dinger heißen, die ich per Skript steuern möchte?
Und diese Frage ist mehr als berechtigt. Die "Dinger" heißen Controller und haben selbst eigene Bezeichnungen. Ein paar wenige Standard-Controller heißen (fast) immer gleich. Die Anzahl und die Bezeichnung der Controller unterscheidet sich aber von Entwickler zu Entwickler. Selbst bei Fahrzeuges eines Entwicklers kann es unterschiedliche Bezeichnungen geben.
Ich habe bisher drei Möglichkeiten gefunden, um an die gewünschten Controllerbezeichnungen zu kommen.
- Logmate
Ja, das soll irgendwie gehen. Aber ich bin zu blöd dafür
- Fahrzeugdatei
Man kann sich die bin-Datei mit der serz.exe in eine mehr oder weniger lesbare XML-Datei umwandeln. In dieser sind die Bezeichnungen der Controller enthalten.
Das Image zeigt beispielhaft den Controller "HLL" einer BR185 von vR. Aber auch das ist mir zu mühsam.
- TS-MFD
Es gibt auf https://www.ts-mfd.de/ das wundervolle gleichnamige Programm. Dieses wurde wohl primär dafür entwickelt einen EBuLa abbilden zu können aber ich nutze die darin ebenfalls enthaltene Funktion zur Anzeige der Controller.
Vorteil des Programms: Ich sehe alle verfügbaren Controller und auch die Werte in "Echtzeit" während das Szenario läuft.
Ab hier habe ich also alle Informationen zusammen, um damit ein Skript basteln zu können, dass das Verhalten meines Fahrzeuges beeinflussen oder eben bestimmte Werte abfragen kann. Was man alles machen kann überlasse ich aber eurer Fantasie.
Damit diejenigen, die den Text bis hier hin gelesen haben auch etwas davon haben, hier ein erstes kleines Beispiel.
Ich fahre ja selbst unheimlich gern mit EBuLa-Fahrplänen. Darum erstelle ich bei meinen Szenarien auch immer Fahrpläne, sofern die Fahrzeuge diese auch anzeigen können. Außerdem fahre ich gern mit Fahrzeugen von virtual Railroads. Einziger Nachteil, die vR-Loks schalten nicht automatisch die einzelnen Seiten des Fahrplans durch. Zu erklären, warum das so ist, würde hier zu weit führen. Aber es ist eben so.
Man kann aber die Seiten am EBuLa im Fahrzeug selbst während des Szenarios durchblättern. Also muss es dafür einen Controller geben, der auf die Interaktion im Szenario reagiert. Die Controller heißt "EbulaPageUp" und muss von unserem Skript angesprochen werden.
Bevor wir das aber tun, sollten wir uns die Bedienhandlung als solche vor Augen führen. Also was passiert beim manuellen Umschalten des Fahrplans wirklich?
Man "drückt" auf den entsprechenden Knopf und lässt ihn auch wieder los. Die eine Aktion, also das Umschalten des Fahrplans, besteht aus zwei einzelnen Aktionen. Und diese beiden Aktionen müssen dann auch im Skript umgesetzt werden. Die große Erklärung, wie Schalter und Taster funktionieren und was es da für Unterschiede gibt, möchte ich mir und euch an dieser Stelle ersparen. Nur kurz so viel: Es gibt Schaltelemente, die auf eine ansteigende Flanke des Signals reagieren - also dem Wechsel von 0 auf 1 - und es gibt Elemente die auf einen abfallenden Flankenwechsel - also von 1 auf 0 - reagieren. Wer mehr dazu wissen möchte, möge die Suchmaschine seines Vertrauens nach Flipflops, NAND-Gatter und dergleichen befragen.
Für unsere konkrete Anforderung ist wichtig zu wissen, dass das Engine-Script auf einen negativen Flankenwechsel reagiert.
Der Controller hat im Grundzustand den Wert 0 und beim bestätigen des Buttons den Wert 1. Wird der Button losgelassen, nimmt der Controller wieder den Wert 0 an.
Im Skript kann es dann so aussehen:
-- -----------------------------------------------------------------------------------
-- EBuLa umschalten
-- dazu Trigger von Scarlet im Szenario verbauen
if event == "EBULANEXTPAGE" then
-- Controller auf den Wert 1 setzen
SysCall ( "PlayerEngine:SetControlTargetValue", "EbulaPageUp" , 0 , 1);
-- 'kurz' abwarten, und dann den Knopf wieder loslassen
SysCall ( "ScenarioManager:TriggerDeferredEvent" , "EBuLaNextPage_null" , 0.5 );
return TRUE;
end
if event == "EBuLaNextPage_null" then
-- loslassen --> Controller auf 0 setzen
SysCall ( "PlayerEngine:SetControlTargetValue", "EbulaPageUp" , 0 , 0);
return TRUE;
end
-- -----------------------------------------------------------------------------------
Alles anzeigen
Das ganze zur einfacheren Lesbarkeit noch als Image mit Syntax-Highlighting
Für das einfache Drücken von Knöpfen kommen wir auch ganz ohne Testconditions aus. Wichtig ist aus meiner Sicht wirklich nur, dass man alle Interaktionen genau betrachtet und diese Aktionen in ihre einzelnen Schritte zerlegt. Diese einzelnen Schritte kann man dann je nach Fahrzeug mehr oder weiniger einfach in einem Skript abbilden.
Nachtrag: Zum Auslösen des Events muss dieses natürlich im Szenario getriggert werden. Ich nutze dazu meist die Scripttrigger von Scarlet
Trigger platzieren, Bezeichnung des Events im Skript eintragen bzw per Copy&Paste einfügen und fertig.
Wenn ich noch weitere Themen niederschreiben soll, dann lasst es mich wissen. Aber als Denkanstoß kann dieser Beitrag sicher schon dienen.
Danke für das Lesen des Beitrages.
VG Ronald