Корутина (сопрограмма) в Unity — это метод C#, который возвращает IEnumerator и запускается через StartCoroutine. На каждом yield return метод приостанавливается и отдает управление движку, а Unity продолжает его с того же места позже: в следующем кадре, через заданное время или когда выполнится условие. Весь код корутины при этом выполняется в главном потоке.
Содержание
Ниже — минимальный рабочий пример, что происходит под капотом, чего можно ждать через yield, как правильно останавливать корутину и когда вместо нее лучше взять async/Awaitable из Unity 6.
Три понятия, которые путают
| Понятие | Что это | Где выполняется код |
|---|---|---|
| Корутина | итератор C#, который Unity «докручивает» по кадрам | главный поток Unity |
Поток (Thread, Task.Run) |
параллельное выполнение средствами .NET | другой поток; API Unity оттуда вызывать нельзя |
async/await с Awaitable |
асинхронный метод C# с типом ожидания от Unity | главный поток, пока явно не перейти в фоновый |
Главное следствие: корутина не делает тяжелый расчет быстрее и не разгружает процессор. Если внутри нее цикл на миллион итераций без yield, кадр зависнет точно так же, как в Update.
Минимальный пример: обратный отсчет
Скрипт вешается на любой объект сцены. Через секунду после запуска в Console появляется следующая строка отсчета.
using System.Collections;
using UnityEngine;
public class CountdownExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Countdown(3));
Debug.Log("Start закончился, корутина продолжает работу");
}
IEnumerator Countdown(int seconds)
{
for (int i = seconds; i > 0; i--)
{
Debug.Log($"Осталось {i} с");
yield return new WaitForSeconds(1f);
}
Debug.Log("Поехали!");
}
}
Порядок сообщений в Console:
Осталось 3 с
Start закончился, корутина продолжает работу
Осталось 2 с
Осталось 1 с
Поехали!
Обратите внимание на первые две строки. StartCoroutine выполняет тело корутины сразу, синхронно, до первого yield, и только потом возвращает управление в Start. Остальные шаги идут с паузой около секунды: WaitForSeconds продолжает работу в первом кадре, когда время ожидания истекло, поэтому пауза не короче заданной, но может быть длиннее на долю кадра.
Что происходит под капотом
Unity опирается на обычные итераторы C#. Вызов метода с yield не выполняет его тело, а только создает объект-итератор. Каждый вызов MoveNext() выполняет код до следующего yield. Это можно увидеть без Unity, в консольном приложении .NET:
using System;
using System.Collections;
class Program
{
static IEnumerator Countdown()
{
Console.WriteLine("старт");
for (int i = 3; i > 0; i--)
{
Console.WriteLine(i);
yield return null; // пауза: вернуть управление вызывающему
}
Console.WriteLine("готово");
}
static void Main()
{
IEnumerator routine = Countdown(); // тело еще НЕ выполнялось
Console.WriteLine("итератор создан");
int frame = 0;
while (routine.MoveNext()) // так Unity "двигает" корутину
{
frame++;
Console.WriteLine($"--- конец кадра {frame}");
}
Console.WriteLine(ReferenceEquals(Countdown(), Countdown()));
}
}
Вывод на .NET 10:
итератор создан
старт
3
--- конец кадра 1
2
--- конец кадра 2
1
--- конец кадра 3
готово
False
Это упрощенная модель: настоящий планировщик Unity еще смотрит на то, что вернул yield (время, условие, другую корутину). Но из примера видны две вещи, на которых строятся частые ошибки. Первая: вызов Countdown() без StartCoroutine ничего не запустит. Вторая: каждый вызов метода создает новый итератор, поэтому ReferenceEquals дает False.
Чего можно ждать через yield
| Что вернуть | Когда корутина продолжится |
|---|---|
yield return null |
в следующем кадре, после Update всех скриптов |
new WaitForSeconds(t) |
через t секунд игрового времени (зависит от Time.timeScale) |
new WaitForSecondsRealtime(t) |
через t секунд реального времени, пауза игры не влияет |
new WaitForFixedUpdate() |
после очередного шага физики (FixedUpdate) |
new WaitForEndOfFrame() |
в конце кадра, после отрисовки |
new WaitUntil(() => условие) / WaitWhile |
когда условие станет истинным / ложным |
StartCoroutine(Other()) |
когда закончится вложенная корутина |
yield break |
корутина завершается сразу |
Практическое следствие про паузу: если игра ставится на паузу через Time.timeScale = 0, корутина на WaitForSeconds не продолжится, пока время снова не пойдет. Для меню паузы и таймеров интерфейса берут WaitForSecondsRealtime.
Как запустить корутину: три варианта
| Запуск | Параметры | Как остановить |
|---|---|---|
StartCoroutine(Blink(0.5f)) |
любые, проверяются компилятором | по сохраненному Coroutine или тому же IEnumerator |
StartCoroutine("Fade", 2f) |
не больше одного, тип object |
StopCoroutine("Fade") |
StartCoroutine(routine), где routine — сохраненный IEnumerator |
заданы при создании итератора | StopCoroutine(routine) |
Строковый вариант хрупкий: опечатку в имени компилятор не поймает. Основной выбор — передача IEnumerator с сохранением результата StartCoroutine.
Корутина принадлежит компоненту MonoBehaviour, у которого вызвали StartCoroutine. Если написать other.StartCoroutine(...), остановить ее можно будет только через other.
Как остановить корутину правильно
Надежный способ — сохранить объект Coroutine, который возвращает StartCoroutine, и передать его в StopCoroutine. Пример: мигание объекта, которое включается и выключается по команде.
using System.Collections;
using UnityEngine;
public class Blinker : MonoBehaviour
{
Coroutine blinkRoutine; // ссылка на запущенную корутину
Renderer rend;
void Awake() => rend = GetComponent<Renderer>();
public void StartBlink()
{
StopBlink(); // не плодить вторую копию
blinkRoutine = StartCoroutine(Blink(0.5f));
}
public void StopBlink()
{
if (blinkRoutine == null) return;
StopCoroutine(blinkRoutine);
blinkRoutine = null;
rend.enabled = true; // вернуть объект в исходное состояние
}
IEnumerator Blink(float interval)
{
var wait = new WaitForSeconds(interval); // один объект на весь цикл
while (true)
{
rend.enabled = !rend.enabled;
yield return wait;
}
}
}
Повторный StartBlink не запускает вторую копию мигания, после остановки объект снова видим, а WaitForSeconds создается один раз на весь цикл.
Ошибка: остановка через новый итератор
Неверно:
void Start() => StartCoroutine(Tick());
void Stop() => StopCoroutine(Tick()); // новый итератор, не тот, что запущен
IEnumerator Tick()
{
while (true) { Debug.Log("tick"); yield return null; }
}
Результат: tick продолжает печататься каждый кадр. Tick() в StopCoroutine создает новый итератор (как False в примере выше), а запущенный остается работать. Исправление — сохранить то, что запускали: Coroutine tickRoutine = StartCoroutine(Tick()); и затем StopCoroutine(tickRoutine);.
Другие способы завершения
yield breakвнутри корутины — завершает ее по собственному условию.StopAllCoroutines()— останавливает все корутины этого компонента, а не всей сцены или проекта.Destroyкомпонента или объекта иgameObject.SetActive(false)останавливают корутины автоматически.enabled = falseу скрипта корутины не останавливает:Updateперестает вызываться, а корутина идет дальше.
Отсюда же известная ошибка: на неактивном объекте корутину запустить нельзя, Unity выведет ошибку в Console и не выполнит ее.
Если не получилось
| Симптом | Вероятная причина | Что сделать |
|---|---|---|
| Корутина не выполняется вообще | метод вызван как Countdown() без StartCoroutine |
обернуть вызов в StartCoroutine |
| Редактор завис при запуске | цикл while (true) без yield внутри |
добавить yield return null или ожидание в каждую итерацию |
| Таймер встал во время паузы | WaitForSeconds при Time.timeScale = 0 |
WaitForSecondsRealtime |
| Эффект запускается дважды и дергается | повторный StartCoroutine без остановки старой |
хранить Coroutine и останавливать перед запуском |
| Остановка не срабатывает | StopCoroutine(Method()) с новым итератором или строкой при запуске через IEnumerator |
останавливать тем же объектом, которым запускали |
| Корутина оборвалась без видимой причины | объект деактивирован или уничтожен; исключение внутри корутины | проверить SetActive/Destroy, посмотреть ошибку в Console |
Корутины или async/Awaitable в Unity 6
В Unity 6 есть тип Awaitable, который позволяет писать ту же логику через async/await. Пример ждет секунду, считает сумму в фоновом потоке и возвращается в главный поток, чтобы снова работать с API Unity:
using UnityEngine;
public class AwaitableExample : MonoBehaviour
{
async void Start()
{
try
{
await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken);
Debug.Log("Прошла секунда");
await Awaitable.BackgroundThreadAsync(); // дальше - фоновый поток
long sum = 0;
for (int i = 0; i < 10_000_000; i++) sum += i;
await Awaitable.MainThreadAsync(); // обратно в главный поток
Debug.Log($"Сумма: {sum}");
}
catch (System.OperationCanceledException)
{
// объект уничтожили раньше, чем закончилось ожидание
}
}
}
Ожидаемо в Console появятся «Прошла секунда» и «Сумма: 49999995000000». Токен destroyCancellationToken отменяет ожидание, если объект уничтожен: в отличие от корутин, async-метод сам по себе при уничтожении объекта не останавливается.
| Задача | Корутина | async + Awaitable |
|---|---|---|
| Пауза, таймер, анимация по кадрам | удобно и привычно | тоже можно |
| Вернуть результат вызывающему коду | неудобно (через поле или колбэк) | естественно: await метода с результатом |
| Обработка ошибок | исключение обрывает корутину | обычные try/catch |
| Тяжелый расчет вне главного потока | нет | BackgroundThreadAsync (API Unity оттуда не вызывать) |
| Автоостановка при уничтожении объекта | да | только через токен отмены |
Выводы
- Корутина в Unity — метод с
IEnumerator, который движок продолжает после каждогоyield; выполняется в главном потоке и не ускоряет расчеты. - Запускать через
StartCoroutine: тело выполнится сразу до первогоyield, дальше — по кадрам или по условию ожидания. - Останавливать надежнее всего сохраненным
Coroutine; вызовStopCoroutine(Method())создает новый итератор и ничего не останавливает. StopAllCoroutinesдействует на один компонент;SetActive(false)иDestroyостанавливают корутины,enabled = false— нет.- В Unity 6 для цепочек ожиданий с результатом и фоновых расчетов удобнее
async+AwaitableсdestroyCancellationToken.
Где применяется / связь с практикой
Корутины встречаются в любом Unity-проекте: таймеры способностей, волны врагов, плавное появление интерфейса, задержка перед сменой сцены, загрузка с индикатором прогресса. Хорошая тренировка — собрать небольшую сцену, где один скрипт запускает, перезапускает и останавливает несколько корутин, и проверить поведение при паузе и деактивации объекта.
Освойте тему на практике
Системно C#, жизненный цикл MonoBehaviour и игровую логику на Unity разбирают на курсе Unity Game Developer. Basic. Посмотреть формат занятий и задать вопросы преподавателям можно на открытых уроках Otus.
FAQ
Можно ли сделать Start корутиной?
Да, если объявить его как IEnumerator Start(): Unity сама запустит его как корутину, и внутри можно использовать yield.
Как вернуть значение из корутины?
Напрямую через yield результат не вернуть. Обычно пишут результат в поле или передают колбэк System.Action<T> параметром; в Unity 6 проще сделать асинхронный метод с Awaitable<T>.
Выделяет ли корутина память на каждом шаге?
Сам запуск создает объекты итератора и Coroutine, а new WaitForSeconds в цикле создает новый объект на каждой итерации. Поэтому объект ожидания в частых циклах создают один раз, как в примере с миганием; yield return null новых объектов не создает.



