Unity3Dコルーチンの詳細リンクで頻繁に参照されているリンクは無効です。コメントと回答に記載されているので、こちらに記事の内容を掲載します。このコンテンツはこのミラーからのものです。
Unity3Dコルーチンの詳細
ゲームの多くのプロセスは、複数のフレームで発生します。パスファインディングのような「密な」プロセスがあり、フレームごとにハードワークしますが、フレームレートにあまり影響を与えないように複数のフレームに分割されます。ゲームプレイトリガーのように、ほとんどのフレームでは何も実行しない「まばらな」プロセスがありますが、重要な作業を実行するように要求されることがあります。そして、2つの間にさまざまなプロセスがあります。
マルチスレッドを使用せずに、複数のフレームで実行されるプロセスを作成する場合は常に、作業をフレームごとに実行できるチャンクに分割する方法を見つける必要があります。中央ループのあるアルゴリズムの場合、それはかなり明白です。たとえば、A *パスファインダーは、ノードリストを半永久的に維持し、開いているリストから少数のノードのみをフレームごとに処理するように構造化できます。すべての作業を一度に実行します。レイテンシを管理するためにいくつかのバランスをとる必要があります。結局のところ、フレームレートを60または30フレーム/秒でロックしている場合、プロセスは60または30ステップ/秒しかかかりません。全体的に長すぎる。きちんとしたデザインは、1つのレベルで可能な限り最小の作業単位を提供する可能性があります。単一のA *ノードを処理し、さらに上位のレイヤーで作業をより大きなチャンクにグループ化する方法–たとえば、A *ノードをXミリ秒間処理し続ける。(一部の人々はこれを「タイムスライシング」と呼んでいますが、私はそうではありません)。
それでも、この方法で作業を分割できるようにするには、あるフレームから次のフレームに状態を転送する必要があります。反復アルゴリズムを分解する場合は、反復全体で共有されるすべての状態を保持し、次にどの反復を実行するかを追跡する手段を講じる必要があります。それは通常それほど悪くはありません-「A *パスファインダークラス」の設計はかなり明白です-しかし、あまり快適でない他の場合もあります。時々、フレームごとに異なる種類の作業をしている長い計算に直面するでしょう。それらの状態をキャプチャするオブジェクトは、データを1つのフレームから次のフレームに渡すために保持される、半ば便利な「ローカル」の大きな混乱に終わる可能性があります。また、スパースプロセスを処理している場合、作業を実行するタイミングを追跡するためだけに、小さなステートマシンを実装しなければならないことがよくあります。
複数のフレームにわたってこのすべての状態を明示的に追跡する必要がなく、マルチスレッド化して同期とロックを管理する必要がないなどの代わりに、関数を単一のコードチャンクとして記述することができれば、すばらしいと思いませんか。関数が「一時停止」して後で実行する特定の場所をマークしますか?
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 Technologiesには勤務していません。Unityのソースコードを見たことがありません。Unityのコルーチンエンジンの根本を見たことがない。しかし、彼らが私がこれから説明しようとしているものと根本的に異なる方法でそれを実装した場合、私はかなり驚かれることでしょう。UTの誰かが参加して、それが実際にどのように機能するかについて話したい場合、それは素晴らしいことです。
大きな手掛かりはC#バージョンにあります。まず、関数の戻り値の型はIEnumeratorであることに注意してください。次に、ステートメントの1つが利回りの収益であることに注意してください。つまり、yieldはキーワードでなければならず、UnityのC#サポートはバニラC#3.5であるため、バニラC#3.5キーワードでなければなりません。実際、これはMSDNにあり、「イテレータブロック」と呼ばれるものについて話します。どうしたの?
まず、このIEnumeratorタイプがあります。IEnumerator型は、シーケンス上のカーソルのように機能し、2つの重要なメンバーを提供します。Currentは、カーソルが現在置かれている要素を示すプロパティであり、MoveNext()は、シーケンス内の次の要素に移動する関数です。IEnumeratorはインターフェイスであるため、これらのメンバーの実装方法を正確に指定していません。MoveNext()は、1つをCurrentに追加するか、ファイルから新しい値をロードするか、インターネットから画像をダウンロードしてハッシュし、新しいハッシュをCurrentに保存するか、または最初の1つのことを実行することもできますシーケンスの要素と、2番目の要素とはまったく異なるもの。必要に応じて、これを使用して無限シーケンスを生成することもできます。MoveNext()はシーケンスの次の値を計算します(値がなくなった場合はfalseを返します)。
通常、インターフェースを実装したい場合は、クラスを作成したり、メンバーを実装したりする必要があります。Iteratorブロックは、手間をかけずにIEnumeratorを実装する便利な方法です。いくつかのルールに従うだけで、IEnumerator実装はコンパイラによって自動的に生成されます。
イテレータブロックは、(a)IEnumeratorを返し、(b)yieldキーワードを使用する通常の関数です。では、yieldキーワードは実際には何をするのでしょうか?シーケンスの次の値が何であるか、または値がないことを宣言します。コードがyield return Xまたはyield breakに遭遇するポイントは、IEnumerator.MoveNext()が停止するポイントです。降伏戻りXはMoveNext()がtrueを返し、Currentに値Xが割り当てられるのに対し、降伏ブレークはMoveNext()が偽を返すようにします。
さて、ここにトリックがあります。シーケンスによって返される実際の値が何であるかは問題ではありません。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; }
}
これですべてです nullの長いシーケンスは正確には役に立ちませんが、副作用に関心があります。私達じゃないの?
実際には、その式でできる便利なことがいくつかあります。単にnullを生成して無視するのではなく、さらに作業が必要になると予想されるときに何かを生成した場合はどうなるでしょうか。多くの場合、必ずではありませんが、次のフレームをそのまま実行する必要があります。アニメーションまたはサウンドの再生が終了した後、または特定の時間が経過した後、実行したい場合がたくさんあります。while(playingAnimation)が生成するものは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")は、while(!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;
特定の行がreturn new WaitForSeconds()、yield return new WaitForEndOfFrame()などを生成することは一般的ですが、実際にはそれ自体が特別な形式ではありません。
第2に、これらのコルーチンは単なる反復子ブロックなので、必要に応じて自分で反復することができます。エンジンに代わって実行する必要はありません。以前にこれを使用して、コルーチンに割り込み条件を追加しました:
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を使用していない場合でも、同じようにそれらを活用すると便利な場合があります。
IEnumerator
/IEnumerable
(または一般的な同等の機能)を返し、yield
キーワードを含むメソッドを変換します。イテレータを調べます。