Ответ 1
Связанная ссылка Unity3D coroutines подробно отключена. Поскольку в комментариях и ответах я упоминаю здесь содержание статьи. Это содержимое происходит от этого зеркала.
Подробное описание Unity3D
Многие процессы в играх происходят в течение нескольких кадров. У вас есть "плотные процессы, такие как pathfinding", которые работают каждый кадр, но разделяются на несколько кадров, чтобы не влиять на частоту кадров слишком сильно. У вас есть "редкие процессы, такие как триггеры геймплея", которые ничего не делают в большинстве фреймов, но иногда требуют критической работы. И у вас есть различные процессы между ними.
Всякий раз, когда вы создаете процесс, который будет выполняться над несколькими кадрами - без многопоточности - вам нужно найти какой-то способ разбить работу на куски, которые можно запустить один за кадром. Для любого алгоритма с центральным циклом его довольно очевидно: например, A * pathfinder может быть структурирован таким образом, что он сохраняет свои списки node полупостоянно, обрабатывая только несколько узлов из открытого списка каждый кадр, вместо этого пытаясь сделать всю работу за один раз. Theres некоторые балансировки для управления латентностью - в конце концов, если вы заблокируете частоту кадров 60 или 30 кадров в секунду, тогда ваш процесс займет всего 60 или 30 шагов в секунду, и это может привести к тому, что процесс займет слишком много времени в общем и целом. Оптимальный дизайн может предлагать наименьшую возможную единицу работы на одном уровне - например, обработать один A * node - и слой сверху, способ группировки, работать вместе в более крупные куски - например, продолжайте обрабатывать узлы A * для X миллисекунд. (Некоторые люди называют это "timelicing", хотя я не знаю).
Тем не менее, разрешая разбить работу таким образом, вы должны перенести состояние из одного кадра в другое. Если вы нарушаете итерационный алгоритм вверх, то вам нужно сохранить все состояние, разделенное между итерациями, а также средство отслеживания последующей итерации. Это обычно не так уж плохо - дизайн класса "A * Pathfinder" довольно очевиден, но есть и другие случаи, которые менее приятны. Иногда вы будете сталкиваться с длинными вычислениями, которые выполняют разные виды работы от кадра к кадру; объект, захвативший свое состояние, может оказаться в большом беспорядке полуполезных "локальных жителей", предназначенных для передачи данных от одного кадра к другому. И если вы имеете дело с разреженным процессом, вам часто приходится внедрять небольшую конечную машину, чтобы отслеживать, когда работа должна быть выполнена вообще.
Не было бы опрятно, если вместо того, чтобы явно отслеживать все это состояние по нескольким кадрам, и вместо того, чтобы иметь многопоточность и управлять синхронизацией и блокировкой и т.д., вы могли бы просто написать свою функцию как единый фрагмент кода, и отметьте конкретные места, где функция должна "приостанавливаться и продолжаться позднее"
Единство - наряду с рядом других сред и языков - обеспечивает это в виде Corouts.
Как они выглядят? В "Unityscript" (Javascript):
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
В С#:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
Как они работают? Позвольте мне сказать, быстро, что я не работаю для Unity Technologies. Ive не видел исходный код Unity. Ive никогда не видел кишки двигателя Unitys coroutine. Однако, если они внедрили его таким образом, который радикально отличается от того, что я собираюсь описать, тогда Ill будет очень удивлен. Если кто-то из UT хочет перезвонить и поговорить о том, как это работает, тогда это будет здорово.
Большие ключи находятся в версии С#. Во-первых, обратите внимание, что возвращаемый тип для функции - IEnumerator. И, во-вторых, обратите внимание, что одним из утверждений является доходность вернуть. Это означает, что доходность должна быть ключевым словом, а поскольку поддержка Unitys С# является ванильным С# 3.5, это должно быть ключевое слово ванили С# 3.5. Действительно, здесь он находится в MSDN - говоря о чем-то, называемом "итераторными блоками". Так что происходит?
Во-первых, этот тип IEnumerator. Тип IEnumerator действует как курсор над последовательностью, предоставляя два значимых элемента: Current, который является свойством, предоставляющим вам элемент, который теперь находится в курсоре, и MoveNext() - функция, которая перемещается к следующему элементу в последовательности. Поскольку IEnumerator - это интерфейс, он точно не определяет, как эти элементы реализованы; MoveNext() может просто добавить один toCurrent или загрузить новое значение из файла или загрузить изображение из Интернета и хешировать его и сохранить новый хэш в Current... или он может даже сделать одно для первого элемент в последовательности и что-то совершенно другое для второго. Вы могли бы даже использовать его для генерации бесконечной последовательности, если хотите. MoveNext() вычисляет следующее значение в последовательности (возвращает false, если больше нет значений), а Current извлекает значение, которое оно вычисляло.
Обычно, если вы хотите реализовать интерфейс, вам придется писать класс, реализовывать элементы и т.д. Блоки Iterator - это удобный способ реализации IEnumerator без всякой хлопот - вы просто следуете нескольким правилам, а реализация IEnumerator генерируется автоматически компилятором.
Блок-итератор - это регулярная функция, которая (a) возвращает IEnumerator, и (b) использует ключевое слово yield. Итак, что же на самом деле делает ключевое слово yield? Он объявляет, что следующее значение в последовательности - или что больше нет значений. Точка, в которой код встречает доходность return X или break break - это точка, в которой должен останавливаться IEnumerator.MoveNext(); возвращаемый доход X приводит к тому, что MoveNext() возвращает true иCurrent, которому присваивается значение X, тогда как доходность break вызывает 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;
}
и то, что вы на самом деле написали, является блоком итератора, который генерирует длинную последовательность нулевых значений, но существенными являются побочные эффекты работы, которую он выполняет для их вычисления. Вы можете запустить эту сопрограмму, используя простой цикл:
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; }
}
Все в сроках Как вы видели, каждый оператор return yield должен предоставить выражение (например, null), так что блок итератора должен что-то фактически назначить IEnumerator.Current. Длинная последовательность нулей не является точно полезной, но больше заинтересована в побочных эффектах. Арент мы?
Что-то удобное мы можем сделать с этим выражением, на самом деле. Что, если вместо того, чтобы просто уступить null и игнорируя его, мы дали что-то, что указывалось, когда мы ожидаем, что вам нужно будет больше работать? Зачастую вам нужно выполнять прямо на следующем кадре, конечно, но не всегда: будет много раз, когда мы хотим продолжить игру после того, как анимация или звук закончили игру, или по прошествии определенного времени. Те, в то время как (playAnimation) return return null; Конструкции немного утомительны, разве вы не думаете?
Unity объявляет базовый тип YieldInstruction и предоставляет несколько конкретных производных типов, которые указывают на конкретные виды ожидания. У вас есть WaitForSeconds, который возобновляет сопрограмму после истечения заданного промежутка времени. У вас есть WaitForEndOfFrame, который возобновляет сопрограмму в определенной точке позже в том же фрейме. У вас есть сам тип Coroutine, который, когда coroutine A дает сопрограмму B, приостанавливает coroutine A до тех пор, пока не закончится coroutine B.
Как это выглядит с точки зрения времени выполнения? Как я уже сказал, я не работаю для Unity, поэтому Ive никогда не видел их кода; но Id представьте, что это может выглядеть примерно так:
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 для обработки других случаев - например, поддержка уровня сигнала на уровне двигателя может быть добавлена с поддержкой YieldInstruction WaitForSignal (SignalName). Добавляя больше YieldInstructions, сами сопрограммы могут стать более выразительными - урожайность return new WaitForSignal ( "GameOver" ) лучше читать тогда (! Signals.HasFired( "GameOver" )) return return null, если вы спросите меня, совершенно независимо от того, что делать это в движке можно быстрее, чем делать это в script.
Несколько неочевидных последствий Theres несколько полезных вещей обо всем этом, что люди иногда пропускают, что я думал, что я должен указать.
Во-первых, доходность доходности просто дает выражение - любое выражение - и 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() и т.д., являются общими, но theyre фактически не являются специальными формами в своем собственном праве.
Во-вторых, поскольку эти сопрограммы - это только блоки итераторов, вы можете перебирать их самостоятельно, если хотите - вам не нужно, чтобы движок делал это для вас. Ive использовал это для добавления условий прерывания в сопрограмму до:
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. Cтераторные блоки С# представляют собой небольшую конструкцию groovy, и даже если вы не используете Unity, возможно, вам будет полезно использовать их таким же образом.