Wenn Dinge in deinem Verse-Code nicht so funktionieren, wie du es erwartest, ist es manchmal schwer zu verstehen, was schief gelaufen ist. Du kannst beispielsweise auf Folgendes stoßen:
Laufzeitfehler.
Code wird in der falschen Reihenfolge ausgeführt.
Prozesse, die länger dauern, als sie sollten.
Dies kann dazu führen, dass sich dein Code auf unerwartete Weise verhält und Probleme für dein Erlebnis verursacht. Die Diagnose von Problemen in deinem Code wird Debuggen genannt, und es gibt verschiedene Lösungen, die du zur Fehlerbehebung und Optimierung deines Codes verwenden kannst.
Verse-Laufzeitfehler
Dein Verse-Code wird sowohl beim Schreiben im Sprachserver als auch beim Kompilieren im Editor oder in Visual Studio Code analysiert. Diese semantische Analyse allein kann jedoch nicht alle möglichen Probleme erfassen, auf die du stoßen kannst. Wenn dein Code zur Laufzeit ausgeführt wird, kannst du Laufzeitfehler auslösen. Dadurch wird die Ausführung aller weiteren Verse-Codes gestoppt, was dein Erlebnis möglicherweise nicht mehr spielbar macht.
Als Beispiel nehmen wir an, dass du Verse-Code hast, der Folgendes tut:
# Has the suspends specifier, so can be called in a loop expression.
SuspendsFunction()<suspends>:void={}
# Calls SuspendFunction forever without breaking or returning,
# causing a runtime error due to an infinite loop.
CausesInfiniteLoop()<suspends>:void=
loop:
SuspendsFunction()Die Funktion CausesInfiniteLoop() würde keine Fehler im Verse-Compiler verursachen und dein Programm würde erfolgreich kompiliert werden. Wenn du jedoch CausesInfiniteLoop() zur Laufzeit aufrufst, wird eine Endlosschleife ausgeführt und damit ein Laufzeitfehler ausgelöst.
Navigiere zum Inhaltsservice-Portal, um Laufzeitfehler zu überprüfen, die in deinem Erlebnis aufgetreten sind. Dort siehst du eine Liste aller deiner Projekte, sowohl veröffentlichter als auch unveröffentlichter. Für jedes Projekt hast du Zugriff auf einen Verse-Tab, der die Kategorien der Laufzeitfehler aufführt, die in einem Projekt aufgetreten sind. Du kannst auch den Verse-Callstack überprüfen, in dem der Fehler gemeldet wurde, um weitere Details darüber zu erhalten, was schief gelaufen sein könnte. Fehlerberichte werden bis zu 30 Tage gespeichert.
Beachte, dass es sich dabei um eine neue Funktion handelt, die sich noch in der Entwicklung befindet. Die Art und Weise, wie dies funktioniert, kann sich in zukünftigen Versionen von UEFN und Verse ändern.
Profilerstellung für langsamen Code
Wenn dein Code langsamer läuft als du es erwartest, kannst du ihn mit dem profile-Ausdruck testen. Der profile-Ausdruck sagt dir, wie lange die Ausführung eines bestimmten Code-Teils dauert, und kann dir helfen, langsame Codeblöcke zu identifizieren und sie zu optimieren. Angenommen, du möchtest herausfinden, ob ein Array eine bestimmte Zahl enthält, und den Index zurückgeben, an dem sie erscheint. Dazu könntest du durch das Array iterieren und prüfen, ob die Zahl mit der gesuchten übereinstimmt.
# An array of test numbers.
TestNumbers:[]int = array{1,2,3,4,5}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Find if the number exists in the TestNumbers array by iterating
# through each element and checking if it matches.
for:
Index -> Number:TestNumbers
Dieser Code ist jedoch ineffizient, da er jede Zahl des Arrays auf eine Übereinstimmung prüfen muss. Dies führt zu einer ineffizienten Zeitkomplexität, da der Code, selbst wenn er das Element findet, den Rest der Liste weiter überprüft. Stattdessen kannst du die Funktion Find[] verwenden, um zu prüfen, ob das Array die gesuchte Zahl enthält, und sie zurückzugeben. Da Find[] sofort zurückkehrt, wenn es das Element findet, wird es umso schneller ausgeführt, je weiter vorne sich das Element in der Liste befindet. Wenn du einen profile-Ausdruck verwendest, um beide Code-Teile zu testen, wirst du feststellen, dass der Code mit der Find[]-Funktion in diesem Fall zu einer kürzeren Ausführungszeit führt.
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Find if the number exists in the TestNumbers array by iterating
# through each element and checking if it matches.
profile("Finding a number by checking each array element"):
for:
Index -> Number:TestNumbers
Number = 4
do:
Diese kleinen Unterschiede in der Ausführungszeit verstärken sich je mehr Elemente du iterieren musst. Jeder Ausdruck, den du ausführst, während du durch eine lange Liste iterierst, erhöht die Zeitkomplexität, insbesondere wenn deine Arrays auf Hunderte oder sogar Tausende von Elementen anwachsen. Wenn du deine Erlebnisse auf immer mehr Spieler skalierst, verwendest du den profile-Ausdruck, um wichtige optimierbare Bereiche zu finden und zu verbessern.
Logger und Protokollierungsausgabe
Wenn du in Verse-Code standardmäßig Print() aufrufst, um eine Nachricht auszugeben, schreibt diese Nachricht in ein spezielles Print-Log. Ausgegebene Nachrichten erscheinen auf dem Bildschirm im Spiel, im In-Game-Log und im Output-Log in UEFN.
Wenn du eine Nachricht mit der Print()-Funktion ausgibst, wird diese Nachricht in das Output-Log, in den Log-Tab des Spiels und auf den Bildschirm im Spiel geschrieben.
Es gibt jedoch viele Fälle, in denen du nicht möchtest, dass Nachrichten auf dem Bildschirm im Spiel angezeigt werden. Du möchtest Nachrichten vielleicht nachverfolgen, wenn etwas passiert, z. B. wenn ein Event ausgelöst wird oder eine bestimmte Zeit verstrichen ist, oder um zu signalisieren, dass etwas in deinem Code schief geht. Das Ausgeben mehrerer Nachrichten während des Spiels kann ablenken, vor allem, wenn sie dem Spieler keine relevanten Informationen liefern.
Um dieses Problem zu lösen, kannst du einen Logger verwenden. Ein Logger ist eine spezielle Klasse, mit der du Nachrichten direkt in das Output-Log und den Log-Tab ausgeben kannst, ohne sie auf dem Bildschirm anzuzeigen.
Logger
Um einen Logger zu erstellen, musst du zunächst einen Log-Kanal erstellen. Jeder Logger gibt Nachrichten ins Output-Log aus, aber es kann schwierig sein, zu erkennen, welche Nachricht von welchem Logger stammt. Bei Log-Kanälen wird der Name des Log-Kanals an den Anfang der Nachricht gesetzt, sodass leicht zu erkennen ist, welcher Logger die Nachricht gesendet hat. Log-Kanäle werden im Modulbereich deklariert, während Logger innerhalb von Klassen oder Funktionen deklariert werden. Im folgenden Beispiel siehst du, wie du einen Log-Kanal im Modulbereich deklarierst und dann einen Logger in einem Verse-Gerät deklarierst und aufrufst.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# A log channel for the debugging_tester class.
# Log channels declared at module scope can be used by any class.
debugging_tester_log := class(log_channel){}
# A Verse-authored creative device that can be placed in a level
debugging_tester := class(creative_device):
Wenn du eine Nachricht mit der Print()-Funktion eines Loggers ausgibst, wird diese Nachricht in das Output-Log und auf den Log-Tab im Spiel geschrieben.
Log-Level
Zusätzlich zu den Kanälen kannst du auch ein Standard- Log-Level festlegen, auf dem der Logger ausgibt. Es gibt fünf Levels, jedes mit eigenen Eigenschaften:
| Log-Level | Gibt aus auf | Besondere Eigenschaften |
|---|---|---|
Debuggen | In-Game-Log | N/A |
Verbose | In-Game-Log | N/A |
Normal | In-Game-Log, Output-Log | N/A |
Warnung | In-Game-Log, Output-Log | Die Textfarbe ist gelb. |
Fehler | In-Game-Log, Output-Log | Die Textfarbe ist rot. |
Wenn du einen Logger erstellst, ist er standardmäßig auf das Log-Level Normal eingestellt. Du kannst das Level eines Loggers ändern, wenn du den Logger erstellst, oder ein Log-Level angeben, auf dem beim Aufruf von Print() ausgegeben werden soll.
# A logger local to the debugging_tester class. By default, this prints
# to log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# A logger with log_level.Debug as the default log channel.
DebugLogger:log = log{Channel := debugging_tester_log, DefaultLevel := log_level.Debug}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
Im obigen Beispiel ist Logger standardmäßig auf den Log-Kanal Normal eingestellt, während DebugLogger standardmäßig auf den Log-Kanal Debug eingestellt ist. Jeder Logger kann auf jedes Log-Level ausgeben, indem du beim Aufruf von Print() das log_level angibst.
Ergebnisse der Verwendung eines Loggers zur Ausgabe in verschiedenen Log-Levels. Beachte, dass log_level.Debug und log_level.Verbose nicht in das In-Game-Log ausgegeben werden, sondern nur in das Output-Log von UEFN.
Ausgeben des Callstack
Der Callstack verfolgt die Liste der Funktionsaufrufe, die zum aktuellen Bereich geführt haben. Das ist wie ein gestapelter Satz von Anweisungen, die dein Code verwendet, um zu wissen, wohin er zurückkehren soll, wenn die Ausführung der aktuellen Routine abgeschlossen ist. Du kannst den Callstack von jedem Logger mit der Funktion PrintCallStack() ausgeben. Nimm zum Beispiel den folgenden Code:
# A logger local to the debugging_tester class. By default, this prints
# to log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Move into the first function, and print the call stack after a few levels.
LevelOne()
Der Code in OnBegin() oben ruft LevelOne() auf, um zur ersten Funktion zu gelangen. Dann ruft LevelOne() LevelTwo() auf, das LevelThree() aufruft, das Logger.PrintCallStack() aufruft, um den aktuellen Callstack auszugeben. Der letzte Aufruf steht oben im Stapel, also wird LevelThree() zuerst ausgegeben. Dann LevelTwo(), LevelOne(), und OnBegin(), in dieser Reihenfolge.
Wenn in deinem Code etwas schief geht, ist das Ausgeben des Callstacks praktisch, um genau zu wissen, welche Aufrufe zu diesem Punkt geführt haben. So ist es einfacher, die Struktur deines Codes zu sehen, während er ausgeführt wird, und du kannst einzelne Stapelverfolgungen in Projekten mit sehr viel Code isolieren.
Visualisierung von Spieldaten mit Debug-Zeichnen
Eine weitere Möglichkeit, verschiedene Funktionen deiner Erlebnisse zu debuggen, ist die Verwendung der Debug-Zeichnen-API. Diese API kann Debugging-Formen erstellen, um Spieldaten zu visualisieren. Hier sind einige Beispiele:
Die Sichtlinie einer Wache.
Die Distanz, um die ein Objektbeweger ein Objekt bewegt.
Der Dämpfungsabstand eines Audioplayers.
Mit diesen Debug-Formen kannst du deinem Erlebnis den Feinschliff verpassen, ohne diese Daten in einem veröffentlichten Erlebnis offenzulegen. Weitere Informationen findest du unter Debug-Zeichnen in Verse.
Optimierung und Timing mit Gleichzeitigkeit
Gleichzeitigkeit ist das Herzstück der Programmiersprache Verse und ein leistungsstarkes Werkzeug, um deine Erlebnisse zu verbessern. Mit Gleichzeitigkeit kannst du ein Verse-Gerät mehrere Vorgänge gleichzeitig ausführen lassen. Dies macht es möglich, flexibleren und kompakteren Code zu schreiben und die Anzahl der in deinem Level verwendeten Geräte gering zu halten. Gleichzeitigkeit ist ein großartiges Werkzeug zur Optimierung, und die Suche nach Möglichkeiten, asynchronen Code zu verwenden, um mehrere Aufgaben gleichzeitig zu bearbeiten, ist eine gute Möglichkeit, die Ausführung in deinen Programmen zu beschleunigen und Probleme im Zusammenhang mit dem Timing zu lösen.
Erstellen von asynchronen Kontexten mit Spawn
Der spawn-Ausdruck startet einen asynchronen Ausdruck aus einem beliebigen Kontext, während die folgenden Ausdrücke sofort ausgeführt werden können. So können mehrere Aufgaben gleichzeitig auf demselben Gerät ausgeführt werden, ohne dass für jede neue Verse-Dateien erstellt werden müssen. Stell dir zum Beispiel ein Szenario vor, in dem du einen Code hast, der jede Sekunde die Kondition jedes Spielers überwacht. Wenn die Kondition eines Spielers unter eine bestimmte Zahl fällt, willst du ihn um ein wenig heilen. Danach möchtest du einen Code ausführen, der eine andere Aufgabe behandelt. Ein Gerät, das diesen Code implementiert, könnte etwa so aussehen:
# A Verse-authored creative device that can be placed in a level
healing_device := class(creative_device):
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AllPlayers:[]agent = GetPlayspace().GetPlayers()
# Every second, check each player. If the player has less than half health,
# heal them by a small amount.
Da diese Schleife aber endlos läuft und nie abgebrochen wird, wird jeder folgende Code nie ausgeführt. Dies ist ein einschränkendes Design, da dieses Gerät nur den Schleifenausdruck ausführt. Damit das Gerät mehrere Dinge gleichzeitig tun und Code gleichzeitig ausführen kann, kannst du den Code aus der Schleife in eine asynchrone Funktion verschieben und ihn während OnBegin() spawnen.
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Use the spawn expression to run HealMonitor() asynchronously.
spawn{HealMonitor()}
# The code after this executes immediately.
Print("This code keeps going while the spawned expression executes")
HealMonitor(Players:[]agent)<suspends>:void=
Dies ist eine Verbesserung, da das Gerät nun anderen Code ausführen kann, während die Funktion HealMonitor() ausgeführt wird. Die Funktion muss aber trotzdem jeden Spieler in einer Schleife durchlaufen, und je mehr Spieler im Erlebnis auftreten, desto eher können Timing-Probleme auftreten. Was wäre, wenn du jedem Spieler Punkte basierend auf seinen KP geben oder prüfen möchtest, ob er einen Gegenstand hält? Das Hinzufügen einer zusätzlichen Pro-Spieler-Logik im for-Ausdruck erhöht die Zeitkomplexität dieser Funktion, und bei einer ausreichenden Anzahl von Spielern kann es sein, dass ein Spieler nicht rechtzeitig geheilt wird, wenn er aufgrund von Timing-Problemen Schaden nimmt.
Anstatt jeden Spieler in einer Schleife zu durchlaufen und einzeln zu überprüfen, kannst du diesen Code weiter optimieren, indem du eine Instanz der Funktion pro Spieler spawnen lässt. Das bedeutet, dass eine einzelne Funktion einen einzelnen Spieler überwachen kann, sodass dein Code nicht jeden einzelnen Spieler überprüfen muss, bevor er zu demjenigen zurückkehrt, der Heilung benötigt. Die Verwendung von Gleichzeitigkeitsausdrücken wie spawn zu deinem Vorteil kann deinen Code effizienter und flexibler machen und den Rest deiner Codebasis für andere Aufgaben frei machen.
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Spawn an instance of the HealMonitor() function for each player.
for:
Player:AllPlayers
do:
# Use the spawn expression to run HealMonitorPerPlayer() asynchronously.
Die Verwendung des spawn-Ausdrucks innerhalb eines loop-Ausdrucks kann bei unsachgemäßer Handhabung zu unerwünschtem Verhalten führen. Da zum Beispiel HealMonitorPerPlayer() nie endet, erzeugt dieser Code unendlich viele asynchrone Funktionen, bis ein Laufzeitfehler auftritt.
# Spawn an instance of the HealMonitor() function for each player, looping forever.
# This will cause a runtime error as the number of asynchronous functions infinitely increases.
loop:
for:
Player:AllPlayers
do:
spawn{HealMonitorPerPlayer(Player)}
Sleep(0.0)Steuerung des Timings mit Events
Es kann schwierig sein, jeden Teil deines Codes korrekt zu synchronisieren, insbesondere bei großen Multiplayer-Erlebnissen mit vielen Scripts, die gleichzeitig ausgeführt werden. Verschiedene Teile deines Codes können sich auf andere Funktionen oder Scripts verlassen, die in einer bestimmten Reihenfolge ausgeführt werden, was ohne strenge Kontrollen zu Timing-Problemen zwischen ihnen führen kann. Betrachte zum Beispiel die folgende Funktion, die für eine bestimmte Zeitspanne herunterzählt, und dann dem Spieler, der ihr übergeben wird, eine Punktzahl gibt, wenn seine Kondition größer als der Schwellenwert ist.
CountdownScore(Player:agent)<suspends>:void=
# Wait for some amount of time, then award each player whose HP is above the threshold some score.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)Da diese Funktion den Modifikator <suspends> hat, kannst du mit spawn() eine Instanz davon pro Spieler asynchron ausführen. Du musst jedoch garantieren, dass jeder andere Code, der sich auf diese Funktion stützt, immer nach ihrem Abschluss ausgeführt wird. Was ist, wenn du jeden Spieler ausgeben möchtest, der nach Ablauf von CountdownScore() Punkte erzielt hat? Du könntest dies in OnBegin() tun, indem du Sleep() aufrufst, um die gleiche Zeit zu warten, wie CountdownScore() für die Ausführung benötigt, aber dies könnte zu Timing-Problemen führen, wenn dein Spiel läuft und eine neue Variable einführt. Du musst sie ständig aktualisieren, wenn du Änderungen an deinem Code vornehmen willst. Stattdessen kannst du benutzerdefinierte Events erstellen und Await() darauf aufrufen, um die Reihenfolge der Events in deinem Code genau zu steuern.
# Custom event to signal when the countdown finishes.
CountdownCompleteEvent:event() = event(){}
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Spawn a CountdownScore function for each player
for:
Da dieser Code nun darauf wartet, dass das CountdownCompletedEvent() signalisiert wird, wird der Punktestand jedes Spielers garantiert erst nach der Ausführung von CountdownScore() geprüft. Viele Geräte haben eingebaute Events, bei denen du Await() aufrufen kannst, um das Timing deines Codes zu steuern, und durch die Nutzung dieser mit deinen eigenen benutzerdefinierten Events kannst du komplexe Spielschleifen mit mehreren beweglichen Teilen erstellen. Als Beispiel verwendet die Verse-Startervorlage mehrere benutzerdefinierte Events, um die Charakterbewegung zu steuern, die Benutzeroberfläche zu aktualisieren und die gesamte Spielschleife von Plan zu Plan zu verwalten.
Umgang mit mehreren Ausdrücken mit Sync, Race und Rush
Mit sync, race und rush kannst du mehrere asynchrone Ausdrücke gleichzeitig ausführen, während du verschiedene Funktionen ausführst, wenn diese Ausdrücke die Ausführung beendet haben. Durch die Nutzung jedes dieser Ausdrücke kannst du die Lebensdauer jedes deiner asynchronen Ausdrücke exakt steuern, was zu einem dynamischeren Code führt, der mit mehreren verschiedenen Situationen umgehen kann.
Nimm zum Beispiel den rush-Ausdruck. Dieser Ausdruck führt mehrere asynchrone Ausdrücke gleichzeitig aus, gibt aber nur den Wert des Ausdrucks zurück, der zuerst beendet wird. Angenommen, du hast ein Minispiel, bei dem Teams eine Aufgabe erfüllen müssen. Das Team, das zuerst fertig ist, erhält ein Powerup, mit dem es die anderen Spieler stören kann, während diese die Aufgabe abschließen. Du könntest eine komplizierte Timing-Logik schreiben, um zu verfolgen, wann jedes Team die Aufgabe abgeschlossen hat, oder du könntest den rush-Ausdruck verwenden. Da der Ausdruck den Wert des ersten asynchronen Ausdrucks zum Beenden zurückgibt, gibt er das Gewinnerteam zurück, während der Code, der die anderen Teams behandelt, weiter ausgeführt werden kann.
WinningTeam := rush:
# All three async functions start at the same time.
RushToFinish(TeamOne)
RushToFinish(TeamTwo)
RushToFinish(TeamThree)
# The next expression is called immediately when any of the async functions complete.
GrantPowerup(WinnerTeam)Der race-Ausdruck folgt denselben Regeln, außer dass die anderen Ausdrücke abgebrochen werden, wenn ein asynchroner Ausdruck abgeschlossen ist. So kannst du die Lebensdauer mehrerer asynchroner Ausdrücke gleichzeitig genau steuern, und du kannst dies sogar mit dem sleep()-Ausdruck kombinieren, um die Zeit zu begrenzen, in der der Ausdruck ausgeführt werden soll. Betrachte das rush-Beispiel, nur dass das Minispiel sofort enden soll, wenn ein Team gewinnt. Außerdem solltest du einen Timer hinzufügen, damit das Minispiel nicht ewig weitergeht. Mit dem race-Ausdruck kannst du beides tun, ohne dass du Events oder andere Gleichzeitigkeitswerkzeuge verwenden musst, um zu wissen, wann du die Ausdrücke abbrechen musst, die das Rennen verlieren.
WinningTeam := race:
# All four async functions start at the same time.
RaceToFinish(TeamOne)
RaceToFinish(TeamTwo)
RaceToFinish(TeamThree)
Sleep(TimeLimit)
# The next expression is called immediately when any of the async functions complete. Any other async functions are canceled.
GrantPowerup(WinnerTeam)Schließlich kannst du mit dem sync-Ausdruck warten, bis die Ausführung mehrerer Ausdrücke abgeschlossen ist, und er garantiert, dass jeder von ihnen abgeschlossen ist, bevor du fortfährst. Da der sync-Ausdruck ein Tupel zurückgibt, das die Ergebnisse jedes der asynchronen Ausdrücke enthält, kannst du die Ausführung aller deiner Ausdrücke abschließen und die Daten von jedem einzeln auswerten. Zurück zum Minispiel-Beispiel: Nehmen wir an, du möchtest stattdessen jedem Team Powerups geben, basierend darauf, wie es im Minispiel abgeschnitten hat. Hier kommt der sync-Ausdruck ins Spiel.
TeamResults := sync:
# All three async functions start at the same time.
WaitForFinish(TeamOne)
WaitForFinish(TeamTwo)
WaitForFinish(TeamThree)
# The next expression is called only when all of the async expressions complete.
GrantPowerups(TeamResults)Wenn du einen asynchronen Ausdruck für mehrere Array-Elemente ausführen möchtest, kannst du die praktische ArraySync()-Funktion verwenden, um zu garantieren, dass sie alle synchronisiert werden.
Jeder dieser Gleichzeitigkeitsausdrücke ist ein leistungsstarkes Werkzeug für sich, und wenn du lernst, wie du sie kombinieren und zusammen verwenden kannst, kannst du Code schreiben, der mit jeder Situation fertig wird. Betrachte dieses Beispiel aus der Vorlage Speedway Race mit Verse-Persistenz, die mehrere Gleichzeitigkeitsausdrücke kombiniert, um nicht nur für jeden Spieler ein Intro vor dem Rennen abzuspielen, sondern es auch abzubrechen, wenn der Spieler während des Intros das Match verlässt. Dieses Beispiel zeigt, wie du Gleichzeitigkeit auf verschiedene Arten nutzen und robusten Code erstellen kannst, der dynamisch auf verschiedene Events reagiert.
# Wait for the player's intro start and display their info.
# Cancel the wait if they leave.
WaitForPlayerIntro(Player:agent, StartOrder:int)<suspends>:void=
var IntroCounter:int = 0
race:
# Waiting for this player to finish the race and then record the finish.
loop:
sync: