web-dev-qa-db-ja.com

UnityでStartCoroutine / yield returnパターンは実際にどのように機能しますか?

コルーチンの原理を理解しています。 UnityのC#で動作する標準のStartCoroutine/yield returnパターンを取得する方法を知っています。 IEnumeratorを介してStartCoroutineを返すメソッドを呼び出し、そのメソッドで何かを実行し、yield return new WaitForSeconds(1);を実行して1秒待ってから、別の処理を実行します。

私の質問は:舞台裏で実際に何が起こっているのですか? StartCoroutineは実際に何をしますか? IEnumeratorはどのようなWaitForSecondsを返しますか? StartCoroutineは、呼び出されたメソッドの「その他」の部分に制御をどのように返しますか?これはすべて、Unityの同時実行モデル(コルーチンを使用せずに多くのことが同時に進行している)とどのように相互作用しますか?

117
Ghopper21

しばしば参照される nity3Dコルーチンの詳細 リンクは無効です。コメントと回答に記載されているので、記事の内容をここに投稿します。このコンテンツは this mirror から来ています。


Unity3Dコルーチンの詳細

ゲームの多くのプロセスは、複数のフレームにわたって行われます。パスファインディングなどの「密な」プロセスがあります。これは各フレームで一生懸命に働きますが、フレームレートにあまり影響を与えないように複数のフレームに分割されます。ゲームプレイトリガーなど、ほとんどのフレームを処理しない「スパース」プロセスがありますが、重要な作業を行うことが時々必要になります。そして、2つの間にさまざまなプロセスがあります。

マルチスレッドを使用せずに、複数のフレームにわたって実行されるプロセスを作成するときはいつでも、フレームごとに実行できるチャンクに作業を分割する方法を見つける必要があります。中央ループを備えたアルゴリズムの場合、それはかなり明白です。たとえば、A *パスファインダーは、ノードリストを半永久的に維持し、各フレームのオープンリストから少数のノードのみを処理するように構成できます。すべての作業を一度に行うことができます。レイテンシを管理するためにいくつかのバランスを取る必要があります。結局、フレームレートを1秒あたり60または30フレームにロックしている場合、プロセスは1秒あたり60または30ステップしかかかりません。全体的に長すぎます。きちんとしたデザインは、1つのレベルで可能な限り最小の作業単位を提供する場合があります。単一のA *ノードを処理し、作業をグループ化してより大きなチャンクにグループ化する方法の最上位にレイヤーを作成します。 Xミリ秒間A *ノードの処理を続けます。 (一部の人々はこれを「タイムスライス」と呼んでいますが、私はそうしていません)。

それでも、この方法で作業を分割できるようにすると、あるフレームから次のフレームに状態を​​転送する必要があります。反復アルゴリズムを分割する場合、反復間で共有されるすべての状態を保持するとともに、次に実行される反復を追跡する手段が必要です。通常、それはそれほど悪くはありません-「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 returnであることに注意してください。つまり、yieldはキーワードでなければならず、UnityのC#サポートはVanilla C#3.5であるため、Vanilla C#3.5キーワードでなければなりません。確かに、 ここはMSDNにあります –「イテレータブロック」と呼ばれるものについて話しています。

まず、このIEnumeratorタイプがあります。 IEnumerator型は、シーケンス上のカーソルのように機能し、2つの重要なメンバーを提供します。Currentは、現在カーソルが置かれている要素を提供するプロパティであり、MoveNext()は、シーケンス内の次の要素に移動する関数です。 IEnumeratorはインターフェイスであるため、これらのメンバーの実装方法を正確に指定していません。 MoveNext()は、1つをCurrentに追加するか、ファイルから新しい値をロードするか、インターネットから画像をダウンロードしてハッシュし、Currentに新しいハッシュを保存できます。シーケンス内の要素、および2番目の要素とはまったく異なるもの。必要に応じて、無限シーケンスを生成するために使用することもできます。 MoveNext()はシーケンス内の次の値を計算し(値がなくなるとfalseを返します)、Currentは計算した値を取得します。

通常、インターフェイスを実装する場合は、クラスを作成し、メンバーを実装する必要があります。 Iteratorブロックは、面倒なことなくIEnumeratorを実装する便利な方法です。いくつかのルールに従うだけで、IEnumeratorの実装はコンパイラによって自動的に生成されます。

イテレータブロックは、(a)IEnumeratorを返す通常の関数であり、(b)yieldキーワードを使用します。 yieldキーワードは実際に何をしますか?シーケンスの次の値が何であるか、または値がもうないことを宣言します。コードがyield return Xまたはyield breakに遭遇するポイントは、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;
}

また、実際に記述したのは、ヌル値の長いシーケンスを生成するイテレーターブロックですが、重要なのは、それらを計算するために行う作業の副作用です。次のような単純なループを使用して、このコルーチンを実行できます。

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ステートメントは式(nullなど)を提供する必要があります。そのため、イテレーターブロックはIEnumerator.Currentに実際に割り当てるものを持っています。 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を追加することで、コルーチン自体の表現力を高めることができます。エンジンで実行する方が、スクリプトで実行するよりも速くなる可能性があります。

いくつかの非自明な影響これらすべてについて、私が指摘すべきだと思っていた人々が時々見逃すいくつかの有用なことがあります。

まず、利回りの戻り値は、式(任意の式)を生成するだけであり、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()などを生成しますが、実際には特別な形式ではありません。

第二に、これらのコルーチンは単なるイテレータブロックであるため、必要に応じて自分でイテレートすることができます。エンジンに代わって実行させる必要はありません。以前にコルーチンに割り込み条件を追加するためにこれを使用しました:

IEnumerator DoSomething()
{
  /* ... */
}

IEnumerator DoSomethingUnlessInterrupted()
{
  IEnumerator e = DoSomething();
  bool interrupted = false;
  while(!interrupted)
  {
    e.MoveNext();
    yield return e.Current;
    interrupted = HasBeenInterrupted();
  }
}

第三に、他のコルーチンに譲ることができるという事実は、あたかもそれらがエンジンによって実装されたかのように性能的ではありませんが、独自の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を使用していない場合でも、同じ方法でそれらを利用すると便利かもしれません。

94
James McMahon

以下の最初の見出しは、質問に対する直接的な答えです。その後の2つの見出しは、日常のプログラマにとってより便利です。

コルーチンのおそらく退屈な実装の詳細

コルーチンは Wikipedia などで説明されています。ここでは、実用的な観点からいくつかの詳細を提供します。 IEnumeratoryieldなどは C#言語機能 であり、Unityで多少異なる目的に使用されます。

簡単に言えば、IEnumeratorは、Listのように、1つずつ要求できる値のコレクションがあると主張しています。 C#では、IEnumeratorを返すシグネチャを持つ関数は実際に作成して返す必要はありませんが、C#に暗黙のIEnumeratorを提供させることができます。その後、関数は、yield returnステートメントを介して、返されるIEnumeratorの内容を遅延形式で提供できます。呼び出し元がその暗黙のIEnumeratorから別の値を要求するたびに、関数は次のyield returnステートメントまで実行され、次の値が提供されます。この副産物として、関数は次の値が要求されるまで一時停止します。

Unityでは、これらを使用して将来の値を提供するのではなく、関数が一時停止するという事実を利用します。この悪用のため、Unityのコルーチンに関する多くのことは意味がありません(IEnumeratorは何と関係がありますか?yieldとは何ですか?new WaitForSeconds(3)?など)。 「内部」で発生するのは、IEnumeratorを介して提供する値がStartCoroutine()によって使用され、次の値をいつ要求するかを決定します。

Unityゲームはシングルスレッド(*)

コルーチンはnotスレッドです。 Unityには1つのメインループがあり、記述するすべての関数は同じメインスレッドによって順番に呼び出されます。これを確認するには、関数またはコルーチンのいずれかにwhile(true);を配置します。 Unityエディターも含め、すべてがフリーズします。これは、すべてが1つのメインスレッドで実行される証拠です。 このリンク ケイが上記のコメントで言及したことも素晴らしいリソースです。

(*)Unityは1つのスレッドから関数を呼び出します。したがって、自分でスレッドを作成しない限り、作成したコードはシングルスレッドです。もちろん、Unityは他のスレッドを採用しており、必要に応じて自分でスレッドを作成できます。

ゲームプログラマ向けのコルーチンの実用的な説明

基本的に、StartCoroutine(MyCoroutine())を呼び出すときは、最初のyield return Xまでは、MyCoroutine()の通常の関数呼び出しとまったく同じです。ここで、Xnullnew WaitForSeconds(3)StartCoroutine(AnotherCoroutine())breakなどです。 。 Unityは、そのyield return X行でその機能を「一時停止」し、他のビジネスを続行し、いくつかのフレームが通過します。また、時間が経つと、Unityはその行の直後にその機能を再開します。関数内のすべてのローカル変数の値を記憶しています。この方法では、たとえば、2秒ごとにループするforループを使用できます。

Unityがコルーチンを再開するタイミングは、yield return Xに含まれていたXに依存します。たとえば、yield return new WaitForSeconds(3);を使用した場合、3秒が経過すると再開します。 yield return StartCoroutine(AnotherCoroutine())を使用した場合は、AnotherCoroutine()が完全に完了した後に再開します。これにより、動作を時間内にネストできます。 yield return null;を使用した場合、次のフレームで再開します。

93
Gazihan Alankus

それはもっと簡単なことではありません。

Unity(およびすべてのゲームエンジン)はframe basedです。

全体のポイント、Unityの存在理由全体は、フレームベースであるということです。 エンジンは「各フレーム」を処理します。(アニメーション、オブジェクトのレンダリング、物理学などを実行します。)

「ああ、それは素晴らしい。フレームごとにエンジンに何かをさせたい場合はどうしたらいいだろう。フレーム内でそんなことをするようにエンジンに伝えるにはどうすればよいか」

答えは...

それがまさに「コルーチン」の目的です。

とても簡単です。

そしてこれを考慮してください....

「更新」機能を知っています。簡単に言うと、そこに入れることはすべて行われますすべてのフレーム。それは文字通りまったく同じで、コルーチン-イールド構文とまったく違いはありません。

void Update()
 {
 this happens every frame,
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 }

...in a coroutine...
 while(true)
 {
 this happens every frame.
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 yield return null;
 }

まったく違いはありません。

脚注:誰もが指摘したように、Unityは単ににスレッドがありません。 Unityまたはゲームエンジンの「フレーム」は、スレッドとはまったく関係がありません。

コルーチン/イールドは、Unityでフレームにアクセスする方法です。それでおしまい。 (そして実際、Unityが提供するUpdate()関数とまったく同じです。)それだけです。それはとても簡単です。

10
Fattie

最近これを掘り下げて、ここに投稿を書いてください- http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ -内部に光を当てる例)、基礎となる IEnumerator インターフェース、およびコルーチンでの使用方法。

この目的でコレクション列挙子を使用することは、私にとってはまだ少し奇妙に思えます。これは、列挙子が設計されていると感じるものの逆です。列挙子のポイントはすべてのアクセスで返される値ですが、コルーチンのポイントは値が返される間のコードです。このコンテキストでは、実際の戻り値は無意味です。

6
Geri