なぜ私はループで動けなくなるのですか


8

Unityは初めてです。私はコルーチンを学び、これを書いた。

private void Fire()
{
    if(Input.GetButtonDown("Fire1"))
    {
        StartCoroutine(FireContinuously());
    }
    if(Input.GetButtonUp("Fire1"))
    {
        StopAllCoroutines();
    }
}

IEnumerator FireContinuously()
{
    while(true)
    {
        GameObject laser = Instantiate(LaserPrefab, transform.position, Quaternion.identity) as GameObject;
        laser.GetComponent<Rigidbody2D>().velocity = new Vector2(0, 10f);
        yield return new WaitForSeconds(firetime);
    }
}

ボタンが押されるとコルーチンが呼び出され、「while」ループに入ります。ボタンを離れると、コルーチンが停止します。それは無限ループなので、「while」ループに引っかかってはいけませんか?どうして?


私は最近Unityに戻ったばかりです。Inputメソッドが文字列を取り込んでいることに気づきました。これは、入力するのでは"Fire1"なく、キーの再マッピングを可能にするためにエンジンで設定できるものですKeycode.Fooか?
Mkalafut

1
これはyield、「Enumerable内の次の項目が要求されるまで呼び出し元に制御を渡す」のが効果的に短いことを理解するのに役立つ場合があります。
3Dave

@Mkalafut は、Unityの ドキュメントページ、チュートリアル、または独自の実験で回答見つからない場合、新しい質問投稿で質問するように聞こえます。
DMGregory

StopAllCoroutines()この場合はお勧めしません。コルーチンを1つだけ使用している場合は問題ありませんが、複数のコルーチンを使用する予定がある場合、これは望ましくない影響を及ぼします。代わりにStopCoroutine()、それらのすべてではなく、関連するものを使用して停止する必要があります。(StopAllCoroutines()たとえば、レベルを終了するとき、または新しい領域をロードするときなどに便利ですが、「もう撃っていない」などの特定のことには役立ちません。)
Darrel Hoffman

回答:


14

その理由はyield C#で特定の意味を持つキーワードです。

単語に出会うと、yield returnC#の関数は期待どおりに戻ります。

反復子を定義するためにyieldを使用すると、明示的な追加クラスの必要がなくなります

[...]

イテレータメソッドでyield returnステートメントに到達すると、式が返され、コード内の現在の場所が保持されます。次に反復関数が呼び出されたときに、その場所から実行が再開されます。

したがって、無限ループはありません。無限に呼び出せる関数/イテレータがあります。

Unity関数StartCoroutine()は、Unityフレームワークに関数/イテレーターをフレームごとに1回呼び出させます。

Unity関数StopAllCoroutinesは、Unityフレームワークに関数/イテレーターの呼び出しを停止させます。

そしてWaitForSeconds(time)イテレータから戻ると、Unityフレームワークはの関数/イテレータの呼び出しを一時停止しますtime


混乱したコメントとそのコメントへの同様に混乱した賛成票は、キーワードyieldが何をし、何をしないかについてさらに詳しく説明するように促しました。

これを書けば:

IEnumerable<int> Count()
{
   int i = 0;
   yield return i++;
}

代わりにこれを書くこともできます:

IEnumerator<int> Count() {
    return new CountEnumerator ();
}
class CountEnumerator : IEnumerator<int> {
    int i = 0;
    bool IEnumerator<int>.MoveNext() { i++; return true; }
    int IEnumerator<int>.Current { get { return i; }
    void IEnumerator<int>.Reset() { throw new NotSupportedException(); }
}

したがって、キーワードyieldはマルチスレッドに関連しておらず、絶対に呼び出されませSystem.Threading.Thread.Yield()


1
On encountering the words yield return a function in C# returns」いいえ、違います。あなたが引用するテキストはそれを説明し、ウィキペディアもそうです -" In computer science, yield is an action that occurs in a computer program during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority."。基本的には、「 `私がいる場所で一時停止し、しばらく他の人に走らせてください」。
Mawgはモニカを復活させると

2
@Mawg私はあなたの懸念に対処するために回答に2番目の部分を追加しました。
ピーター

明確にしていただきありがとうございます(賛成)。私は確かに今日何か新しいことを学びました:-)
Mawgはモニカを復活させると

8

発射ボタンが離されると、2番目のifステートメントが入力され、StopAllCoroutinesが実行されます。これは、whileループが実行されているコルーチンが終了することを意味し、無限ループはなくなります。コルーチンは、コードを実行するコンテナのようなものです。

UnityマニュアルUnity Scripting APIをお勧めします。コルーチンとは何か、コルーチンがどれほど強力であるかについて理解を深めることができます。

このブログとYouTubeの検索記事は、コルーチンをよりよく使用するのにも役立ちました。


3

コルーチンは奇妙な獣です。Yield returnにより、メソッドは後でステップ実行されるまで実行を一時停止します。舞台裏では、次のようになります。

class FireContinuouslyData {
    int state;
    bool shouldBreak;
}

object FireContinuously(FireContinuouslyData data) {
    switch (data.state) {
        case 0:
            goto State_0;
    }
    while (true) {
        GameObject laser = ...;
        laser.GetComponent...
        //the next three lines handle the yield return
        data.state = 0;
        return new WaitForSeconds(fireTime);
        State_0:
    }
}

また、Unity / C#の内部(yield returnはネイティブのc#機能であるため)、StartCoroutineを呼び出すと、FireContinuouslyDataオブジェクトが作成され、メソッドに渡されます。戻り値に基づいて、後で再度呼び出すタイミングを決定し、FireContinuouslyDataオブジェクトを格納して次回に渡すだけです。

利回りの中断を行った場合、内部的に設定data.shouldBreak = trueするだけで、Unityは単にデータを破棄し、再度スケジュールしません。

また、実行と実行の間に保存する必要のあるデータがある場合、そのデータは後で使用できるようにデータに保存されます。

Unity / C#がコルーチン機能を実装する方法の例:

//Internal to Unity/C#

class Coroutine {
    Action<object> method;
    object data;
}

Coroutine StartCoroutine(IEnumerator enumerator) {
    object data = CreateDataForEnumerator(method); //Very internal to C#
    Action<object> method = GetMethodForEnumerator(enumerator); //Also very internal to C#
    Coroutine coroutine = new Coroutine(method, data);
    RunCoroutine(coroutine);
    return coroutine;
}

//Called whenever this coroutine is scheduled to run
void RunCoroutine(Coroutine coroutine) {
    object yieldInstruction = coroutine.method(coroutine.data);
    if (!data.shouldBreak) {
        //Put this coroutine into a collection of coroutines to run later, by calling RunCoroutine on it again
        ScheduleForLater(yieldInstruction, coroutine);
    }
}

1

別の答えは、"Fire1"アップ時にコルーチンを停止することです。これは、最初にを押した後にコルーチンがGameObjectsのインスタンス化を継続しない限り、完全に正しいことです"Fire1"

しかしあなたの場合、このコードは無限ループで「スタック」while(true) {}することはありません。これは、外部で停止していなくても、ループに対する答えを探しているように見えます。

スタックすることはありませんが、コルーチンは終了しません(StopCoroutine()またはを呼び出さない限りStopAllCoroutines())。ユニティコルーチンがあるため、これがあり、その呼び出し元に制御を。yieldingは以下とは異なりreturningます。

  • return声明は、より多くのコードがそれ以下の場合であっても、関数の実行を中止します
  • yield文は後に次の行から始まる、機能を一時停止しますyield再開したとき。

通常、コルーチンはフレームごとに再開されますが、WaitForSecondsオブジェクトも返します。

この行yield return new WaitForSeconds(fireTime)は、「今は私を一時停止し、fireTime数秒が経過するまで戻ってこない」と大まかに変換されます。

IEnumerator FireContinuously()
{
    // When started, this coroutine enters the below while loop...
    while(true)
    {
        // It does some things... (Infinite coroutine code goes here)

        // Then it yields control back to it's caller and pauses...
        yield return new WaitForSeconds(fireTime);
        // The next time it is called , it resumes here...
        // It finds the end of a loop, so will re-evaluate the loop condition...
        // Which passes, so control is returned to the top of the loop.
    }
}

停止しない限り、これはコルーチンであり、開始すると、ループ全体を1 fireTime秒ごとに実行します。


1

簡単な説明:内部では、Unityは関数が返すyield returnを使用しIEnumeratorて(YieldInstructionまたはnullなどの)コレクションを繰り返し処理しています。

yieldキーワードを使用するため、メソッドはイテレータです。これはUnityのものではなく、C#言語の機能です。どのように機能しますか?

それは遅延であり、すべてのコレクションを一度に生成しません(そして、コレクションは無限であり、一度に生成できない場合があります)。コレクションの要素は必要に応じて生成されます。関数は、Unityが動作するイテレーターを返します。これは、そのMoveNextメソッドを呼び出して、それにCurrentアクセスするための新しい要素とプロパティを生成します。

したがって、ループは無限ではなく、コードを実行し、要素を返し、制御をUnityに戻すので、スタックしたり、入力を処理してコルーチンを停止したりするなど、他の作業を実行できます。


0

どのように機能するかを考えてみましょうforeach

foreach (var number in Enumerable.Range(1, 1000000))
{
  if (number > 10) break;
}

反復の制御は呼び出し側にあります。反復を停止すると(ここではbreak)、それだけです。

yieldキーワードは、C#で列挙を作るための簡単な方法です。名前はこれを示しています- yield return制御を呼び出し元(この場合、私たちforeach)に戻します。次の項目にいつ進むかを決めるのは発信者です。したがって、次のようなメソッドを作成できます。

IEnumerable<int> ToInfinity()
{
  var i = 0;
  while (true) yield return i++;
}

これは、単純に、永久に実行されるように見えます。しかし実際には、それは完全に呼び出し元に依存します。あなたはこのようなことをすることができます:

var range = ToInfinity().Take(10).ToArray();

この概念に慣れていない場合、これは少し混乱する可能性がありますが、これが非常に便利なプロパティであることも明らかであることを願っています。これは、発信者に制御を渡すことができる最も簡単な方法であり、発信者がフォローアップすることを決定した場合、次のステップを実行できます(Unityが今日作成された場合、おそらくのawait代わりに使用しyieldますが、await存在しませんでした)その後)。

独自のコルーチン(言うまでもなく、最も単純な愚かなコルーチン)を実装する必要があるのは次のとおりです。

List<IEnumerable> continuations = new List<IEnumerable>();

void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);

void MainLoop()
{
  while (GameIsRunning)
  {
    foreach (var continuation in continuations.ToArray())
    {
      if (!continuation.MoveNext()) continuations.Remove(continuation);
    }

    foreach (var gameObject in updateableGameObjects)
    {
      gameObject.Update();
    }
  }
}

非常に単純なWaitForSeconds実装を追加するには、次のようなものが必要です。

interface IDelayedCoroutine
{
  bool ShouldMove();
}

class Waiter: IDelayedCoroutine
{
  private readonly TimeSpan time;
  private readonly DateTime start;

  public Waiter(TimeSpan time)
  {
    this.start = DateTime.Now;
    this.time = time;
  }

  public bool ShouldMove() => start + time > DateTime.Now;
}

そして、メインループの対応するコード:

foreach (var continuation in continuations.ToArray())
{
  if (continuation.Current is IDelayedCoroutine dc)
  {
    if (!dc.ShouldMove()) continue;
  }

  if (!continuation.MoveNext()) continuations.Remove(continuation);
}

Ta-da-それはすべて単純なコルーチンシステムのニーズです。そして、呼び出し元に制御を委譲することにより、呼び出し元は多くのことを決定できます。フレームごとにすべてのコルーチンを反復処理するのではなく、ソートされたイベントテーブルを持っている場合があります。優先順位や依存関係があるかもしれません。協調的なマルチタスクの非常にシンプルな実装を可能にします。yield:)のおかげで、これがいかに簡単かを見てください。

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