Каким образом исходный шаблон StartCoroutine / yield действительно работает в Unity?

Я понимаю принцип сопрограммы. Я знаю, как получить стандартный StartCoroutine yield return StartCoroutine / yield return для работы в C # в Unity, например, вызвать метод, возвращающий IEnumerator через StartCoroutine и в этом методе что-то сделать, StartCoroutine yield return new WaitForSeconds(1); подождать секунду, затем сделать что-то еще.

Мой вопрос: что действительно происходит за кулисами? Что действительно делает StartCoroutine ? Что возвращает IEnumerator WaitForSeconds ? Как StartCoroutine возвращает управление части «что-то еще» вызываемого метода? Как все это взаимодействует с моделью параллелизма Unity (где много вещей происходит одновременно без использования сопрограмм)?

Оставшая ссылка Unity3D coroutines в деталях ссылка мертва. Поскольку в комментариях и ответах я упоминаю здесь содержание статьи. Это содержимое происходит из этого зеркала .


Подробное описание Unity3D

Многие процессы в играх происходят в течение нескольких кадров. У вас есть «плотные» процессы, такие как pathfinding, которые работают над каждым фреймом, но разделяются на несколько фреймов, чтобы не влиять на частоту кадров слишком сильно. У вас есть «редкие» процессы, такие как триггеры геймплея, которые ничего не делают в большинстве фреймов, но иногда требуют критической работы. И у вас есть различные процессы между ними.

Всякий раз, когда вы создаете процесс, который будет проходить через несколько кадров – без многопоточности – вам нужно найти способ разбить работу на куски, которые можно запускать за один кадр. Для любого алгоритма с центральным циклом это довольно очевидно: например, A * pathfinder может быть структурирован таким образом, что он сохраняет свои списки узлов полупостоянно, обрабатывая только несколько узлов из открытого списка каждый кадр, вместо того, чтобы пытаться чтобы сделать всю работу за один раз. Для управления задержкой необходимо выполнить некоторую балансировку – в конце концов, если вы блокируете частоту кадров 60 или 30 кадров в секунду, тогда ваш процесс будет принимать только 60 или 30 шагов в секунду, и это может привести к простому выполнению процесса слишком длинный в целом. Оптимальный дизайн может предлагать наименьшую возможную единицу работы на одном уровне – например, обрабатывать один узел A *, а слой сверху – способ группировки работать вместе в более крупные куски – например, продолжать обрабатывать узлы A * для X миллисекунд. (Некоторые люди называют это «время», хотя я этого не делаю).

Тем не менее, позволяя разбить работу таким образом, вы должны перенести состояние из одного кадра в другое. Если вы нарушаете итерационный алгоритм вверх, тогда вам нужно сохранить все состояние, разделенное между итерациями, а также средство отслеживания последующей итерации. Это обычно не так уж плохо – дизайн classа «A * Pathfinder» довольно очевиден, но есть и другие случаи, которые менее приятны. Иногда вам придется столкнуться с длинными вычислениями, которые выполняют разные виды работы от кадра к кадру; объект, захвативший свое состояние, может оказаться в большом беспорядке полуполезных «местных жителей», предназначенных для передачи данных из одного кадра в другой. И если вы имеете дело с разреженным процессом, вам часто приходится выполнять небольшую государственную машину, чтобы отслеживать, когда работа должна быть выполнена вообще.

Разве это не было бы аккуратно, если вместо того, чтобы явно отслеживать все это состояние по нескольким кадрам, и вместо того, чтобы иметь multithreading и управлять синхронизацией и блокировкой и т. Д., Вы могли бы просто написать свою функцию как единый fragment кода и отметьте конкретные места, где функция должна «приостанавливаться» и продолжать в дальнейшем?

Единство – наряду с рядом других сред и языков – обеспечивает это в виде Coroutines.

Как они выглядят? В «Unityscript» (Javascript):

 function LongComputation() { while(someCondition) { /* Do a chunk of work */ // Pause here and carry on next frame yield; } } в 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; } } в 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. И, во-вторых, обратите внимание, что одно из утверждений – возврат доходности. Это означает, что доходность должна быть ключевым словом, а поскольку поддержка C # Unity является ванильным C # 3.5, это должно быть ключевое слово ванили C # 3.5. Действительно, здесь это в MSDN – речь о чем-то, называемом «iteratorными блоками». Так, что происходит?

Во-первых, этот тип IEnumerator. Тип IEnumerator действует как курсор над последовательностью, предоставляя два значимых элемента: Current, который является свойством, предоставляющим вам элемент, который теперь находится в курсоре, и MoveNext () – функция, которая перемещается к следующему элементу в последовательности. Поскольку IEnumerator – это интерфейс, он не указывает точно, как эти элементы реализованы; MoveNext () может просто добавить один toCurrent или загрузить новое значение из файла или загрузить изображение из Интернета и хешировать его и сохранить новый hash в Current … или он может даже сделать одно для первого элемент в последовательности и что-то совершенно другое для второго. Вы могли бы даже использовать его для генерации бесконечной последовательности, если хотите. MoveNext () вычисляет следующее значение в последовательности (возвращает false, если больше нет значений), а Current извлекает значение, которое оно вычисляло.

Обычно, если вы хотите реализовать интерфейс, вам придется писать class, реализовывать элементы и т. Д. Блоки Iterator – это удобный способ реализации IEnumerator без всякой проблемы – вы просто следуете нескольким правилам, а реализация IEnumerator генерируется автоматически компилятором.

Блок-iterator является регулярной функцией, которая (a) возвращает IEnumerator, и (b) использует ключевое слово yield. Итак, что же на самом деле делает ключевое слово yield? Он объявляет, что следующее значение в последовательности – или что больше нет значений. Точка, в которой код встречает возврат доходности X или разрыв выхода, является точкой, в которой должен останавливаться IEnumerator.MoveNext (); возвращаемый доход X приводит к тому, что MoveNext () возвращает true и Current, которому присваивается значение X, тогда как разрыв выхода вызывает MoveNext () для возврата false.

Теперь вот трюк. Не имеет значения, каковы фактические значения, возвращаемые последовательностью. Вы можете повторно вызвать MoveNext () и игнорировать Current; вычисления все равно будут выполнены. Каждый раз, когда вызывается MoveNext (), ваш блок iteratorа запускается в следующий оператор 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 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 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 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; } 

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

 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; } } в IEnumerator e = TellMeASecret(); while(e.MoveNext()) { // If they press 'Escape', skip the cutscene if(Input.GetKeyDown(KeyCode.Escape)) { break; } } 

Все в порядке. Как вы видели, каждый оператор возврата yield должен предоставлять выражение (например, null), так что блок iteratorа должен что-то фактически назначить IEnumerator.Current. Длительная последовательность нhive не совсем полезна, но нас больше интересуют побочные эффекты. Не так ли?

На самом деле есть что-то удобное, что мы можем сделать с этим выражением. Что, если вместо того, чтобы просто уступить null и игнорировать его, мы дали что-то, что указывалось, когда мы ожидаем, что вам нужно будет больше работать? Часто нам нужно нести прямо на следующий кадр, конечно, но не всегда: будет много раз, когда мы хотим продолжить работу после того, как анимация или звук закончили игру, или по прошествии определенного времени. Те, в то время как (playAnimation) дают return null; конструкции немного утомительны, не так ли?

Unity объявляет базовый тип YieldInstruction и предоставляет несколько конкретных производных типов, которые указывают на конкретные виды ожидания. У вас есть WaitForSeconds, который возобновляет сопрограмму после истечения заданного количества времени. У вас есть WaitForEndOfFrame, который возобновляет сопрограмму в определенной точке позже в том же фрейме. У вас есть сам тип Coroutine, который, когда coroutine A дает coroutine B, приостанавливает coroutine A до тех пор, пока не закончится coroutine B.

Как это выглядит с точки зрения времени выполнения? Как я уже сказал, я не работаю для Unity, поэтому я никогда не видел их кода; но я бы подумал, что это может выглядеть примерно так:

 List unblockedCoroutines; List shouldRunNextFrame; List shouldRunAtEndOfFrame; SortedList 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 для обработки других случаев – например, поддержка уровня сигнала на уровне двигателя может быть добавлена ​​с поддержкой YieldInstruction WaitForSignal (SignalName). Добавляя больше YieldInstructions, сами сопроводители могут стать более выразительными – вернуть доход new WaitForSignal («GameOver») лучше читать тогда (! Signals.HasFired («GameOver»)) возвращает доход null, если вы спросите меня, совершенно отдельно от тот факт, что выполнение этого в движке может быть быстрее, чем выполнение этого сценария.

Несколько неочевидных последствий. Есть несколько полезных вещей обо всем этом, что люди иногда скучают, что я думал, что должен указать.

Во-первых, доходность доходности просто дает выражение – любое выражение – и YieldInstruction – это обычный тип. Это означает, что вы можете делать такие вещи, как:

 YieldInstruction y; if(something) y = null; else if(somethingElse) y = new WaitForEndOfFrame(); else y = new WaitForSeconds(1.0f); yield return y; 

Конкретные строки возвращают новые WaitForSeconds (), yield return new WaitForEndOfFrame () и т. Д., Являются общими, но они фактически не являются специальными формами.

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

 IEnumerator DoSomething() { /* ... */ } IEnumerator DoSomethingUnlessInterrupted() { IEnumerator e = DoSomething(); bool interrupted = false; while(!interrupted) { e.MoveNext(); yield return e.Current; interrupted = HasBeenInterrupted(); } } в 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); /* ... */ } 

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

Заключение Надеюсь, это немного разъяснит некоторые из того, что действительно происходит, когда вы используете Coroutine в Unity. Блоки iteratorов C # представляют собой небольшую конструкцию, и даже если вы не используете Unity, возможно, вам будет полезно использовать их одинаково.

Первый заголовок ниже – прямой ответ на вопрос. Эти два заголовка более полезны для повседневного программиста.

Возможно, скучная информация о реализации Corouts

Корутины объясняются в Википедии и в других местах. Здесь я просто приведу некоторые детали с практической точки зрения. IEnumerator , yield и т. Д. – это языковые функции C # , которые используются в какой-то другой цели в Unity.

Проще говоря, IEnumerator утверждает, что имеет набор значений, которые вы можете запросить один за другим, вроде List . В C # функция с сигнатурой, возвращающей IEnumerator , не должна фактически создавать и возвращать одну, но может позволить C # предоставить неявный IEnumerator . Затем функция может предоставить содержимое возвращаемого IEnumerator в будущем ленивым способом, с помощью операторов yield return . Каждый раз, когда вызывающий абонент запрашивает другое значение из этого неявного IEnumerator , функция выполняется до следующего оператора yield return , который предоставляет следующее значение. В качестве побочного продукта функция останавливается до следующего запроса.

В Unity мы не используем их для обеспечения будущих значений, мы используем тот факт, что функция приостанавливается. Из-за этой эксплуатации много вещей о сопрограммах в Unity не имеет смысла (что делает IEnumerator что-либо? Что такое yield ? Почему new WaitForSeconds(3) ? И т. Д.). Что происходит «под капотом», значения, которые вы предоставляете через IEnumerator, используются StartCoroutine() чтобы решить, когда запрашивать следующее значение, которое определяет, когда ваша сопрограмма снова перестанет отображаться.

Ваша игра Unity – однопоточная (*)

Корутинги не являются нитями. Существует один основной цикл Unity, и все те функции, которые вы пишете, вызываются одним и тем же основным streamом по порядку. Вы можете проверить это, поместив некоторое while(true); в любой из ваших функций или сопрограмм. Это заморозит все это, даже редактор Unity. Это свидетельствует о том, что все работает в одном основном streamе. Эта ссылка, которую Кей упомянул в вышеупомянутом комментарии, также является отличным ресурсом.

(*) Unity вызывает ваши функции из одного streamа. Итак, если вы сами не создаете stream, код, который вы написали, является однопоточным. Конечно, Unity использует другие streamи, и вы можете сами создавать темы, если хотите.

Практическое описание Coroutines для игровых программистов

В основном, когда вы вызываете StartCoroutine(MyCoroutine()) , это точно так же, как обычный вызов функции MyCoroutine() , до тех пор, пока первая yield return X , где X – что-то вроде null , new WaitForSeconds(3) , StartCoroutine(AnotherCoroutine()) , break и т. д. Это когда он начинает отличаться от функции. Unity «приостанавливает» эту функцию прямо при этом yield return X строки yield return X , продолжается с другим бизнесом, а некоторые кадры проходят, и когда снова наступает время, Unity возобновляет эту функцию сразу после этой строки. Он запоминает значения для всех локальных переменных в функции. Таким образом, вы можете иметь цикл for который петли каждые две секунды, например.

Когда Unity возобновит вашу сопрограмму, зависит от того, что X в вашем yield return X Например, если вы использовали yield return new WaitForSeconds(3); , он возобновляется через 3 секунды. Если вы использовали yield return StartCoroutine(AnotherCoroutine()) , он возобновляется после того, как AnotherCoroutine() полностью завершен, что позволяет вам AnotherCoroutine() определять поведение. Если вы только что использовали yield return null; , он возобновляется прямо в следующем кадре.

Это не могло быть проще:

Unity (и все игровые движки) основаны на фреймах .

Весь смысл, весь смысл Единства, заключается в том, что он основан на рамах. Двигатель делает вещи «каждый кадр» для вас. (Анимация, рендеринг объектов, физика и т. Д.)

Вы можете спросить: «О, это здорово. Что делать, если я хочу, чтобы движок что-то делал для меня в каждом кадре? Как я могу заставить движок делать такое-то в рамке?»

Ответ …

Именно это и есть «сопрограмма».

Все просто.

И подумайте об этом ….

Вы знаете функцию «Обновить». Совершенно просто, все, что вы там вставляете, выполняется каждый кадр . Это буквально точно то же самое, без каких-либо различий, от синтаксиса coroutine-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; } в 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 или в любом игровом движке никак не связаны с нитями.

Coroutines / yield – это просто то, как вы получаете доступ к кадрам в Unity. Вот и все. (И действительно, это абсолютно то же самое, что функция Update (), предоставляемая Unity.) Вот и все, что нужно, все просто.

Впишется в это в последнее время, написал сообщение здесь – http://eppz.eu/blog/understanding-ienumerator-in-unity-3d/ – который пролил свет на внутренности (с плотными примерами кода), базовый интерфейс IEnumerator , и как он используется для сопрограмм.

Использование коллекции счетчиков для этой цели по-прежнему кажется немного странным для меня. Это инверсия того, для чего предназначены энкодеры. Точкой enums является возвращаемое значение для каждого доступа, но точка Coroutines – это код между возвращаемым значением. Фактическое возвращаемое значение в этом контексте бессмысленно.

  • Остановить движение / rotation сразу после столкновения
  • База данных установки (SQLite) для Unity
  • Доступ к переменным / функциям из другого сценария
  • Как сделать Texture2D доступным для чтения через скрипт
  • Как добавить проект распознавания речи в Unity?
  • Работа с развернутой петлей, для цикла не работает
  • Unity: Null при создании нового экземпляра classа
  • Как подключиться к базе данных Unity
  • Unity - проверка работоспособности проигрывателя
  • Автозаполнение не работает в Visual Studio
  • Сериализация и десериализация Json и Json Array в Unity
  • Давайте будем гением компьютера.