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


134

コルーチンの原理を理解しています。UnityのC#で標準のStartCoroutine/ yield returnパターンを機能させる方法を知っています。たとえば、IEnumerator経由StartCoroutineで戻るメソッドを呼び出し、そのメソッドで何かを実行yield return new WaitForSeconds(1);し、少し待ってから、別の何かを実行します。

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


3
C#コンパイラは、IEnumerator/ IEnumerable(または一般的な同等の機能)を返し、yieldキーワードを含むメソッドを変換します。イテレータを調べます。
Damien_The_Unbeliever

4
イテレータは「ステートマシン」の非常に便利な抽象化です。最初にそれを理解すれば、Unityコルーチンも取得できます。 en.wikipedia.org/wiki/State_machine
Hans Passant

2
unityタグはMicrosoft Unityによって予約されています。誤用しないでください。
Lex Li

11
:私はかなり照明この記事見つけ詳しくUnity3Dのコルーチン
ケイ

5
@ケイ-私はあなたにビールを買うことができればいいのに。その記事はまさに私が必要としていたものです。私の質問は意味がないように思えたので、私は正気を疑い始めていましたが、記事は私の質問に想像以上によく答えます。おそらく、このリンクを使用して回答を追加できます。これは、将来のSOユーザーの利益のために受け入れることができますか?
Ghopper21 '10 / 10/18

回答:


109

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を使用していない場合でも、同じようにそれらを活用すると便利な場合があります。


2
再現していただきありがとうございます。それは素晴らしく、私を大きく助けてくれました。
Naikrovek、2018年

96

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

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

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

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

Unityでは、これらを将来の値を提供するために使用せず、関数が一時停止するという事実を利用します。この悪用のため、Unityのコルーチンに関する多くのことは意味がありません(何IEnumeratorと関係があるのyieldでしょうnew WaitForSeconds(3)か?何ですか?なぜですか?など)。"内部で"何が起こるかは、IEnumeratorを通じて提供する値を使用しStartCoroutine()て、次の値を要求するタイミングを決定することにより、コルーチンが再び一時停止を解除するタイミングを決定します。

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

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

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

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

お電話の際は基本的に、StartCoroutine(MyCoroutine())それは正確に通常の関数呼び出しのようだMyCoroutine()、最初までyield return X、どこXのようなものがあるnullnew WaitForSeconds(3)StartCoroutine(AnotherCoroutine())breakそれは機能と異なる開始したときなどです。Unityはそのyield return X行でその機能を「一時停止」し、他のビジネスに進み、いくつかのフレームが通過し、再び時間が来ると、Unityはその行の直後にその機能を再開します。関数内のすべてのローカル変数の値を記憶しています。このようにして、forたとえば2秒ごとにループするループを作成できます。

Unityがいつコルーチンを再開するかXは、内の内容によって異なりますyield return X。たとえば、を使用した場合yield return new WaitForSeconds(3);、3秒が経過すると再開します。を使用した場合は、完全に完了したyield return StartCoroutine(AnotherCoroutine())後に再開するため、AnotherCoroutine()時間内に動作をネストできます。を使用したばかりの場合yield return null;は、次のフレームからすぐに再開されます。


2
残念ですが、UnityGemsはしばらくの間ダウンしているようです。Redditの上の一部の人々はアーカイブの最後のバージョンを取得するために管理:web.archive.org/web/20140702051454/http://unitygems.com/...
ForceMagic

3
これは非常にあいまいであり、正しくないリスクがあります。ここでは、コードが実際にコンパイルされる方法と、それが機能する理由を示します。また、これも質問の答えにはなりません。stackoverflow.com/questions/3438670/...
ルイ・香港

ええ、私は「Unityのコルーチンはどのように機能するのか」をゲームプログラマの観点から説明したと思います。実際の問題は、内部で何が起こっているのかを尋ねることでした。私の答えの誤った部分を指摘できるなら、私はそれを喜んで修正します。
Gazihan Alankus、2015

4
利回りの戻り値についてfalseに同意します。誰かが私の答えを知らないためにそれを批判し、それがさらに有用であるかどうかを検討するために急いでリンクを追加したので、それを追加しました。今は外しました。ただし、Unityがシングルスレッドであることと、コルーチンがどのように適合するかは、誰にとっても明白ではないと思います。私が話し合った初心者のUnityプログラマーの多くは、全体を非常に漠然と理解していて、そのような説明から利益を得ている。私は自分の回答を編集して、質問に対する実際の回答を提供しました。提案は大歓迎です。
Gazihan Alankus

2
Unityはシングルスレッドのfwiwではありません。MonoBehaviourライフサイクルメソッドが実行されるメインスレッドがありますが、他のスレッドもあります。独自のスレッドを自由に作成することもできます。
ベンテハット2017

10

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

Unity(およびすべてのゲームエンジン)はフレームベースです。

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

あなたは..「ああ、それは素晴らしいです。もしエンジンがフレームごとに何かをするようにしたい場合はどうしたらいいですか?エンジンにそのようなことをフレームで実行するように指示するにはどうすればよいですか?」

答えは...

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

とても簡単です。

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

あなたは「更新」機能を知っています。簡単に言うと、そこに配置したものはすべてフレームごとに処理されます。コルーチン-yield構文と文字通りまったく同じですが、まったく違いはありません。

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やゲームエンジンの「フレーム」は、まったくスレッドとはまったく関係がありません。

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


ありがとう!しかし、あなたの答えはコルーチンの使い方を説明するものであり、裏でどのように機能するかではありません。
Ghopper21、2016

1
それは私の喜びです、ありがとう。私はあなたが何を意味しているのか理解しています-これは、一体何がコルーチンであるかを常に尋ねている初心者にとって良い答えになるでしょう。乾杯!
Fattie 2016

1
実際-少しでも、答えのどれも「裏で」何が起こっているかを説明しません。(これは、IEnumeratorがスケジューラーにスタックされることです。)
Fattie

あなたは「絶対に違いはない」と言った。では、Unityがコルーチンを作成したのはなぜUpdate()ですか?つまり、これらの2つの実装とそのユースケースの間には、かなり明白な少なくともわずかな違いがあるはずです。
Leandro Gecozo 2017年

ちょっと@LeandroGecozo-私はもっと言うでしょう、 "Update"は彼らが追加した一種の( "ばかげた")単純化です。(多くの人は決してそれを使用せず、単にコルーチンを使用します!)私はあなたの質問に良い答えはないと思います、それはUnityがどのようにあるかだけです。
Fattie

5

最近これを掘り下げて、ここ(http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/)に投稿しました- 内部(密なコード例)と基礎となるIEnumeratorインターフェースに光を当てていますそしてそれがコルーチンにどのように使われるか。

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


0

自動的に取得するUnityの基本関数はStart()関数とUpdate()関数であるため、コルーチンは基本的にStart()関数とUpdate()関数と同様の関数です。古い関数func()は、コルーチンを呼び出すのと同じ方法で呼び出すことができます。Unityは明らかに、通常の関数とは異なるコルーチンに特定の境界を設定しています。一つの違いは

  void func()

あなたが書く

  IEnumerator func()

コルーチン用。そして、次のようなコード行で通常の関数の時間を制御できるのと同じ方法

  Time.deltaTime

コルーチンには、時間を制御できる方法に特定のハンドルがあります。

  yield return new WaitForSeconds();

これはIEnumerator / Coroutineの内部で実行できる唯一のことではありませんが、コルーチンが使用される便利なものの1つです。UnityのスクリプトAPIを調査して、コルーチンの他の特定の用途を学ぶ必要があります。


0

StartCoroutineは、IEnumerator関数を呼び出すメソッドです。これは単純なvoid関数を呼び出すだけの場合と似ていますが、IEnumerator関数で使用する点が異なります。このタイプの関数は、特別なyield関数を使用できるため、ユニークです。何かを返す必要があることに注意してください。それは私の知る限りです。ここで私はシンプル書いたテキスト以上フリッカゲームを団結方法

    public IEnumerator GameOver()
{
    while (true)
    {
        _gameOver.text = "GAME OVER";
        yield return new WaitForSeconds(Random.Range(1.0f, 3.5f));
        _gameOver.text = "";
        yield return new WaitForSeconds(Random.Range(0.1f, 0.8f));
    }
}

次に、IEnumerator自体から呼び出しました

    public void UpdateLives(int currentlives)
{
    if (currentlives < 1)
    {
        _gameOver.gameObject.SetActive(true);
        StartCoroutine(GameOver());
    }
}

ご覧のとおり、StartCoroutine()メソッドをどのように使用したかを確認します。どういうわけか私が助けてくれることを願っています。私は自分自身を理解しているので、あなたが私を訂正したり、私に感謝したりした場合、どんな種類のフィードバックも素晴らしいでしょう。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.