Coroutine в Unity C#: асинхронная работа с примерами
Coroutine в Unity — это метод, который может приостановить своё выполнение и продолжить его позже, уже в другом кадре. Синтаксически корутина — метод с возвращаемым типом IEnumerator, содержащий хотя бы один yield return. Именно эта строка и есть точка приостановки: до неё код выполняется в текущем кадре, после — возобновляется в следующем.
Обычный метод отрабатывает целиком за один кадр. Если попробовать реализовать плавное затухание объекта циклом в Update, объект исчезнет мгновенно — все итерации уложатся в один кадр. Корутина решает эту задачу естественно: одна итерация — один кадр.
Как устроена корутина
Три обязательных элемента:
- Возвращаемый тип
IEnumerator— неvoidи неIEnumerator<T>. Пространство имёнSystem.Collections. yield return— строка, после которой выполнение «подвешивается».- Запуск через
StartCoroutine— сама корутина сама по себе не выполняется, её нужно запустить.
Простейший пример — затухание объекта за 10 кадров:
using UnityEngine;
using System.Collections;
public class Fader : MonoBehaviour
{
private Renderer rend;
void Start()
{
rend = GetComponent(typeof(Renderer)) as Renderer;
StartCoroutine(Fade());
}
IEnumerator Fade()
{
Color c = rend.material.color;
// 10 итераций = 10 кадров
for (int i = 10; i != 0; i--)
{
c.a = i / 10f;
rend.material.color = c;
yield return null; // пауза до следующего кадра
}
}
}
Строка yield return null означает: «продолжи в следующем кадре». Каждая итерация цикла выполняется в своём кадре, и затухание видно глазами.
На что можно «ждать» — инструкции ожидания
Слева от yield return можно поставить разные объекты — от них зависит, когда корутина продолжит работу:
| Инструкция | Когда продолжает | Где пригодится |
|---|---|---|
null |
В следующем кадре | Покадровая логика, анимация через код |
new WaitForSeconds(сек) |
Через указанное игровое время | Задержки геймплея, перезарядка, спавн |
new WaitForSecondsRealtime(сек) |
Через реальное время, не зависит от Time.timeScale |
Таймеры меню и паузы |
new WaitForFixedUpdate() |
После следующего физического обновления | Работа с Rigidbody после расчёта физики |
new WaitForEndOfFrame() |
В конце кадра, после отрисовки всех камер и интерфейса | Скриншоты, чтение экрана через ReadPixels |
StartCoroutine(другая) |
После завершения другой корутины | Последовательности из нескольких этапов |
Пример с задержкой: корутина ждёт 3 секунды между тиками. Такой код выполняет логику раз в интервал, не нагружая Update:
using UnityEngine;
using System.Collections;
public class Ticker : MonoBehaviour
{
void Start()
{
StartCoroutine(Tick());
}
IEnumerator Tick()
{
while (true)
{
Debug.Log("Тик " + Time.time);
// Пауза 3 секунды, затем следующая итерация
yield return new WaitForSeconds(3f);
}
}
}
Остановка всей корутины компонента — одним вызовом. Полезно при деактивации объекта или завершении раунда:
using UnityEngine;
using System.Collections;
public class Spawner : MonoBehaviour
{
void Start()
{
StartCoroutine(SpawnLoop());
StartCoroutine(BackgroundLoop());
// Через 10 секунд останавливаем ВСЕ корутины этого компонента
Invoke(nameof(StopEverything), 10f);
}
IEnumerator SpawnLoop()
{
while (true)
{
Debug.Log("Спавн врага");
yield return new WaitForSeconds(2f);
}
}
IEnumerator BackgroundLoop()
{
while (true)
{
Debug.Log("Фоновая логика");
yield return null;
}
}
void StopEverything()
{
StopAllCoroutines();
Debug.Log("Все корутины остановлены");
}
}
Подробнее о самой распространённой инструкции — WaitForSeconds в Unity C#.
Корутина — не поток
Это принципиальный момент. Корутина выполняется на главном потоке. Синхронные операции внутри неё по-прежнему блокируют главный поток целиком — «подвешивается» только управление выполнением, а не сам процессор. Если задача грузит ЦП, корутина её не разгрузит; для настоящей параллелизации нужны Task и потоки.
Что корутины умеют хорошо — распределять работу по кадрам. Загрузка ресурсов с уведомлением о прогрессе, пошаговая генерация карты, обработка очереди действий по одному за кадр — всё это классические сценарии.
Что останавливает корутину
Корутина завершается сама, когда доходит до конца метода. Принудительно её останавливают так:
StopCoroutine()— останавливает конкретную корутинуStopAllCoroutines()— останавливает все корутины данного компонента
Кроме того, корутина останавливается автоматически, если:
- значение
GameObject.activeSelfстановитсяfalse— объект деактивирован - компонент уничтожен вызовом
Object.Destroy
Важно: отключение компонента через enabled = false корутину не останавливает. Это частая неожиданность: скрипт выключен, а код продолжает выполняться.
Производительность: где корутины создают нагрузку
Каждая запущенная корутина — это конечный автомат на IEnumerator, а запуск сопровождается аллокациями в куче. Практические следствия:
- Не запускайте корутину каждый кадр. Цикл в
Update, стартующий корутину на каждый кадр, создаёт мусор и вызывает всплески сборщика мусора. - Длинные живущие корутины лучше частых коротких. Одна корутина с циклом и
yield return nullвнутри дешевле, чем сотня коротких подряд. - Кэшируйте
WaitForSecondsс фиксированными задержками. Создание объекта ожидания каждый раз — лишняя аллокация. - Следите за завершением. Корутина держит ссылки на владельца и захваченные переменные — «вечная» корутина не даёт объекту освободиться.
Корутины и async/await
В современных версиях Unity из корутины можно вернуть объект Awaitable — так корутина интегрируется с асинхронным кодом. Ограничение: поддерживается только необобщённый Awaitable, Awaitable<T> из корутины возвращать нельзя.
Когда корутины работают
Корутины — в первую очередь среда выполнения рантайма. Инструкции ожидания из UnityEngine работают в режиме Play и в собранной игре. В режиме редактора код с корутинами выполняется, если на скрипте стоят атрибуты ExecuteAlways или ExecuteInEditMode. Для корутин, живущих исключительно в редакторе, есть пакет Editor Coroutines.
Частые ошибки
1. yield в методе без IEnumerator
void MyMethod() { yield return null; } не скомпилируется. Компилятор выдаст: «Cannot yield in the body of a method with return type void». Метод должен возвращать IEnumerator.
2. Вызов корутины как обычного метода
Вызов Fade(); без StartCoroutine создаст объект IEnumerator, но не выполнит ни строчки тела. Правильно: StartCoroutine(Fade());.
3. Работа в цикле без yield
Если внутри while нет ни одной строки yield return, цикл зависнет — кадр никогда не завершится, игра «замрёт». Даже yield return null обязателен.
4. Ожидание остановленной корутины
Если в yield return StartCoroutine(...) передана корутина, которая остановлена извне, внешняя корутина продолжит работу немедленно — планировать логику на «гарантированное завершение» нельзя.