Привет, разработчик! Сегодня разберем две важные вещи в Unity - корутины и UniTask. Представь, что ты готовишь обед. Корутины - это как готовить по старинке, а UniTask - как современная мультиварка. Давай разберемся, что лучше.

Что такое корутины?

Корутины - это способ Unity выполнять задачи по частям, не останавливая всю игру. Представь, что ты готовишь обед. Вместо того чтобы стоять у плиты и ждать, пока сварится суп, ты можешь поставить его вариться, пойти нарезать овощи для салата, потом вернуться и проверить суп, снова отойти и накрыть на стол. Так и корутины - они позволяют игре делать несколько дел одновременно.

Важно: Корутины работают только с MonoBehaviour (компонентами Unity).

using System.Collections;
using UnityEngine;

public class SimpleCoroutine : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(CountNumbers());
    }

    IEnumerator CountNumbers()
    {
        for (int i = 1; i <= 5; i++)
        {
            Debug.Log($"Число: {i}");
            yield return new WaitForSeconds(1f);
        }
        Debug.Log("Счет закончен!");
    }
}

Что здесь происходит:

  • Мы создали функцию CountNumbers(), которая возвращает IEnumerator

  • Внутри цикла мы выводим число и ждем секунду

  • yield return new WaitForSeconds(1f) говорит Unity: "Подожди секунду, потом продолжай"

  • Корутина выполняется по частям, не блокируя игру

Что такое UniTask?

UniTask - это современная библиотека для асинхронного программирования. Она работает быстрее корутин и дает больше возможностей. Представь, что корутины - это старый телефон, а UniTask - это смартфон.

Важно: UniTask нужно установить отдельно через Package Manager Unity. Это не встроенная функция, как корутины.

Примечание: В примерах мы используем async UniTaskVoid вместо async void для большей безопасности. async void может привести к необработанным исключениям.

using Cysharp.Threading.Tasks;
using UnityEngine;

public class SimpleUniTask : MonoBehaviour
{
    async UniTaskVoid Start()
    {
        await CountNumbersAsync();
    }

    async UniTask CountNumbersAsync()
    {
        for (int i = 1; i <= 5; i++)
        {
            Debug.Log($"Число: {i}");
            await UniTask.Delay(1000);
        }
        Debug.Log("Счет закончен!");
    }
}

Что здесь происходит:

  • Мы используем async и await - современный способ писать асинхронный код

  • UniTask.Delay(1000) ждет 1000 миллисекунд (1 секунда)

  • Код выглядит как обычный, но выполняется асинхронно

  • UniTask работает быстрее корутин для сложных задач

Сравнение производительности

Корутины создают объекты в памяти, но Unity их кэширует (запоминает) для повторного использования. UniTask работает еще эффективнее и не создает лишних объектов. Представь разницу между покупкой новой коробки для каждой вещи и использованием одной коробки много раз.

// Корутина
yield return new WaitForSeconds(1f);

// UniTask
await UniTask.Delay(1000);

Обработка ошибок

В корутинах сложно обрабатывать ошибки. UniTask делает это просто. Представь разницу между старым телефоном, где нельзя перезвонить, и смартфоном с историей звонков.

Проблема с корутинами

IEnumerator LoadDataCoroutine()
{
    // Если здесь произойдет ошибка, корутина просто остановится
    // и никто об этом не узнает
    yield return new WaitForSeconds(1f);
    
    // Этот код может не выполниться из-за ошибки выше
    Debug.Log("Данные загружены");
}

// Вызываем корутину
void Start()
{
    StartCoroutine(LoadDataCoroutine());
    // Мы не знаем, успешно ли выполнилась корутина
}

Что здесь происходит:

  • Если в корутине произойдет ошибка, она просто остановится

  • Код, который вызвал корутину, не узнает об ошибке

  • Нет способа узнать, что пошло не так

Решение с UniTask

using Cysharp.Threading.Tasks;
using UnityEngine;

async UniTask LoadDataAsync()
{
    try
    {
        await UniTask.Delay(1000);
        Debug.Log("Данные загружены");
    }
    catch (System.Exception e)
    {
        Debug.LogError($"Ошибка загрузки: {e.Message}");
        // Можно попробовать загрузить данные снова
        await RetryLoadData();
    }
}

async UniTask RetryLoadData()
{
    Debug.Log("Пробуем загрузить данные снова...");
    await UniTask.Delay(2000);
    Debug.Log("Данные загружены со второй попытки");
}

// Вызываем UniTask
async UniTaskVoid Start()
{
    try
    {
        await LoadDataAsync();
        Debug.Log("Все прошло успешно!");
    }
    catch (System.Exception e)
    {
        Debug.LogError($"Критическая ошибка: {e.Message}");
    }
}

Что здесь происходит:

  • Если произойдет ошибка, мы сразу об этом узнаем

  • Можем обработать ошибку и попробовать снова

  • Код, который вызвал функцию, тоже узнает об ошибке

  • Есть полный контроль над тем, что делать при ошибке

Отмена операций

UniTask позволяет легко отменить операцию. Корутины этого не умеют.

using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class CancellationExample : MonoBehaviour
{
    private CancellationTokenSource _cancellationTokenSource;

    async UniTaskVoid Start()
    {
        _cancellationTokenSource = new CancellationTokenSource();
        await LongOperation(_cancellationTokenSource.Token);
    }

    async UniTask LongOperation(CancellationToken cancellationToken)
    {
        for (int i = 0; i < 100; i++)
        {
            // Проверяем, не отменили ли операцию
            cancellationToken.ThrowIfCancellationRequested();
            
            Debug.Log($"Шаг {i}");
            await UniTask.Delay(100, cancellationToken: cancellationToken);
        }
    }

    void OnDestroy()
    {
        // Отменяем операцию при уничтожении объекта
        _cancellationTokenSource?.Cancel();
    }
}

Что здесь происходит:

  • CancellationTokenSource - это как пульт управления для отмены операций

  • cancellationToken - это сигнал, который говорит "отмени операцию"

  • cancellationToken.ThrowIfCancellationRequested() - проверяет, не нажали ли кнопку отмены

  • Если операцию отменили, код выбрасывает исключение и останавливается

  • await UniTask.Delay(100, cancellationToken: cancellationToken) - ждет 100 миллисекунд, но может отмениться раньше

  • _cancellationTokenSource?.Cancel() - нажимает кнопку отмены при уничтожении объекта

Когда использовать корутины?

Корутины подходят для простых задач:

  • Анимации

  • Простые задержки

  • Когда не нужна отмена операции

  • Когда ты делаешь простую игру

  • Когда не хочешь устанавливать дополнительные библиотеки

Плюсы корутин:

  • Уже есть в Unity

  • Простые в использовании

  • Подходят для начинающих

Когда использовать UniTask?

UniTask лучше для сложных задач:

  • Загрузка данных из интернета

  • Работа с файлами

  • Когда нужна отмена операции

  • Когда важна производительность

  • Работа с ECS (Entity Component System)

  • Большие проекты

Плюсы UniTask:

  • Быстрее работает

  • Лучше обрабатывает ошибки

  • Можно отменять операции

  • Работает везде, не только с MonoBehaviour

ECS и асинхронность

ECS (Entity Component System) - это другой способ создавать игры в Unity. Вместо MonoBehaviour здесь используются чистые C# классы. Это как разница между готовкой по рецепту (MonoBehaviour) и готовкой по интуиции (ECS).

Проблема с корутинами в ECS

using System.Collections;
using UnityEngine;

// Корутины работают только с MonoBehaviour
public class PlayerSystem : MonoBehaviour
{
    private float playerHealth = 50f;
    private float maxHealth = 100f;
    private float healAmount = 10f;
    
    IEnumerator HealPlayerCoroutine()
    {
        while (playerHealth < maxHealth)
        {
            playerHealth += healAmount;
            yield return new WaitForSeconds(1f);
        }
    }
}

// В ECS нет MonoBehaviour, поэтому корутины не работают!
public class PlayerSystem : SystemBase
{
    // Здесь нельзя использовать корутины
    // IEnumerator HealPlayerCoroutine() - НЕ РАБОТАЕТ!
}

Что здесь происходит:

  • Корутины требуют MonoBehaviour для работы

  • В ECS используются чистые C# классы без MonoBehaviour

  • Корутины просто не запустятся в ECS системах

Решение с UniTask в ECS

using Unity.Entities;
using Unity.Collections;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class PlayerSystem : SystemBase
{
    private bool _isHealing = false; // Флаг, чтобы не запускать лечение много раз

    protected override void OnUpdate()
    {
        // Запускаем лечение только один раз
        if (!_isHealing)
        {
            _isHealing = true;
            _ = HealPlayerAsync(); // _ = означает "запустить и забыть"
        }
    }

    private async UniTask HealPlayerAsync()
    {
        var playerQuery = GetEntityQuery(typeof(PlayerComponent));
        var playerEntities = playerQuery.ToEntityArray(Allocator.Temp);

        foreach (var entity in playerEntities)
        {
            var playerComponent = EntityManager.GetComponentData<PlayerComponent>(entity);
            
            while (playerComponent.health < playerComponent.maxHealth)
            {
                playerComponent.health += playerComponent.healAmount;
                EntityManager.SetComponentData(entity, playerComponent);
                
                await UniTask.Delay(1000); // Ждем 1 секунду
            }
        }

        playerEntities?.Dispose();
        _isHealing = false; // Сбрасываем флаг
    }
}

// Компонент для игрока
public struct PlayerComponent : IComponentData
{
    public float health;
    public float maxHealth;
    public float healAmount;
}

Что здесь происходит:

  • UniTask работает в любом C# классе, включая ECS системы

  • Мы можем использовать await в ECS системах

  • Асинхронные операции работают с компонентами данных

  • Нет зависимости от MonoBehaviour

  • _ = означает "запустить задачу и не ждать её завершения"

  • Allocator.Temp - это способ выделить память для временных данных

Преимущества UniTask в ECS

using Unity.Entities;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class GameManager : SystemBase
{
    private bool _isInitialized = false;

    protected override void OnUpdate()
    {
        // Запускаем инициализацию только один раз
        if (!_isInitialized)
        {
            _isInitialized = true;
            _ = InitializeGameAsync();
        }
    }

    private async UniTask InitializeGameAsync()
    {
        // Запускаем несколько асинхронных операций одновременно
        var loadLevelTask = LoadLevelAsync();
        var updateUITask = UpdateUIAsync();
        var saveGameTask = SaveGameAsync();

        // Ждем завершения всех задач
        await UniTask.WhenAll(loadLevelTask, updateUITask, saveGameTask);
        Debug.Log("Все задачи завершены!");
    }

    private async UniTask LoadLevelAsync()
    {
        // Загружаем уровень
        await UniTask.Delay(2000);
        Debug.Log("Уровень загружен");
    }

    private async UniTask UpdateUIAsync()
    {
        // Обновляем интерфейс
        await UniTask.Delay(500);
        Debug.Log("UI обновлен");
    }

    private async UniTask SaveGameAsync()
    {
        // Сохраняем игру
        await UniTask.Delay(1000);
        Debug.Log("Игра сохранена");
    }
}

Что здесь происходит:

  • В ECS можно запускать несколько асинхронных операций одновременно

  • Каждая операция работает независимо

  • Нет блокировки основного потока

  • Код остается чистым и понятным

  • UniTask.WhenAll() ждет завершения всех задач

Установка UniTask

Чтобы использовать UniTask, нужно его установить:

  1. Открой Window → Package Manager в Unity

  2. Нажми "+" → "Add package from git URL"

  3. Введи: https://github.com/Cysharp/UniTask.git

  4. Нажми "Add"

Или через OpenUPM:

  1. Открой Window → Package Manager

  2. Нажми "+" → "Add package from git URL"

  3. Введи: com.cysharp.unitask

  4. Нажми "Add"

Или скачай с GitHub и добавь в проект вручную.

Заключение

Корутины - это старый, но надежный способ. UniTask - современный и быстрый.

Выбирай так:

  • Корутины - для простых игр, анимаций, когда ты только учишься

  • UniTask - для сложных проектов, когда нужна производительность и контроль

Помни: UniTask нужно установить отдельно, корутины уже есть в Unity.

Комментарии (2)


  1. NeriaLab
    16.07.2025 12:11

    У корутин есть еще одна болячка: она не может запустится в том MonoBehaviour, чей GameObject не активен и сразу падает с ошибкой, так что приходится проводить доп. проверку на activeInHierarchy


  1. Lekret
    16.07.2025 12:11

    1. Корутины можно остановить через StopCoroutine, имхо это даже проще токенов.

    2. На пример с "ECS" больно смотреть, написали бы лучше пример с заведомо async вещами, тот же http-запрос, потому что игровую логику тасками в ECS не пишут, но окей.

    3. Пример с ECS не раскрывает минуса корутин, свой глобальный CoroutineManager для сцены пишется за 5 минут. Я бы наоборот выделял привязку к объекту как возможный плюс, потому что таскаться с токенами, либо ловить нулрефы в повисших тасках не прикольно.