Зачем в Unity корутины?

1,00
р.
Зачем в Unity корутины? Ведь, насколько я понял, они позволяют выполнять параллельно какие то действия, во время основной работы программы. Цикл ожидания подключения там крутить. В Unity в каждом скрипте мы можем завести Update,FixedUpdate,LateUpdate. При этом у нас есть такой отличный метод как InvokeRepeating, который тоже может
параллельно что то своё запускать, с нужной частотой, и не раз в каждом скрипте.
При том что самих скриптов может быть сколько угодно.
Есть ли какая то реальная необходимость сопрограмм в Unity? Может, они лучше с точки зрения производительности? Или могут применяться более точно?

Ответ
Во-первых, сразу напишу, что не совсем корректно сравнивать корутину с Update,FixedUpdate,LateUpdate и InvokeRepeating. Ибо она может выполняться только один раз. Например
void Start() { Debug.Log("Start game") StartCoroutine(wait()) }
IEnumerator wait() { yield return new WaitForSeconds(3f) Debug.Log("Coroutine is work") }
выведет Start game и через 3 секунды Coroutine is work. Всё. Никаких повторных вызовов.

Далее.
Отличие её от всех вышеперечисленных методов как минимум в наличии ключевого слова yield. Когда в методе-итераторе (коей является корутина) встречается оператор yield return, возвращается выражение expression и сохраняется текущее положение в коде. Выполнение будет продолжено из этого местоположения при очередном вызове функции итератора.
Пример
void Start () { StartCoroutine(Test(FinalAction)) }
IEnumerator Test(Action act) { for (int i = 0 i < 5 i++) { Debug.Log("Test" + i) yield return new WaitForSeconds(1.5f) Debug.Log("Test" + i + i) } }
Будет выведено "Test0", а через 1,5 секунды "Test00", "Test1" (т.к. программа продолжило выполнение с момента где остановилась и потом опять продолжила цикл)... потом "Test11", "Test2" и т.д.
Таким образом корутина позволяет прерывать вычисления, отдавать ресурсы в основной поток, а потом возобновлять следующую часть вычислений.

Другой пример: нам нужно инстанциировать 10.000 объектов. Порциями по 10-100 или просто в цикле, неважно. Если мы воткнем это в методе Update, то пока цикл не отработает обновления экрана не будет, приложение "висит" все это время. У пользователя "бомбит". То есть корутину можно применять для длительных операций, которые можно "размазать" по кадрам. Причем (как написано выше) можно вызывать примерно следующую последовательно действий:
// счетчик цикла Debug.Log("Инстанциируем объекты и складируем их в массив") yield return new WaitForSeconds(1) Debug.Log("делаем доп работу с этим массивом") yield return new WaitForSeconds(1) Debug.Log("Еще какая-то работа") yield return new WaitForSeconds(1)
другой пример с пулями:
void Start () { StartCoroutine("FireThriceAndWait") }
IEnumerator FireThriceAndWait () { while (true) { fire() yield return new WaitForSeconds(0.5f) fire() yield return new WaitForSeconds(0.5f) fire() yield return new WaitForSeconds(5f) } }
void fire(){ Instantiate(enemy_bullet,this.transform.position, Quaternion.LookRotation(target.transform.position- this.transform.position)) }
Такую работу проделать InvokeRepeating не позволит в принципе.
Еще пример. Мы хотим, чтобы до прогона действий и после что-то происходило, например логирование сообщений сделано что-то или нет. Как это делать в InvokeRepeating или Update? Вешать всякий флаги было сделано что-то или нет, зашел в метод или нет? Зачем, если можно сделать её в корутине
void Start () { StartCoroutine(Test(StartAction, FinalAction)) }
IEnumerator Test(Action actBefore,Action actAfter) { actBefore()
for (int i = 0 i < 5 i++) { Debug.Log("Test" + i) yield return new WaitForSeconds(1.5f) Debug.Log("Test" + i + i) }
actAfter() }
void StartAction() { Debug.Log("I'm a start action") }
void FinalAction() { Debug.Log("I'm a final action") }
Ну и еще пример.. допустим хотим мы заставить мигать спрайт (уменьшить прозрачность, увеличить) с интервалом 0.5 сек. Поставим методы в Update - будет виснуть основной поток. Для InvokeRepeating придется ставить разные флаги и доп функции - был ли вызван нужный метод или нет, если да, то повторять другой метод, если нет то первый...Сопрограммой решается это так
IEnumerator Test() { while (true) { var color = obj.GetComponent().material.color for (float i = 1 i >= 0 i-=0.1f) { color.a = i obj.GetComponent().material.color = color yield return null }
yield return new WaitForSeconds(0.5f)
for (float i = 0 i < 1 i += 0.1f) { color.a = i obj.GetComponent().material.color = color yield return null } yield return new WaitForSeconds(0.5f) } }
Циклы можно вынести в отдельные методы - но не суть. Факт в том, что если запустить то сопрограмма будет работать параллельно без зависаний и сложных манипуляций.

А еще корутина может дожидаться действий от ForFixedUpdate, т.е. прерывает выполнение до кадра, в котором обновляется физика (вызывается посредством WaitForFixedUpdate) или конца фрейма (WaitForEndOfFrame). Что бывает полезно сделать и для того же InvokreRepeating придется лепить что-то для этого.

В итоге. Как я описал выше: корутину можно применять для длительных операций, которые можно "размазать" по кадрам от которых главный поток не повиснет. Для некой отдельной микропрограммки, которая будет работать параллельно (пример с миганием спрайта, или запустить персонажа бродить в одну сторону и в другую "tween"), которую сложно зарепитить из-за разности действий.
Да и не стоит забывать, что в том же Update инструкции происходят последовательно, а значит десяток методов с циклами, в которых некие действия, поставленные один за другим будут выполняться последовательно и дольше, нежели корутины и от этого может зависеть сама игра: игрок прыгнул вверх 10 раз, а потом стрельнул 10 раз или прыгнул-стрельнул 10 раз - разница.
Надеюсь я не запутал вас)