頻繁に参照されるUnity3Dコルーチンの詳細リンクは機能していません。コメントと回答に記載されているので、ここに記事の内容を掲載します。このコンテンツはこのミラーからのものです。
Unity3Dコルーチンの詳細
ゲームの多くのプロセスは、複数のフレームの過程で行われます。パスファインディングのような「高密度」プロセスがあります。これは、フレームごとに一生懸命働きますが、フレームレートに大きな影響を与えないように複数のフレームに分割されます。ゲームプレイトリガーのように、ほとんどのフレームを実行しない「スパース」プロセスがありますが、重要な作業を行うように求められることがあります。そして、2つの間にさまざまなプロセスがあります。
マルチスレッドを使用せずに複数のフレームで実行されるプロセスを作成する場合は常に、フレームごとに1つずつ実行できるチャンクに作業を分割する方法を見つける必要があります。中央ループを備えたアルゴリズムの場合、それはかなり明白です。たとえば、A *パスファインダーは、ノードリストを半永久的に維持し、フレームごとにオープンリストから少数のノードのみを処理するように構成できます。すべての作業を一度に行うことができます。レイテンシーを管理するために行うべきバランスがいくつかあります。結局のところ、フレームレートを毎秒60または30フレームでロックしている場合、プロセスは毎秒60または30ステップしかかからないため、プロセスに時間がかかる可能性があります。全体的に長すぎます。きちんとしたデザインは、1つのレベルで可能な限り最小の作業単位を提供する場合があります。単一のA*ノードを処理し、作業をより大きなチャンクにグループ化する方法を上に重ねます。たとえば、A*ノードをXミリ秒処理し続けます。(私はそうはしませんが、これを「タイムスライシング」と呼ぶ人もいます)。
それでも、このように作業を分割できるようにすることは、あるフレームから次のフレームに状態を転送する必要があることを意味します。反復アルゴリズムを分割する場合は、反復間で共有されるすべての状態と、次に実行される反復を追跡する手段を保持する必要があります。これは通常、それほど悪くはありません–「A *パスファインダークラス」の設計はかなり明白です–しかし、他の場合も、あまり快適ではありません。フレームごとに異なる種類の作業を行う長い計算に直面することがあります。それらの状態をキャプチャするオブジェクトは、あるフレームから次のフレームにデータを渡すために保持される、半有用な「ローカル」の大きな混乱に終わる可能性があります。また、まばらなプロセスを扱っている場合は、作業をいつ行うべきかを追跡するためだけに、小さなステートマシンを実装しなければならないことがよくあります。
複数のフレームにわたってこのすべての状態を明示的に追跡する代わりに、マルチスレッド化して同期やロックなどを管理する代わりに、関数を単一のコードチャンクとして記述できれば、それは素晴らしいことではないでしょうか。関数が「一時停止」して後で続行する必要がある特定の場所にマークを付けますか?
Unityは、他の多くの環境や言語とともに、これをコルーチンの形で提供します。
彼らはどのように見えますか?「Unityscript」(Javascript)の場合:
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
C#の場合:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
それらはどのように機能しますか?私はユニティテクノロジーズで働いていません。Unityのソースコードを見たことがありません。Unityのコルーチンエンジンの内臓を見たことがありません。しかし、私がこれから説明しようとしているものとは根本的に異なる方法でそれを実装している場合、私は非常に驚かれることでしょう。UTの誰かがチャイムを鳴らして、それが実際にどのように機能するかについて話したいのであれば、それは素晴らしいことです。
大きな手がかりはC#バージョンにあります。まず、関数の戻りタイプがIEnumeratorであることに注意してください。次に、ステートメントの1つがyieldreturnであることに注意してください。つまり、yieldはキーワードである必要があり、UnityのC#サポートはバニラC#3.5であるため、バニラC#3.5キーワードである必要があります。確かに、ここではMSDNにあり、「イテレータブロック」と呼ばれるものについて話しています。どうしたの?
まず、このIEnumeratorタイプがあります。IEnumeratorタイプは、シーケンス上でカーソルのように機能し、2つの重要なメンバーを提供します。カーソルが現在上にある要素を提供するプロパティであるCurrentと、シーケンス内の次の要素に移動する関数であるMoveNext()です。IEnumeratorはインターフェイスであるため、これらのメンバーの実装方法を正確に指定していません。MoveNext()は、Currentに1つ追加するか、ファイルから新しい値をロードするか、インターネットから画像をダウンロードしてハッシュし、新しいハッシュをCurrentに保存することができます。シーケンス内の要素、および2番目の要素とはまったく異なるもの。必要に応じて、これを使用して無限シーケンスを生成することもできます。MoveNext()は、シーケンス内の次の値を計算します(値がこれ以上ない場合はfalseを返します)。
通常、インターフェースを実装したい場合は、クラスを作成したり、メンバーを実装したりする必要があります。イテレータブロックは、面倒なことなくIEnumeratorを実装する便利な方法です。いくつかのルールに従うだけで、IEnumeratorの実装はコンパイラによって自動的に生成されます。
イテレータブロックは、(a)IEnumeratorを返し、(b)yieldキーワードを使用する通常の関数です。では、yieldキーワードは実際に何をするのでしょうか。シーケンス内の次の値が何であるか、またはこれ以上値がないことを宣言します。コードがyieldreturnXまたはyieldBreakに遭遇するポイントは、IEnumerator.MoveNext()が停止するポイントです。イールドリターンXによりMoveNext()はtrueを返し、Currentには値Xが割り当てられ、イールドブレークによりMoveNext()はfalseを返します。
さて、ここにトリックがあります。シーケンスによって返される実際の値が何であるかは問題ではありません。MoveNext()を繰り返し呼び出して、Currentを無視することができます。計算は引き続き実行されます。MoveNext()が呼び出されるたびに、イテレータブロックは、実際に生成される式に関係なく、次の'yield'ステートメントまで実行されます。したがって、次のように書くことができます。
IEnumerator TellMeASecret()
{
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null;
Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null;
PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}
実際に作成したのは、null値の長いシーケンスを生成するイテレータブロックですが、重要なのは、それらを計算するために行う作業の副作用です。このコルーチンは、次のような単純なループを使用して実行できます。
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
または、もっと便利なことに、他の作業と組み合わせることができます。
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
これまでのように、各yield returnステートメントは、イテレータブロックが実際にIEnumerator.Currentに割り当てるものを持つように、式(nullなど)を提供する必要があります。nullの長いシーケンスは必ずしも有用ではありませんが、副作用に関心があります。じゃないですか?
実際、その表現でできる便利なことがあります。nullを生成して無視するのではなく、さらに作業が必要になると予想されるときにそれを示す何かを生成した場合はどうなりますか?多くの場合、次のフレームに直接進む必要がありますが、常にそうとは限りません。アニメーションやサウンドの再生が終了した後、または特定の時間が経過した後、続けたい場合がたくさんあります。while(playingAnimation)yieldはnullを返します。コンストラクトは少し面倒ですよね?
UnityはYieldInstruction基本型を宣言し、特定の種類の待機を示すいくつかの具体的な派生型を提供します。指定された時間が経過した後にコルーチンを再開するWaitForSecondsがあります。WaitForEndOfFrameがあります。これは、同じフレームの後の特定のポイントでコルーチンを再開します。コルーチンタイプ自体があります。これは、コルーチンAがコルーチンBを生成すると、コルーチンBが終了するまでコルーチンAを一時停止します。
ランタイムの観点から、これはどのように見えますか?私が言ったように、私はUnityで働いていないので、彼らのコードを見たことがありません。しかし、私はそれがこのように少し見えるかもしれないと想像します:
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
foreach(IEnumerator coroutine in unblockedCoroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;
if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
shouldRunNextFrame.Add(coroutine);
continue;
}
if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */
}
unblockedCoroutines = shouldRunNextFrame;
他のケースを処理するためにYieldInstructionサブタイプを追加する方法を想像するのは難しくありません。たとえば、WaitForSignal( "SignalName")YieldInstructionをサポートして、信号のエンジンレベルのサポートを追加できます。YieldInstructionsを追加することで、コルーチン自体をより表現力豊かにすることができます。yieldreturn new WaitForSignal( "GameOver")は、読みやすくなります(!Signals.HasFired( "GameOver"))yield return nullエンジンでそれを行うことは、スクリプトでそれを行うよりも速い可能性があるという事実。
いくつかの非自明な影響これらすべてについて、私が指摘すべきだと思っていた、人々が時々見逃すいくつかの有用なことがあります。
まず、yield returnは、式(任意の式)を生成するだけであり、YieldInstructionは通常の型です。これは、次のようなことができることを意味します。
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
特定の行yieldreturnnew WaitForSeconds()、yield return new WaitForEndOfFrame()などは一般的ですが、実際にはそれ自体が特別な形式ではありません。
次に、これらのコルーチンは単なるイテレーターブロックであるため、必要に応じて自分でコルーチンを繰り返すことができます。エンジンに代わって実行させる必要はありません。以前、コルーチンに割り込み条件を追加するためにこれを使用しました。
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
第3に、他のコルーチンで譲歩できるという事実により、エンジンによって実装された場合ほどパフォーマンスが高くない場合でも、独自のYieldInstructionsを実装できるようになります。例えば:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
ただし、これはあまりお勧めしません。コルーチンを開始するコストは、私の好みでは少し重いです。
結論これにより、Unityでコルーチンを使用したときに実際に何が起こっているのかが少し明らかになることを願っています。C#のイテレーターブロックはかっこいい小さな構造であり、Unityを使用していない場合でも、同じようにそれらを利用すると便利な場合があります。