Verse コードで期待どおりに動作しない場合、何が問題だったのかを把握するのが難しい場合があります。たとえば、次のような状況に遭遇する可能性があります。
- ランタイム エラー。
- 間違った順序でのコードの実行。
- 必要以上に長い処理時間。
これらはいずれもコードに作用して、ゲーム体験において想定外の挙動を引き起こしたり問題を発生したりする可能性があります。コード内の問題を診断する行為は デバッグ と呼ばれ、コードを修正して最適化するために使用できるさまざまなソリューションがあります。
Verse ランタイム エラー
Verse コードは、言語サーバーで記述するときと、エディタまたは Visual Studio Code からコンパイルするときの両方で分析されます。ただし、このセマンティック分析だけでは、発生する可能性のあるすべての問題を検出することはできません。ランタイム時にコードを実行すると、ランタイム エラー が発生する可能性があります。これにより、以降のすべての Verse コードの実行が停止し、プレイできなくなる可能性があります。
たとえば、次のようなことを実行する Verse コードがあるとします。
# suspends 指定子があるため、ループ式で呼び出すことができます。
SuspendsFunction()<suspends>:void={}
# 中断したり戻ったりせずに SuspendFunction を永久に呼び出します。
# 無限ループによりランタイム エラーが発生します。
CausesInfiniteLoop()<suspends>:void=
loop:
SuspendsFunction()
CausesInfiniteLoop()
関数は Verse コンパイラでエラーを発生せず、プログラムは正常にコンパイルされます。ただし、CausesInfiniteLoop()
をランタイム時に呼び出すと、無限ループが実行され、ランタイム エラーが発生します。
ゲーム内で発生したランタイム エラーについて調べるには、コンテンツ サービス ポータル に移動します。そこで自身の公開済みと未公開の両方のプロジェクトがすべて表示されます。プロジェクト内で発生したランタイム エラーのカテゴリが一覧表示されている、各プロジェクトの [Verse] タブにアクセスできます。また、そのエラーが報告された Verse コール スタックで、エラーを引き起こした原因について詳細をさらに確認することもできます。エラー レポートは最大 30 日間保存されます。

これは開発の初期段階にある新機能であるため、UEFN および Verse の今後のバージョンで動作方法が変更する可能性があることにご注意ください。
遅いコードのプロファイリング
コードの実行が想定よりも遅い場合、profile 式を使ってそれをテストできます。profile 式により、特定のコードの実行にかかる時間が分かるため、遅延しているコード ブロックを見きわめ、それを最適化できます。たとえば、配列に特定の数字が含まれるかどうかを調べて、含まれる場所にインデックスを返すとします。配列に対して反復することで、探している数字と一致するものがあるかどうかを確認できます。
# テスト番号の配列。
TestNumbers:[]int = array{1,2,3,4,5}
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
# 反復処理によって、TestNumbers 配列に番号が存在するかどうかを確認します
# 各要素を調べて一致するかどうかを確認します。
for:
Index -> Number:TestNumbers
数 = 4
do:
Print("Found the number at Index {Index}!")
しかしこのコードは、一致するかどうかを配列の各数字とチェックしなければならないため非効率です。要素を見つけたとしても、残りのリストのチェックも引き続き行うため、非効率な 時間的複雑度 が生じます。代わりに Find[]
関数を使うと、探している数字が配列に含まれているかどうかを調べて、それを返します。Find[]
は、要素を見つけたときに瞬時に返すため、その要素がリストの早い段階で見つかるほど早く実行されます。profile
式も使って両方でコードをテストしてみれば、Find[]
式を使った方が、コードの実行時間が短いことが分かります。
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
# 反復処理によって、TestNumbers 配列に番号が存在するかどうかを確認します
# 各要素を調べて一致するかどうかを確認します。
profile("配列の各要素をチェックして数値を見つける"):
for:
Index -> Number:TestNumbers
数 = 4
do:
Print("Found the number at Index {Index}!")
# Find if the number exists by using the Find[] function.
profile("Finding a number using the Find[] function"):
if:
FoundIndex := TestNumbers.Find[4]
then:
Print("Found the number at Index {FoundIndex}!")
else:
Print("Failed to find the number!")
このような実行時間のわずかな差異は、反復する必要のある要素が増えるほど大きくなっていきます。広範なリストを反復しながら式を実行するたびに、時間的複雑度が増し、配列の要素数が数百や数千まで増えるにつれ、その差は顕著に表れます。profile
式は、遅延の主な領域を見つけて対処し、より多くのプレイヤーにゲーム体験を拡張するために使用してください。
ロガーとロギング出力
Verse コードで Print()
を呼び出してメッセージを印刷するとき、デフォルトでは専用の Print
ログにメッセージが書き込まれます。印刷されたメッセージはゲーム内の画面、ゲーム内のログ、UEFN の 出力ログ に表示されます。
Print() 関数を使ってメッセージを印刷するとき、そのメッセージは 出力ログ、ゲーム内の ログ タブ、ゲーム内の画面に書き込まれます。
しかし、メッセージをゲーム内の画面に表示したくないことも多々あります。メッセージは、イベントの発生や一定時間の経過といったことを追跡したり、あるいはコードに問題が発生したときにシグナルを受け取ったりするために使うと便利です。しかしゲームプレイ中に複数のメッセージが表示されていると注意力が妨げられます。それがプレイヤーに関係する情報でなければなおさらです。
それを解決するために、ロガー を使用することができます。ロガーは、メッセージを画面に表示することなく、直接的に 出力ログ と ログ タブに印刷できるようにする特別クラスです。
ロガー
ロガーを構築するには、まず ログ チャンネル を作成する必要があります。どのロガーも出力ログにメッセージを印刷しますが、そのような状態ではメッセージの発生元のロガーを見分けるのが難しくなります。ログ チャンネルは、メッセージの先頭にログ チャンネル名を付けるため、メッセージを送信したロガーを簡単に確認できるようになります。ログ チャンネル はモジュール スコープで宣言され、ロガーはクラス内または関数内で宣言されます。以下の例では、ログ チャンネルをモジュール スコープで宣言し、Verse の仕掛け内でロガーを宣言して呼び出しています。
using { /Fortnite.com/Devices}
using { /Verse.org/Simulation}
using { /UnrealEngine.com/Temporary/Diagnostics}
# debugging_tester クラスのログ チャンネル。
# モジュール スコープで宣言されたログ チャンネルは、どのクラスでも使用できます。
debugging_tester_log := class(log_channel){}
# レベルに配置できる、Verse で作成したクリエイティブの仕掛け
debugging_tester := class(creative_device):
# debug_tester クラスのローカル ロガー。
Logger:log = log{Channel := debugging_tester_log}
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
Print("This is the print channel speaking!")
Logger.Print("And this is the debugging tester speaking!")
ロガーの Print() 関数を使ってメッセージを印刷するとき、そのメッセージは 出力ログ とゲーム内ログ ログ タブに書き込まれます。
ログ レベル
チャンネルだけでなく、ロガーの印刷先の ログ レベル もデフォルトで指定することができます。5 つのレベルがあり、それぞれに独自のプロパティがあります。
ログ レベル | 印刷先 | 特別なプロパティ |
---|---|---|
デバッグ | ゲーム内ログ | N/A |
詳細 | ゲーム内ログ | N/A |
通常 | ゲーム内ログ、出力ログ | N/A |
警告 | ゲーム内ログ、出力ログ | テキストの色は黄色です |
エラー | ゲーム内ログ、出力ログ | テキストの色は赤です |
ロガーを作成するときは、デフォルトで Normal
ログ レベルになっています。ロガーを作成するときにロガー レベルを変更するか、印刷のために Print()
を呼び出すときのログ レベルを指定しておくこともできます。
# debug_tester クラスのローカル ロガー。 デフォルトでは、これは次のように印刷されます
# 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}
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
# デフォルトでは、ロガーは通常のログ チャンネルに印刷され、デバッグ ロガーは
# デバッグ ログ チャンネルに印刷されます。どのロガーでも、任意のレベルに印刷できます。
# specifying the ?Level argument when calling Print()
Logger.Print("This message prints to the Normal log channel!")
DebugLogger.Print("And this message prints to the Debug log channel!")
Logger.Print("This can also print to the Debug channel!", ?Level := log_level.Debug)
上記の例では、Logger
のデフォルトは Normal
ログ チャンネル、DebugLogger
のデフォルトは Debug
ログ チャンネルになっています。どのロガーでも、Print()
を呼び出すときに log_level
を指定することにより、任意のログ レベルで出力できます。
1 つのロガーを使って異なる複数のログ レベルに出力した結果。log_level.Debug と log_level.Verbose はゲーム内ログには印刷せず、UEFN 出力ログ にのみ印刷することに注意してください。
コール スタックの印刷
コール スタック は、現在のスコープに至るまでの関数 Unreal Editor for Fortnite の用語集 リストをトラッキングします。これは、現在のルーチンの実行が終了したらどこに戻るべきかをコードが認識するために使用する、積み重ねられた命令セットのようなものです。PrintCallStack()
関数を使うことで、あらゆるロガーのコール スタックを印刷できます。例として以下のコードを取りあげます。
# debugging_tester クラスに対してローカルであるロガー。デフォルトでは、これは次のように印刷されます
# log_level.Normal に設定します。
Logger:log = log{Channel := debugging_tester_log}
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
# 最初の関数に取り込まれ、数レベル後にコール スタックを出力します。
LevelOne()
# LevelTwo() を呼び出して 1 レベル下位へ移動。
LevelOne():void=
LevelTwo()
# LevelThree() を呼び出して 1 レベル下位へ移動。
LevelTwo():void=
LevelThree()
# この時点に至るまでの関数呼び出し順に
# コール スタックを出力します。
LevelThree():void=
Logger.PrintCallStack()
上位の OnBegin()
のコードが LevelOne()
を呼び出して第 1 の関数に移ります。次に LevelOne()
が LevelTwo()
を呼び出します。これが LevelThree()
を呼び出し、LevelThree()
が現在のコール スタックを出力する Logger.PrintCallStack()
を呼び出します。もっとも最近の呼び出しがスタックの最上位に来るため、LevelThree()
が最初に出力されます。このとおりの順序で、LevelTwo()
、LevelOne()
、OnBegin()
と続きます。
コール スタックの出力は、コードで何か問題が発生したときに、どの呼び出しが原因でそこに至ったのかを正確に知るのに便利です。これにより、コードが密集したプロジェクト内で個々のスタック トレースが分離され、実行中のコードの構造を把握するのが容易になります。
デバッグ描画によるゲーム データの視覚化
さまざまなゲーム機能をデバッグする別の方法は、API のデバッグ描画 を使用することです。 この API により、デバッグ形状を構築してゲーム データを視覚化できるようになります。以下はその一例です。
- 警備員から目標までの照準線。
- 小道具移動装置がオブジェクトを動かす距離。
- オーディオ プレーヤーの減衰距離。
このようなデバッグ形状を、公開済みゲームにおいて対象データを公開せずに使用して、ゲーム体験の微調整を行うことができます。詳細については Verse のデバッグ描画 の方法をご確認ください。
並列処理による最適化とタイミング
並列処理 は Verse プログラミング言語の中核であり、ゲーム体験を拡充してくれる強力なツールです。並列処理により、1 つの Verse の仕掛けで複数の処理を同時実行することができます。これにより、より融通の利くコンパクトなコードを記述でき、レベル内で使用する仕掛けの数も抑えられます。並列処理は最適化のための優れたツールであるだけでなく、非同期コードを使用して複数タスクを同時処理できることは、プログラムの実行をスピードアップしてタイミングに関する問題に対処する上でも、素晴らしい方法です。
Spawn による非同期コンテキストの作成
spawn
式は、任意のコンテキストから非同期式を開始し、後続の式をすぐに実行できるようにします。これにより、それぞれに新しい Verse ファイルを作成する必要なく、同じ仕掛けから複数のタスクを同時に実行できるようになります。たとえば、各プレイヤーの体力を毎秒監視するコードがあるシナリオを考えてみましょう。プレイヤーの体力が一定の数値を下回っていて、それを回復させたいとき、回復は少量で実行したいと考えます。その後にいくつかコードを実行して別のタスクも処理するとします。その場合、コードを実装する仕掛けは以下のようになります。
# レベルに配置できる、Verse で作成したクリエイティブの仕掛け
healing_device := class(creative_device):
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
AllPlayers:[]agent = GetPlayspace().GetPlayers()
# 各プレイヤーを毎秒チェックします。体力の残量が半分未満になると、
# 少しだけ回復します。
loop:
のために:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Print("This is the rest of the code!")
ところが、このループは永遠に実行され、決して中断しないため、それに続くコードはまったく実行されません。この仕掛けはループ式を実行する以外、進まなくなるため、この設計では限界があります。仕掛けが複数タスクを同時処理できるようにコードを並列に実行するには、loop
コードを非同期関数に入り込ませて OnBegin()
中にそれをスポーンします。
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
# spawn 式を使って HealMonitor() を非同期に実行します。
spawn{HealMonitor()}
# このあとのコードがすぐに実行されます。
Print("This code keeps going while the spawned expression executes")
HealMonitor(Players:[]agent)<suspends>:void=
# 各プレイヤーを毎秒チェックします。体力の残量が半分未満になると、
# 少しだけ回復します。
loop:
のために:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
HealMonitor()
関数を実行しながら他のコードを実行できるようになったことは、進歩だと言えます。しかし、この関数では、まだ各プレイヤーをループする必要があるため、体験内のプレイヤー数が増えるほどタイミングの問題が発生することが想定されます。たとえば、各プレイヤーの HP に基づいてスコアを付与したり、アイテムを持っているかどうかを確認したりする場合はどうすればよいでしょうか。for
式にプレイヤーごとのロジックをさらに追加すると、この関数の時間的複雑さが増し、多数のプレイヤーがいる場合、タイミングの問題により、ダメージを受けたプレイヤーの 1 人が時間内に回復しない可能性があります。
各プレイヤーをループして個別にチェックする代わりに、プレイヤーごとに関数のインスタンスをスポーンすることで、このコードをさらに最適化できます。つまり、1 つの関数で 1 人のプレイヤーを監視できるため、コードで、回復が必要なプレイヤーにループバックする前に、すべてのプレイヤーを個別にチェックする必要がなくなります。spawn
のような並列処理式をうまく使用すると、コードの効率と柔軟性が向上し、コードベースの残りの部分を解放して他のタスクを処理できるようになります。
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# 各プレイヤーに対して HealMonitor() 関数のインスタンスをスポーンします。
のために:
プレイヤー:すべてのプレイヤー
do:
# spawn 式を使用して HealMonitorPerPlayer() を非同期に実行します。
spawn{HealMonitorPerPlayer(Player)}
# このあとのコードがすぐに実行されます。
Print("This code keeps going while the spawned expression executes")
HealMonitorPerPlayer(Player:agent)<suspends>:void=
if:
Character := Player.GetFortCharacter[]
then:
# 監視対象のプレイヤーを毎秒チェックします。体力の残量が半分未満になると、
# 少しだけ回復します。
loop:
PlayerHP := Character.GetHealth()
if:
PlayerHP <= HPThreshold
then:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
loop
式内で spawn
式を使用すると、正しく扱わない場合に予期しない動作になることがあります。たとえば、HealMonitorPerPlayer()
は終了しないため、このコードはランタイム エラーが発生するまで、非同期関数を際限なくスポーンし続けます。
# 各プレイヤーに対して HealMonitor() 関数のインスタンスをスポーンし、無制限にループします。
# 非同期関数の数が無限に増加するため、ランタイム エラーが発生します。
loop:
のために:
プレイヤー:すべてのプレイヤー
do:
spawn{HealMonitorPerPlayer(Player)}
Sleep(0.0)
タイミングをイベントで制御する
コードのすべての部分を正しく同期させることは、困難になることがあります。特に多くのスクリプトが同時に実行される大規模なマルチプレイヤー エクスペリエンスでは顕著になります。コードの異なる部分が、設定された順序で実行される他の関数またはスクリプトに依存している場合があり、厳密に制御しないと、それらの間にタイミングの問題が発生する可能性があります。たとえば、一定時間をカウントダウンし、渡されたプレイヤーの HP がしきい値より大きい場合にスコアを付与する次の関数を考えてみましょう。
CountdownScore(Player:agent)<suspends>:void=
# しばらく待ってから、HP がしきい値を超えた各プレイヤーにスコアを付与します。
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
プレイヤーHP >= HPしきい値
then:
ScoreManager.Activate(Player)
この関数には <suspends>
モディファイアがあるため、spawn()
を使用してプレイヤーごとに非同期でそのインスタンスを実行できます。 ただし、この関数に依存する他のコードは、関数の完了後に必ず実行されるようにする必要があります。 CountdownScore()
の終了後にスコアを獲得した各プレイヤーを出力する場合はどうすればよいでしょうか。 OnBegin()
で Sleep()
を呼び出して CountdownScore()
の実行にかかる時間と同じ時間待機することでこれを行うことができますが、ゲームの実行中にタイミングの問題が発生し、コードを変更する場合に常に更新する必要がある新しい変数を導入しなければならなくなります。 代わりに、カスタム イベントを作成し、それらに対して Await()
を呼び出して、コード内のイベントの順序を厳密に制御できます。
# カウントダウンが終了したときに通知するカスタム イベント。
CountdownCompleteEvent:event() = event(){}
# 実行中のゲームで仕掛けが開始されたときに実行します
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# 各プレイヤーに CountdownScore 関数をスポーンします
のために:
プレイヤー:すべてのプレイヤー
do:
spawn{CountdownScore(Player)}
# CountdownCompletedEvent が通知されるまで待機します。
CountdownCompleteEvent.Await()
# プレイヤーにスコアがある場合は、それをログに出力します。
のために:
プレイヤー:すべてのプレイヤー
CurrentScore := ScoreManager.GetCurrentScore(Player)
CurrentScore > 0
do:
Print("This Player has score!")
CountdownScore(Player:agent)<suspends>:void=
# しばらく待ってから、HP がしきい値を超えた各プレイヤーにスコアを付与します。
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
# イベントを通知して、待機中のコードが続行できるようにします。
CountdownCompleteEvent.Signal()
このコードは CountdownCompletedEvent()
がシグナルされるのを待機するため、CountdownScore()
の実行が完了した後にのみ各プレイヤーのスコアをチェックすることが保証されます。多くの仕掛けには、コードのタイミングを制御するために Await()
を呼び出すことができる組み込みイベントがあり、独自のカスタム イベントでこれらを活用することで、複数の変動部分がある複雑なゲーム ループを作成できます。たとえば、Verse スターター テンプレート では、複数のカスタム イベントを使用して、キャラクターの動きを制御し、UI を更新して、ボードからボードへの全体的なゲーム ループを管理します。
Sync、Race、Rush により複数の式を扱う
sync、race、rush を使用すると、複数の非同期式を同時に実行しながら、それらの式の実行が終了したときに異なる機能を実行できます。これらを活用することで、非同期式それぞれの存続期間を厳密に制御でき、複数の異なる状況を処理できるさらに動的なコードを作成できます。
たとえば、rush
式を取りあげます。この式は複数の非同期式を並列で実行しますが、最初に終了した式の値のみを返します。チームが何らかのタスクを完了しなければならないミニゲームがあり、最初にタスクを完了したチームが、終了するまで他のプレイヤーを妨害できるパワーアップを獲得するとします。各チームがタスクを完了した時点を追跡するための複雑なタイミング ロジックを記述することも、rush
式を使用することもできます。この式は終了する最初の非同期式の値を返すため、勝利したチームを返し、他のチームを処理するコードは実行を継続できます。
WinningTeam := rush:
# 3 つの非同期関数はすべて同時に開始されます。
RushToFinish(TeamOne)
RushToFinish(TeamTwo)
RushToFinish(TeamThree)
# いずれかの非同期関数が完了すると、次の式が直ちに呼び出されます。
GrantPowerup(優勝チーム)
race
式は同様の動作ですが、異なるのは、1 つの非同期式が完了すると他の式がキャンセルされるところです。これにより、複数の非同期式の存続期間を一度に厳密に制御できるようになります。これを sleep()
式と組み合わせて、式を実行する時間を制限することもできます。先ほどの rush
の例と同様ですが、今回は 1 チームが勝利したらすぐにミニゲームを終了することを考えます。ミニゲームが永遠に続かないようにタイマーを追加することも必要です。race
式を使用すると、これら両方を実行できます。競争に敗れた式をキャンセルするタイミングを知るためにイベントやその他の並行処理ツールを使用する必要はありません。
WinningTeam := race:
# 4 つの非同期関数はすべて同時に開始されます。
RaceToFinish(TeamOne)
RaceToFinish(TeamTwo)
RaceToFinish(TeamThree)
Sleep(TimeLimit)
# いずれかの非同期関数が完了すると、次の式が直ちに呼び出されます。その他の非同期関数はすべてキャンセルされます。
GrantPowerup(優勝チーム)
最後に、sync
式を使用すると、複数の式の実行が完了するまで待機して、それぞれの式が完了してから続行することを保証できます。sync
式は各非同期式の結果を含む タプル を返すため、すべての式の実行を完了し、それぞれの式からのデータを個別に評価できます。ミニゲームの例に戻り、今回はミニゲームでの成績に基づいて各チームにパワーアップを付与する場合を考えます。まさにこのような場合に sync
式を使用します。
TeamResults := sync:
# 3 つの非同期関数はすべて同時に開始されます。
WaitForFinish(TeamOne)
WaitForFinish(TeamTwo)
WaitForFinish(TeamThree)
# 次の式は、すべての非同期式が完了したときにはじめて呼び出されます。
GrantPowerups(TeamResults)
複数の配列要素に対して非同期式を実行する場合は、便利なArraySync()
関数を使用すれば、すべてが同期されることを保証できます。
これらの並列処理式はそれ自体が強力なツールであり、それらを組み合わせて使用する方法を学習することで、あらゆる状況に対応するコードを記述できるようになります。Speedway Race (Verse Persistence) テンプレート のサンプルを考えてみましょう。この例では、複数の並列処理式を組み合わせて、レース前に各プレイヤーのイントロを再生するだけでなく、イントロ中にプレイヤーが退出した場合にそれをキャンセルします。この例では並列処理をさまざまな形で使用し、異なるイベントに動的に反応する、弾力的なコードを構築する方法について説明します。
# プレイヤーのイントロが開始され、情報を表示されるのを待機します。
# プレイヤーがゲームから退出した場合は待機をキャンセルします。
WaitForPlayerIntro(Player:agent, StartOrder:int)<suspends>:void=
var IntroCounter:int = 0
race:
# このプレイヤーがレースを終えるまで待ち、完了を記録します。
loop:
sync:
block:
StartPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
PlayerLeaderboard.UpdatePopupUI(Player, PopupDialog)
EndPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
break
set IntroCounter += 1
# このプレイヤーがゲームから退出するまで待ちます。
loop:
LeavingPlayer := GetPlayspace().PlayerRemovedEvent().Await()
if:
LeavingPlayer = Player
then:
break