Часто упоминаемая ссылка подробных сведений о сопрограммах Unity3D мертва. Поскольку это упоминается в комментариях и ответах, я опубликую здесь содержание статьи. Этот контент исходит из этого зеркала .
Подробнее о сопрограммах Unity3D
Многие процессы в играх происходят в течение нескольких кадров. У вас есть «плотные» процессы, такие как поиск пути, которые усердно работают с каждым кадром, но разделяются на несколько кадров, чтобы не слишком сильно влиять на частоту кадров. У вас есть «разреженные» процессы, такие как триггеры игрового процесса, которые ничего не делают в большинстве кадров, но иногда требуются для выполнения важной работы. И у вас есть разные процессы между ними.
Всякий раз, когда вы создаете процесс, который будет происходить в нескольких кадрах - без многопоточности - вам нужно найти способ разбить работу на части, которые можно запускать по одному на кадр. Для любого алгоритма с центральным циклом это довольно очевидно: например, поисковик A * может быть структурирован таким образом, чтобы он постоянно поддерживал свои списки узлов, обрабатывая только несколько узлов из открытого списка в каждом кадре, вместо того, чтобы пытаться делать всю работу за один раз. Чтобы управлять задержкой, необходимо выполнить некоторую балансировку - в конце концов, если вы фиксируете частоту кадров на уровне 60 или 30 кадров в секунду, тогда ваш процесс будет занимать только 60 или 30 шагов в секунду, и это может привести к тому, что процесс просто займет слишком долго в целом. Аккуратный дизайн может предлагать минимально возможную единицу работы на одном уровне - например, обрабатывать один узел 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 Technologies. Я не видел исходного кода Unity. Я никогда не видел внутренностей движка сопрограмм Unity. Однако, если они реализовали это способом, радикально отличным от того, что я собираюсь описать, я буду очень удивлен. Если кто-нибудь из UT захочет вмешаться и поговорить о том, как это работает на самом деле, это было бы здорово.
Основные подсказки находятся в версии для C #. Во-первых, обратите внимание, что тип возвращаемого значения для функции - IEnumerator. А во-вторых, обратите внимание, что одно из утверждений - yield return. Это означает, что yield должен быть ключевым словом, а поскольку Unity поддерживает C # в обычном C # 3.5, это должно быть ключевое слово vanilla C # 3.5. В самом деле, здесь он находится в MSDN - речь идет о чем-то, что называется «блоками итератора». Так что же происходит?
Во-первых, это тип IEnumerator. Тип IEnumerator действует как курсор над последовательностью, предоставляя два важных члена: Current, которое является свойством, дающим вам элемент, над которым сейчас находится курсор, и MoveNext (), функция, которая перемещается к следующему элементу в последовательности. Поскольку IEnumerator является интерфейсом, он не определяет, как именно реализованы эти члены; MoveNext () может просто добавить один кCurrent, или он может загрузить новое значение из файла, или он может загрузить изображение из Интернета и хэшировать его и сохранить новый хеш в Current ... или он может даже сделать что-то одно для первого элемент в последовательности, и что-то совсем другое для второго. Вы даже можете использовать его для создания бесконечной последовательности, если хотите. MoveNext () вычисляет следующее значение в последовательности (возвращает false, если значений больше нет),
Обычно, если вы хотите реализовать интерфейс, вам нужно написать класс, реализовать члены и так далее. Блоки итератора - это удобный способ реализации IEnumerator без всех этих хлопот - вы просто следуете нескольким правилам, и реализация IEnumerator создается компилятором автоматически.
Блок итератора - это обычная функция, которая (а) возвращает IEnumerator, и (б) использует ключевое слово yield. Так что же на самом деле делает ключевое слово yield? Он объявляет, какое следующее значение в последовательности - или что значений больше нет. Точка, в которой код встречает yield return X или yield break, является точкой, в которой IEnumerator.MoveNext () должен остановиться; доходность return 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;
}
и то, что вы на самом деле написали, - это блок итератора, который генерирует длинную последовательность нулевых значений, но важны побочные эффекты работы, которую он выполняет для их вычисления. Вы можете запустить эту сопрограмму, используя простой цикл, например:
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 должен предоставлять выражение (например, null), чтобы блоку итератора было что-то присвоить IEnumerator.Current. Длинная последовательность нулей не совсем полезна, но нас больше интересуют побочные эффекты. Не так ли?
На самом деле, с этим выражением можно сделать что-то полезное. Что, если вместо того, чтобы просто выдавать значение null и игнорировать его, мы получили что-то, что указывало бы, когда мы ожидаем, что нам потребуется выполнить больше работы? Часто нам нужно будет сразу перейти к следующему кадру, конечно, но не всегда: будет много раз, когда мы захотим продолжить после того, как анимация или звук закончится, или по прошествии определенного времени. Те while (playsAnimation) yield return null; Конструкции немного утомительны, вам не кажется?
Unity объявляет базовый тип YieldInstruction и предоставляет несколько конкретных производных типов, указывающих на определенные виды ожидания. У вас есть WaitForSeconds, которая возобновляет работу сопрограммы по истечении заданного времени. У вас есть WaitForEndOfFrame, который возобновляет сопрограмму в определенной точке позже в том же кадре. У вас есть сам тип Coroutine, который, когда сопрограмма A дает сопрограмму B, приостанавливает сопрограмму A до завершения сопрограммы B.
На что это похоже с точки зрения времени выполнения? Как я уже сказал, я не работаю в 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 для обработки других случаев - например, можно было бы добавить поддержку сигналов на уровне движка с помощью YieldInstruction WaitForSignal ("SignalName"), поддерживающего это. Добавив больше YieldInstructions, сами сопрограммы могут стать более выразительными - yield return 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;
Конкретные строки yield return new 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();
}
}
В-третьих, тот факт, что вы можете уступать другим сопрограммам, может как бы позволить вам реализовать свои собственные 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 # - это отличная маленькая конструкция, и даже если вы не используете Unity, возможно, вы сочтете полезным воспользоваться ими таким же образом.
IEnumerator
/IEnumerable
(или универсальные эквиваленты) и содержащиеyield
ключевое слово. Найдите итераторы.