146

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

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

4

7 に答える 7

118

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

于 2015-09-09T00:09:16.420 に答える
104

以下の最初の見出しは、質問に対する正解です。後の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())関数呼び出しとまったく同じです。これは、関数とは異なり始めるときです。Unityはその行でその機能を「一時停止」し、他のビジネスを続行し、一部のフレームが通過します。また時間になると、Unityはその行の直後にその機能を再開します。関数内のすべてのローカル変数の値を記憶しています。このようにして、たとえば2秒ごとにループするループを作成できます。MyCoroutine()yield return XXnullnew WaitForSeconds(3)StartCoroutine(AnotherCoroutine())breakyield return Xfor

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

于 2013-03-14T15:08:46.813 に答える
8

これ以上簡単なことはありません。

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がマルチスレッド、ハイパースレッド、または量子コンピューターで実行された場合!! ...フレーム/コルーチンとはまったく関係がありません。完全に、完全に、絶対に、無関係の問題です。)

Unityがマルチスレッド、ハイパースレッド、または量子コンピューターで実行された場合!! ...フレーム/コルーチンとはまったく関係がありません。それは完全に、完全に、絶対に、無関係の問題です。

要約すると...

つまり、コルーチン/歩留まりは、Unityのフレームにアクセスする方法にすぎません。それでおしまい。

(実際、Unityが提供するUpdate()関数とまったく同じです。)

それがすべてです、それはとても簡単です。

なぜIEnumerator?

これ以上簡単なことはありません。IEnumeratorは「何度も」物事を返します。

(そのリストは、「10個」などの特定の長さを持つことも、永久に続くこともあります。)

したがって、自明のことですが、IEnumeratorを使用します。

「何度も繰り返し」たい.Netのどこにでも、IEnumeratorはこの目的のために存在します。

もちろん、.Netを使用したすべてのフレームベースのコンピューティングは、IEnumeratorを使用して各フレームを返します。他に何を使用できますか?

(C#を初めて使用する場合は、IEnumeratorは、配列内のアイテムなど、「通常の」ものを1つずつ返すためにも使用されることに注意してください。)

于 2016-02-08T22:05:15.353 に答える
6

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

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

于 2015-01-30T10:37:38.823 に答える
1

Unity 2017+では、非同期コードにネイティブC#async/awaitキーワードを使用できますが、それ以前は、C#には非同期コードを実装するネイティブな方法がありませんでした。

Unityは、非同期コードの回避策を使用する必要がありました。彼らは、当時人気のあった非同期手法であったC#イテレータを活用することでこれを実現しました。

C#イテレータを見てください

あなたがこのコードを持っているとしましょう:

IEnumerable SomeNumbers() {
  yield return 3;
  yield return 5;
  yield return 8;
}

配列であるかのように呼び出してループを実行すると、次のようになります3 5 8

// Output: 3 5 8
foreach (int number in SomeNumbers()) {
  Console.Write(number);
}

イテレータに慣れていない場合(ほとんどの言語では、リストとコレクションを実装するためのイテレータがあります)、イテレータは配列として機能します。違いは、コールバックが値を生成することです。

それらはどのように機能しますか?

C#でイテレータをループするときMoveNextは、次の値に移動するために使用します。

この例ではforeach、内部でこのメソッドを呼び出すを使用しています。

を呼び出すMoveNextと、イテレータは次のまですべてを実行しますyield。親の呼び出し元は、によって返される値を取得しますyield。次に、イテレータコードが一時停止し、次のMoveNext呼び出しを待ちます。

「怠惰な」機能のため、C#プログラマーはイテレーターを使用して非同期コードを実行しました。

イテレータを使用したC#での非同期プログラミング

2012年以前は、イテレーターの使用は、C#で非同期操作を実行するための一般的なハックでした。

例-非同期ダウンロード機能:

IEnumerable DownloadAsync(string URL) {
  WebRequest  req      = HttpWebRequest.Create(url);
  WebResponse response = req.GetResponseAsync();
  yield return response;

  Stream resp = response.Result.GetResponseStream();
  string html = resp.ReadToEndAsync().ExecuteAsync();
  yield return html;

  Console.WriteLine(html.Result);
}

PS:上記のコードは、イテレータを使用した非同期プログラミングに関するこの優れた、しかし古い記事からのものです:http: //tomasp.net/blog/csharp-async.aspx/

async代わりに使用する必要がありStartCoroutineますか?

2021に関しては、公式のUnityドキュメントは例ではなくコルーチンを使用していasyncます。

また、コミュニティは非同期ではなくコルーチンを支持しているようです。

  • 開発者はコルーチンに精通しています。
  • コルーチンはUnityと統合されています。
  • その他;

2019年からのこのUnityレクチャー「ベストプラクティス:非同期vs.コルーチン-Unite Copenhagen 2019」をお勧めします:https ://youtu.be/7eKi6NKri6I


PS:これは2012年からの古い質問ですが、2021年でもまだ関連しているので、私はそれに答えています。

于 2021-05-17T16:34:54.723 に答える
-1

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

  void func()

あなたが書く

  IEnumerator func()

コルーチン用。そして、同じように、次のようなコード行を使用して通常の関数で時間を制御できます。

  Time.deltaTime

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

  yield return new WaitForSeconds();

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

于 2020-03-24T21:21:52.383 に答える
-1

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()メソッドをどのように使用したかがわかります。私が何とか助けてくれたことを願っています。私は自分自身が恩人なので、あなたが私を訂正したり、私を感謝したりするなら、どんな種類のフィードバックも素晴らしいでしょう。

于 2020-05-21T07:17:12.357 に答える