Почему я не зацикливаюсь

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', поскольку это бесконечный цикл? Почему?

babybrain
источник
Я только недавно вернулся в Unity. Я заметил, что методы Input принимают строку. "Fire1"Это то, что вы можете настроить в движке, чтобы разрешить перераспределение клавиш вместо того, чтобы печатать Keycode.Foo?
Мкалафут
1
Это могло бы помочь понять, что yieldэто фактически означает «контроль доходности для вызывающего до тех пор, пока не будет запрошен следующий элемент в Enumerable».
3Dave
@Mkalafut, что звучит как что-то, что можно задать в новом посте «Вопрос», если вы не можете найти ответ на страницах документации Unity , в учебных пособиях или в своих собственных экспериментах.
DMGregory
Я не рекомендую StopAllCoroutines()в этом случае. Это хорошо, когда вы используете только одну сопрограмму, но если вы когда-либо планировали иметь более одной, это может иметь нежелательные эффекты. Вместо этого вы должны использовать StopCoroutine()и просто остановить тот, который имеет значение, а не все из них. ( StopAllCoroutines()было бы полезно, например, при завершении уровня или загрузке новой области и т. д., но не для конкретных вещей, таких как «Я больше не стреляю».)
Даррел Хоффман,

Ответы:

14

Причиной является ключевое слово,yield которое имеет конкретное значение в C #.

При встрече со словами yield returnфункция в C # возвращается, как и следовало ожидать.

Использование yield для определения итератора устраняет необходимость в явном дополнительном классе

[...]

Когда в методе итератора достигается оператор yield return, возвращается выражение и текущее местоположение в коде сохраняется. Выполнение возобновляется с этого места при следующем вызове функции итератора.

Так что бесконечного цикла нет. Существует функция / итератор, которую можно вызывать бесконечное количество раз.

Функция Unity StartCoroutine()заставляет инфраструктуру Unity вызывать функцию / итератор один раз за кадр.

Функция 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 Я добавил в ответ вторую часть, чтобы решить вашу проблему.
Питер
Большое спасибо за разъяснения (голосование одобрено). Я определенно узнал что-то новое сегодня :-)
Mawg говорит восстановить Monica
8

Когда кнопка огня нажата, вводится второй оператор if и запускается StopAllCoroutines. Это означает, что Coroutine, в котором запущен цикл while, закончен, поэтому бесконечного цикла больше нет. Сопрограмма подобна контейнеру для выполнения кода.

Я могу порекомендовать Руководство по Unity и API сценариев Unity, чтобы лучше понять, какие сопрограммы и насколько они эффективны.

Этот блог и поисковый пост на YouTube также помогли мне лучше использовать сопрограммы.

Дэвид Уэзерхед
источник
3

Сопрограммы странный зверь. Возврат возврата приводит к тому, что метод приостанавливает выполнение до тех пор, пока он не будет изменен. За кулисами это может выглядеть примерно так:

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()) либо. Это происходит потому , что Unity сопрограмма выхода управления их абонент. 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.
    }
}

Если не остановить, это сопрограмма, которая после запуска будет выполнять весь цикл раз в fireTimeсекунду.

Джо
источник
1

Простое объяснение: под капотом Unity перебирает коллекцию (из YieldInstruction s или null или чего угодно yield return), используя то, IEnumeratorчто возвращает ваша функция.

Поскольку вы используете yieldключевое слово, ваш метод является итератором . Это не вещь Unity, это особенность языка C #. Как это работает?

Он ленив и не генерирует всю коллекцию одновременно (а коллекция может быть бесконечной и ее невозможно создать сразу). Элементы коллекции генерируются по мере необходимости. Ваша функция возвращает итератор для работы с Unity. Он вызывает свой MoveNextметод для генерации нового элемента и Currentсвойства для доступа к нему.

Таким образом, ваш цикл не является бесконечным, он запускает некоторый код, возвращает элемент и возвращает управление обратно в Unity, чтобы он не завис и мог выполнять другую работу, такую ​​как обработка ввода, чтобы остановить сопрограмму.

trollingchar
источник
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);
}

Та-да - это все, что нужно простой системе сопрограмм. А передавая управление вызывающему, вызывающий может принять решение по любому количеству вещей; у них может быть отсортированная таблица событий, вместо того, чтобы перебирать все сопрограммы в каждом кадре; у них могут быть приоритеты или зависимости. Это позволяет очень просто реализовать совместную многозадачность. И просто посмотрите, как это просто, спасибо yield:)

Luaan
источник