Unity C#

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(...) передана корутина, которая остановлена извне, внешняя корутина продолжит работу немедленно — планировать логику на «гарантированное завершение» нельзя.

Связанные статьи