Форум программистов, компьютерный форум, киберфорум
GameUnited
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Вложенные корутины в Unity

Запись от GameUnited размещена 28.06.2025 в 18:24
Показов 5260 Комментарии 0

Нажмите на изображение для увеличения
Название: Вложенные корутины в Unity.jpg
Просмотров: 361
Размер:	190.9 Кб
ID:	10941
Работа с корутинами в Unity кажется простой до тех пор, пока не начинаешь их вкладывать друг в друга. Я потратил несколько месяцев на изучение этого механизма, и до сих пор иногда ловлю себя на том, что корутины ведут себя не так, как ожидаешь. Особенно когда речь заходит о синхронизации и ожидании завершения одной корутины перед запуском другой. Проблема в том, что Unity предоставляет несколько способов работы с корутинами, но документация не всегда объясняет нюансы их поведения. В результате разработчики часто сталкиваются с ситуациями, когда код работает не так, как задумано: корутины выполняются не в том порядке, зависают или создают утечки памяти.

Самая частая проблема - неправильное понимание разности между асинхронным и синхронным выполнением корутин. Многие думают, что StartCoroutine() всегда ведет себя предсказуемо, но это далеко не так. Иногда нужно дождаться завершения одной корутины перед запуском другой, иногда - запустить несколько параллельно и ждать их всех, а иногда - прервать выполнение посередине.

Почему yield return StartCoroutine часто не работает как ожидается



Первая ловушка, в которую попадает практически каждый разработчик - это понимание того, что происходит при вызове yield return StartCoroutine(). На первый взгляд кажется, что эта конструкция должна запустить корутину и дождаться её завершения. И в большинстве случаев так и происходит, но есть нюансы.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerator MainCoroutine()
{
    Debug.Log("Начинаем основную корутину");
    
    // Пытаемся дождаться завершения дочерней корутины
    yield return StartCoroutine(ChildCoroutine());
    
    Debug.Log("Дочерняя корутина завершена");
}
 
IEnumerator ChildCoroutine()
{
    Debug.Log("Дочерняя корутина запущена");
    yield return new WaitForSeconds(2f);
    Debug.Log("Дочерняя корутина завершается");
}
Этот код работает корректно в большинстве случаев, но проблемы начинаются, когда дочерняя корутина может завершиться с ошибкой или быть остановленной извне. Unity не предоставляет встроенного механизма обработки исключений в корутинах, поэтому если ChildCoroutine прерывается аварийно, родительская корутина может зависнуть в ожидании.
Еще одна проблема связана с жизненным циклом объектов. Если GameObject, на котором запущена дочерняя корутина, уничтожается, то yield return StartCoroutine() в родительской корутинке может не получить сигнал о завершении. Это приводит к тому, что код после yield никогда не выполнится.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
IEnumerator ProblematicCoroutine()
{
    GameObject temporaryObject = new GameObject();
    MonoBehaviour component = temporaryObject.AddComponent<CoroutineRunner>();
    
    // Запускаем корутину на временном объекте
    yield return StartCoroutine(component.LongRunningTask());
    
    // Объект может быть уничтожен до завершения задачи
    Destroy(temporaryObject);
    
    // Этот код может никогда не выполниться
    Debug.Log("Задача завершена");
}
Третья проблема - это неочевидное поведение при вложенности нескольких уровней корутин. Когда корутина A запускает корутину B, которая в свою очередь запускает корутину C, цепочка становится хрупкой. Остановка любого звена может привести к непредсказуемым результатам. Особенно коварно ведут себя корутины при работе с UI-анимациями. Пользователь может случайно нажать кнопку дважды, что приведет к запуску двух одинаковых цепочек корутин. В результате анимации будут перекрывать друг друга, а состояние интерфейса станет непредсказуемым.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UIController : MonoBehaviour
{
    private bool isAnimating = false;
    
    public void OnButtonClick()
    {
        if (isAnimating) return; // Простая защита от двойного нажатия
        
        StartCoroutine(AnimateButton());
    }
    
    IEnumerator AnimateButton()
    {
        isAnimating = true;
        
        // Анимация может прерваться, и флаг останется true навсегда
        yield return StartCoroutine(ScaleAnimation());
        yield return StartCoroutine(ColorAnimation());
        
        isAnimating = false;
    }
}
Проблема усугубляется тем, что Unity не предоставляет удобных инструментов для отслеживания состояния корутин. В отличие от Task в .NET, у корутин нет свойства Status или IsCompleted. Единственный способ понять, что корутина завершилась - это дождаться возврата управления после yield return.

Еще один подводный камень связан с порядком выполнения. Многие разработчики думают, что корутины выполняются строго по очереди, но это не так. Unity может перемешивать порядок выполнения корутин в зависимости от времени их запуска и приоритетов системы. Особенно это заметно при работе с файловой системой или сетевыми запросами.

Пролагивают корутины в Unity
Я делаю игру где надо считать счёт и пройденое расстояние для этого я использую корутины. Игра...

Интерактивный музей, задержка действий на время выполнения анимации и Корутины в Unity
Доброго времени суток! Я новичок. Делаю проект интерактивного музея, там есть театральная сцена...

Анимация в unity и корутины
Добрый день. Мне нужно что бы по нажатию кнопки у меня менялись декорации на сцене. У меня есть...

Не запускаются корутины при выполнении If, Unity
у меня задача чтобы бот сначала просто бежал в направлении к игроку(Run()), потом когда он...


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



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

Первая проблема связана с тем, что каждая корутина создает итератор - объект, который хранит состояние выполнения. При глубокой вложенности корутин количество таких объектов растет экспоненциально. Я видел проекты, где одна "главная" корутина порождала цепочку из 10-15 уровней вложенности, и каждый уровень создавал свой итератор с собственным стеком вызовов.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
IEnumerator DeepNestedCoroutine(int depth)
{
    if (depth <= 0)
    {
        yield return new WaitForSeconds(0.1f);
        yield break;
    }
    
    // Каждый уровень создает новый итератор
    yield return StartCoroutine(DeepNestedCoroutine(depth - 1));
    yield return StartCoroutine(DeepNestedCoroutine(depth - 1));
    
    // При depth=10 создается 2^10 = 1024 итератора
}
Unity хранет все активные корутины в списке и обрабатывает их каждый кадр. Чем больше корутин активно, тем больше времени движок тратит на их обход. При этом большинство корутин в типичный момент времени просто ждут - выполняют WaitForSeconds или WaitForEndOfFrame. Но Unity все равно должен проверить каждую из них. Особенно болезненно это сказывается на мобильных платформах, где производительность CPU ограничена. Я сталкивался с кейсом, где 200 одновременно работающих корутин UI-анимации вызывали просадки FPS с 60 до 30 кадров в секунду только за счет накладных расходов на их обработку.

Вторая проблема - это сборка мусора. Каждый yield в корутине потенциально создает новый объект. WaitForSeconds, WaitForEndOfFrame, и особенно пользовательские YieldInstruction-ы аллоцируют память. При активном использовании корутин сборщик мусора начинает срабатывать чаще, что приводит к заметным фризам.

C#
1
2
3
4
5
6
7
8
9
10
11
IEnumerator BadCoroutine()
{
    while (true)
    {
        // Создает новый объект каждую итерацию
        yield return new WaitForSeconds(Random.Range(0.1f, 0.5f));
        
        // Еще один объект для каждого кадра
        yield return new WaitForEndOfFrame();
    }
}
Хуже всего дела обстоят с корутинами, которые выполняют частые операции с коллекциями или строками. Каждая конкатенация строк или добавление элемента в список внутри корутины может вызвать аллокацию. А поскольку корутины часто используются для логики, которая выполняется длительное время, эти аллокации накапливаются.

Третий источник проблем - это неправильное использование yield return null. Многие разработчики думают, что эта конструкция "бесплатна", но на самом деле она заставляет корутину выполняться каждый кадр. Если у вас есть корутина, которая должна ждать выполнения условия, но проверяет его через yield return null, она будет расходовать CPU каждый кадр.

C#
1
2
3
4
5
6
7
8
9
10
11
IEnumerator WaitForCondition()
{
    while (!someCondition)
    {
        // Выполняется 60 раз в секунду при 60 FPS
        yield return null;
    }
    
    // Гораздо эффективнее было бы:
    // yield return new WaitUntil(() => someCondition);
}
Еще одна неочевидная проблема связана с корутинами, которые работают с Transform-компонентами. Доступ к position, rotation или scale через свойства является довольно дорогой операцией, поскольку Unity должен синхронизировать данные между C# и нативным кодом. Если корутина выполняет анимацию, меняя эти свойства каждый кадр, накладные расходы могут оказаться значительными. Профилирование корутин тоже представляет отдельную сложность. В Unity Profiler корутины не всегда отображаются корректно, особенно вложенные. Часто приходится использовать собственные инструменты измерения времени, чтобы понять, где именно тратится производительность.

На практике я видел проекты, где переход от корутин к более специализированным решениям - DOTween для анимаций, системе событий для UI, пулам объектов для частых операций - давал прирост производительности в 20-30%. Корутины удобны для прототипирования, но в продакшене часто требуют более тщательного подхода.

Основы вложенности корутин



Механизм вложенности корутин в Unity построен на принципе итераторов C#, но с существенными модификациями со стороны движка. Когда мы пишем yield return StartCoroutine(), происходит не простая передача управления, а создание сложной иерархической структуры, которая требует понимания внутренних процессов Unity.

Каждая корутина представляет собой объект, реализующий интерфейс IEnumerator. Unity хранит ссылки на все активные корутины в специальном менеджере, который обрабатывает их в определенном порядке каждый кадр. Когда корутина A запускает корутину B через StartCoroutine(), создается связь типа "родитель-потомок", но эта связь не является строгой иерархией в традиционном понимании.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CoroutineNestingExample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(ParentCoroutine());
    }
    
    IEnumerator ParentCoroutine()
    {
        Debug.Log("Родительская корутина начала работу");
        
        // Запуск дочерней корутины с ожиданием завершения
        yield return StartCoroutine(ChildCoroutine("Первая"));
        
        Debug.Log("Первая дочерняя завершена, запускаем вторую");
        
        // Последовательное выполнение
        yield return StartCoroutine(ChildCoroutine("Вторая"));
        
        Debug.Log("Родительская корутина завершена");
    }
    
    IEnumerator ChildCoroutine(string name)
    {
        Debug.Log($"Дочерняя корутина {name} запущена");
        
        for (int i = 0; i < 3; i++)
        {
            yield return new WaitForSeconds(0.5f);
            Debug.Log($"{name}: шаг {i + 1}");
        }
        
        Debug.Log($"Дочерняя корутина {name} завершена");
    }
}
Ключевое отличие между различными способами запуска корутин заключается в том, как Unity управляет их жизненным циклом. Существует три основных паттерна вложенности, каждый из которых имеет свои особенности и применение:

Синхронное выполнение через yield return StartCoroutine() создает блокирующую операцию. Родительская корутина приостанавливается до полного завершения дочерней. Этот подход идеально подходит для пошаговых операций, где каждый следующий шаг зависит от результата предыдущего. Однако здесь кроется подвох - если дочерняя корутина никогда не завершится, родительская зависнет навсегда.

Асинхронное выполнение through простой вызов StartCoroutine() без yield return запускает дочернюю корутину параллельно. Родительская корутина продолжает выполнение немедленно, не дожидаясь завершения дочерней. Это полезно для запуска фоновых задач - анимаций, звуковых эффектов, периодических проверок состояния.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerator AsyncCoroutineExample()
{
    Debug.Log("Запускаем фоновые задачи");
    
    // Запускаем анимацию фона (не ждем завершения)
    StartCoroutine(BackgroundAnimation());
    
    // Запускаем музыку (не ждем)
    StartCoroutine(PlayBackgroundMusic());
    
    // Продолжаем основную логику
    Debug.Log("Основная логика выполняется параллельно");
    
    yield return new WaitForSeconds(1f);
    Debug.Log("Основная задача завершена, фон может продолжать работу");
}
Условное выполнение возможно через сохранение ссылки на корутину и проверку её состояния. Unity возвращает объект Coroutine при вызове StartCoroutine(), который можно использовать для дальнейшего управления. Этот подход дает максимальный контроль, но требует дополнительного кода для управления состоянием.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IEnumerator ConditionalCoroutineExample()
{
    Debug.Log("Запускаем управляемую корутину");
    
    Coroutine managedCoroutine = StartCoroutine(LongRunningTask());
    
    // Выполняем другую работу
    yield return new WaitForSeconds(2f);
    
    // Решаем прервать задачу по условию
    if (ShouldCancelTask())
    {
        StopCoroutine(managedCoroutine);
        Debug.Log("Задача прервана принудительно");
    }
    else
    {
        // Дожидаемся завершения
        yield return managedCoroutine;
        Debug.Log("Задача завершена естественно");
    }
}
Важный нюанс заключается в том, что Unity не гарантирует порядок выполнения корутин в рамках одного кадра. Если несколько корутин готовы к выполнению одновременно, порядок их обработки может варьироваться. Это особенно заметно при работе с корутинами, которые используют yield return null или WaitForEndOfFrame.

Еще одна особенность касается передачи данных между вложенными корутинами. в отличие от обычных функций, корутины не могут напрямую возвращать значения через return. Вместо этого приходится использоватькласы-обертки или ссылочные параметры.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CoroutineResult<T>
{
    public T Value { get; set; }
    public bool IsCompleted { get; set; }
}
 
IEnumerator DataExchangeExample()
{
    var result = new CoroutineResult<string>();
    
    yield return StartCoroutine(DataProducerCoroutine(result));
    
    if (result.IsCompleted)
    {
        Debug.Log($"Получен результат: {result.Value}");
    }
}
 
IEnumerator DataProducerCoroutine(CoroutineResult<string> result)
{
    yield return new WaitForSeconds(1f);
    
    result.Value = "Данные готовы";
    result.IsCompleted = true;
}
Глубина вложенности корутин теоретически не ограничена, но практически каждый уровень добавляет накладные расходы. Unity должен отслеживать состояние каждой корутины в иерархии, что увеличивает потребление памяти и времени CPU. На практике вложенность больше 5-7 уровней начинает заметно влиять на производительность, особенно на мобильных устройствах.

Разбор механизма StartCoroutine против прямого вызова



Разница между StartCoroutine(SomeMethod()) и простым вызовом SomeMethod() кажется очевидной только на первый взгляд. На самом деле эти два подхода приводят к кардинально разному поведению программы, и понимание этих различий критично для написания стабильного кода. Когда мы вызываем метод напрямую SomeMethod(), он выполняется немедленно и полностью в текущем кадре. Если этот метод содержит обращения к yield, они просто игнорируются, поскольку Unity не знает, что метод должен обрабатываться как корутина. Результат - неработающий код, который может молча пропускать важную логику.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
IEnumerator ProblematicDirectCall()
{
    Debug.Log("Начинаем работу");
    
    // НЕПРАВИЛЬНО: прямой вызов корутины
    AnimateObject(); // Весь код выполнится мгновенно, yield будет проигнорирован
    
    Debug.Log("Анимация 'завершена'");
    yield return new WaitForSeconds(1f);
}
 
IEnumerator AnimateObject()
{
    for (int i = 0; i < 10; i++)
    {
        transform.position += Vector3.up * 0.1f;
        yield return new WaitForSeconds(0.1f); // Этот yield никогда не выполнится
    }
}
StartCoroutine(), напротив, создает новый контекст выполнения для метода. Unity регистрирует метод в своем внутреннем планировщике корутин и начинает выполнять его поэтапно, кадр за кадром. каждый yield становится точкой приостановки, где управление возвращается движку. Критическое различие проявляется в том, как обрабатывается стек вызовов. При прямом вызове метод выполняется в контексте текущего стека, а все локальные переменные остаются доступными. При использовании StartCoroutine создается отдельный контекст итератора, который сохраняет состояние между вызовами.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
IEnumerator StackContextExample()
{
    string localVariable = "Доступна в основной корутине";
    
    // Прямой вызов - переменная доступна
    DirectMethod();
    
    // Корутина - создается отдельный контекст
    yield return StartCoroutine(SeparateContext());
    
    // localVariable все еще доступна здесь
    Debug.Log(localVariable);
}
 
void DirectMethod()
{
    // Эта переменная НЕ видна из основной корутины
    string methodLocal = "Локальная переменная метода";
}
 
IEnumerator SeparateContext()
{
    // Эта корутина имеет свой собственный контекст
    // localVariable из StackContextExample здесь недоступна
    string contextLocal = "Локальная переменная корутины";
    yield return null;
}
Еще один important аспект касается обработки исключений. Исключения в прямо вызванных методах поднимаются по стеку и могут быть перехвачены стандартными try-catch блоками. Исключения в корутинах ведут себя иначе - они логируются Unity, но не прерывают выполнение родительского кода.

Производительность this двух подходов тоже различается существенно. Прямой вызов имеет минимальные накладные расходы - это обычный вызов метода C#. StartCoroutine создает объект итератора, регистрирует его в планировщике Unity, выделяет дополнительную память для хранения состояния. Для простых операций без yield эта разница может быть критичной.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Тест производительности разных подходов
IEnumerator PerformanceComparison()
{
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    
    // Прямые вызовы
    sw.Start();
    for (int i = 0; i < 1000; i++)
    {
        SimpleMath(); // ~0.1ms для 1000 вызовов
    }
    sw.Stop();
    Debug.Log($"Прямые вызовы: {sw.ElapsedMilliseconds}ms");
    
    // Корутины
    sw.Reset();
    sw.Start();
    for (int i = 0; i < 1000; i++)
    {
        StartCoroutine(SimpleMathCoroutine()); // ~15ms для 1000 вызовов
    }
    yield return new WaitForSeconds(0.1f); // даем время завершиться
    sw.Stop();
    Debug.Log($"Корутины: {sw.ElapsedMilliseconds}ms");
}
Особую осторожность нужно проявлять при работе с корутинами, которые возвращают значения. Прямой вызов корутины возвращает объект итератора IEnumerator, а не результат выполнения логики. Это часто приводит к логическим ошибкам в коде.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
IEnumerator CalculateResult()
{
    yield return new WaitForSeconds(1f);
    // Как "вернуть" результат из корутины?
    // return 42; // Так нельзя!
}
 
void WrongUsage()
{
    // result будет содержать IEnumerator, а не число!
    var result = CalculateResult();
    Debug.Log($"Result: {result}"); // Выведет тип объекта, а не значение
}
Правильный подход требует использования ссылочных параметров или классов-оберток для передачи результатов из корутин. Это делает код более громоздким, но обеспечивает корректную работу.

В практических проектах я часто вижу смешение этих подходов без четкого понимания последствий. разработчики то вызывают методы напрямую, то заверачивают в StartCoroutine, не осознавая, что получают совершенно разное поведение. Это приводит к трудноотлавливаемым багам, особенно в сложной логике с множественными условными переходами.

Типичные ошибки при создании цепочек корутин



Цепочки корутин в Unity напоминают карточный домик - одна неправильно поставленная карта может обрушить всю конструкцию. За годы работы с корутинами я собрал коллекцию наиболее распространенных ошибок, которые делают не только начинающие, но и опытные разработчики.

Самая коварная ошибка - это создание циклических зависимостей между корутинами. Корутина A ждет завершения корутины B, а корутина B в свою очередь ждет корутину A. В результате получается deadlock, который может проявиться не сразу, а только при определенных условиях.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
IEnumerator DeadlockExample()
{
    Debug.Log("Запускаем корутину A");
    yield return StartCoroutine(CoroutineA());
}
 
IEnumerator CoroutineA()
{
    Debug.Log("Корутина A ждет корутину B");
    yield return StartCoroutine(CoroutineB());
    Debug.Log("A завершена"); // Никогда не выполнится
}
 
IEnumerator CoroutineB()
{
    Debug.Log("Корутина B ждет корутину A");
    yield return StartCoroutine(CoroutineA()); // Рекурсивный вызов!
    Debug.Log("B завершена"); // Никогда не выполнится
}
Вторая частая проблема связана с неправильным управление ресурсами в цепочках. Разработчики часто забывают освобождать ресурсы, если цепочка корутин прерывается посередине. Особенно это касается загруженных ассетов, открытых файлов или подключений к сети.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IEnumerator ResourceLeakExample()
{
    Texture2D loadedTexture = null;
    
    try
    {
        // Загружаем ресурс
        yield return StartCoroutine(LoadTextureCoroutine(result => loadedTexture = result));
        
        // Используем ресурс
        yield return StartCoroutine(ProcessTexture(loadedTexture));
        
        // Если корутина прервется здесь, ресурс не освободится
        yield return StartCoroutine(ApplyEffects(loadedTexture));
    }
    finally
    {
        // Unity не гарантирует выполнение finally в корутинах!
        if (loadedTexture != null)
        {
            DestroyImmediate(loadedTexture);
        }
    }
}
Третья ошибка - неправильная обработка null-ссылок в цепочках. Unity может уничтожить GameObject посередине выполнения цепочки корутин, но сами корутины продолжат выполняться. Обращения к компонентам уничтоженного объекта приведут к NullReferenceException.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ChainedCoroutineComponent : MonoBehaviour
{
    IEnumerator FragileChain()
    {
        Debug.Log("Начинаем цепочку");
        yield return StartCoroutine(Phase1());
        
        // Объект может быть уничтожен между фазами
        if (this == null) yield break; // Проверка обязательна!
        
        yield return StartCoroutine(Phase2());
        
        // transform может быть null здесь
        transform.position = Vector3.zero; // Потенциальная ошибка!
    }
    
    IEnumerator Phase1()
    {
        yield return new WaitForSeconds(5f);
        // Долгая операция, за время которой объект может исчезнуть
    }
    
    IEnumerator Phase2()
    {
        // Эта корутина может запуститься на уничтоженном объекте
        yield return new WaitForSeconds(2f);
    }
}
Четвертая типичная ошибка касается неправильного порядка остановки корутин в цепочках. Многие разработчики вызывают StopAllCoroutines(), не понимая, что это может привести к неконсистентному состоянию системы. Если остановить родительскую корутину, дочерние продолжат работать.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class IncorrectStopExample : MonoBehaviour
{
    private Coroutine mainChain;
    
    void Start()
    {
        mainChain = StartCoroutine(MainChain());
    }
    
    IEnumerator MainChain()
    {
        // Запускаем дочерние корутины
        StartCoroutine(BackgroundTask()); // Эта корутина НЕ остановится
        yield return StartCoroutine(PrimaryTask());
        StartCoroutine(CleanupTask()); // И эта тоже
    }
    
    void OnDestroy()
    {
        // НЕПРАВИЛЬНО: останавливаем только главную цепочку
        if (mainChain != null)
        {
            StopCoroutine(mainChain);
        }
        // Дочерние корутины продолжат работать на уничтоженном объекте!
    }
}
Пятая ошибка связана с неправильным использованием общих переменных между звеньями цепочки. Разработчики часто предполагают, что переменные сохранят свои значения между различными корутинами в цепи, но это не всегда так.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SharedStateError : MonoBehaviour
{
    private int sharedCounter = 0;
    
    IEnumerator ProblematicChain()
    {
        sharedCounter = 10;
        
        // Запускаем параллельную корутину, которая может изменить счетчик
        StartCoroutine(ModifyCounter());
        
        yield return new WaitForSeconds(1f);
        
        // sharedCounter может быть уже не 10!
        yield return StartCoroutine(UseCounter(sharedCounter));
    }
    
    IEnumerator ModifyCounter()
    {
        yield return new WaitForSeconds(0.5f);
        sharedCounter = 0; // Неожиданно меняем значение
    }
    
    IEnumerator UseCounter(int expectedValue)
    {
        // expectedValue может не соответствовать реальному sharedCounter
        if (sharedCounter != expectedValue)
        {
            Debug.LogError("Состояние нарушено!");
        }
        yield return null;
    }
}
Шестая распространенная ошибка - игнорирование временных ограничений. цепочки корутин могут выполняться очень долго, особенно при неправильной логике ожидания. Отсутствие таймаутов приводит к зависанию игры в неопределенном состоянии.

Еще одна проблема касается отладки цепочек корутин. При возникновении ошибки в середине цепочки сложно понять, на каком именно этапе произошла проблема. Unity не предоставляет подробную информацию о стеке вызовов корутин, что затрудняет поиск bugs.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerator HardToDebugChain()
{
    yield return StartCoroutine(Step1());
    yield return StartCoroutine(Step2()); // Ошибка здесь
    yield return StartCoroutine(Step3()); // Этого мы никогда не увидим
}
 
IEnumerator Step2()
{
    // Сложная логика с потенциальной ошибкой
    yield return new WaitForSeconds(Random.Range(1f, 10f));
    if (Random.Range(0, 10) > 7)
    {
        throw new System.Exception("Случайная ошибка"); // Сложно отследить
    }
}
Многие также неправильно обрабатывают преждевременное завершение цепочек. Если игрок закрывает меню посередине анимации или переходит на другую сцену во время загрузки, цепочка корутин должна корректно завершиться. Но часто разработчики не предусматривают такие сценарии.

Особенности работы со StopCoroutine и утечками памяти



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

Когда вы вызываете StopCoroutine, Unity немедленно удаляет корутину из внутренного планировщика. Но - и это критично - все ресурсы, захваченные корутиной, остаются в памяти. В отличие от обычных методов, где можно использовать using или try-finally, корутины не имеют гарантированного механизма очистки.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
IEnumerator LeakyCoroutine()
{
    // Захватываем ресурсы
    AudioSource audioSource = GetComponent<AudioSource>();
    Texture2D temporaryTexture = new Texture2D(1024, 1024);
    List<GameObject> spawnedObjects = new List<GameObject>();
    
    for (int i = 0; i < 100; i++)
    {
        GameObject obj = Instantiate(prefab);
        spawnedObjects.Add(obj);
        yield return new WaitForSeconds(0.1f);
    }
    
    // Если корутину остановят до этого места, ресурсы не очистятся
    DestroyImmediate(temporaryTexture);
    foreach (var obj in spawnedObjects)
    {
        Destroy(obj);
    }
}
 
public void ForceStop()
{
    StopCoroutine(LeakyCoroutine()); // Утечка памяти гарантирована!
}
Еще хуже дела обстоят с корутинами, которые держат ссылки на временные объекты. Unity не освобождает такие объекты автоматически, поскольку на них все еще есть ссылки из остановленных корутин. Это особенно заметно при работе с UI-элементами, которые создаются и уничтожаются динамически.

Вторая проблема касается вложенных корутин. StopCoroutine останавливает только указанную корутину, но не затрагивает дочерние. В результате дочерние корутины продолжают работать, но теряют связь с родительской логикой.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
IEnumerator ParentCoroutineWithChildren()
{
    Debug.Log("Запускаем дочерние задачи");
    
    // Эти корутины НЕ остановятся при остановке родительской
    StartCoroutine(ChildTask1());
    StartCoroutine(ChildTask2());
    StartCoroutine(ChildTask3());
    
    yield return new WaitForSeconds(10f);
    Debug.Log("Родительская задача завершена");
}
 
Coroutine parentCoroutine;
 
void Start()
{
    parentCoroutine = StartCoroutine(ParentCoroutineWithChildren());
}
 
void StopEverything()
{
    // НЕПРАВИЛЬНО: дочерние задачи продолжат работу
    StopCoroutine(parentCoroutine);
}
Третья ловушка связана с тем, что StopCoroutine принимает либо ссылку на корутину, либо строку с именем метода. Использование строк крайне ненадежно, поскольку Unity ищет корутины с точно таким же именем метода. Если у вас несколько корутин с одинаковым именем, результат непредсказуем.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void ProblematicStop()
{
    // Запускаем несколько корутин с одинаковым именем
    StartCoroutine(SameNamedTask());
    StartCoroutine(SameNamedTask());
    StartCoroutine(SameNamedTask());
    
    // Какую именно корутину остановит этот вызов? Неизвестно!
    StopCoroutine("SameNamedTask");
}
 
IEnumerator SameNamedTask()
{
    while (true)
    {
        yield return new WaitForSeconds(1f);
        Debug.Log($"Задача выполняется на {gameObject.name}");
    }
}
Утечки памяти особенно критичны при работе с событийными системами. Если корутина подписывается на события, а затем прерывается до отписки, ссылки на объект остаются в системе событий. Это приводит к накоплению dead references, которые мешают сборщику мусора освобождать память.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IEnumerator EventSubscriptionLeak()
{
    // Подписываемся на события
    GameEvents.OnPlayerDamaged += HandlePlayerDamage;
    GameEvents.OnItemCollected += HandleItemCollection;
    
    // Длительная обработка
    while (someCondition)
    {
        yield return new WaitForSeconds(0.5f);
        ProcessEventData();
    }
    
    // Если корутину остановят, отписка не произойдет
    GameEvents.OnPlayerDamaged -= HandlePlayerDamage;
    GameEvents.OnItemCollected -= HandleItemCollection;
}
Правильный подход требует создания паттерна управляемых ресурсов внутри корутин. Каждая корутина должна отслеживать свои ресурсы и иметь механизм принудительной очистки.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class ManagedCoroutine : MonoBehaviour
{
    private List<IDisposable> managedResources = new List<IDisposable>();
    private List<GameObject> spawnedObjects = new List<GameObject>();
    private Coroutine activeCoroutine;
    
    IEnumerator SafeCoroutine()
    {
        try
        {
            // Безопасное управление ресурсами
            AudioSource audio = GetComponent<AudioSource>();
            
            for (int i = 0; i < 100; i++)
            {
                if (this == null) yield break; // Проверка на уничтожение
                
                GameObject obj = Instantiate(prefab);
                spawnedObjects.Add(obj);
                
                yield return new WaitForSeconds(0.1f);
            }
        }
        finally
        {
            // Эта секция может не выполниться при StopCoroutine!
            CleanupResources();
        }
    }
    
    public void SafeStop()
    {
        if (activeCoroutine != null)
        {
            StopCoroutine(activeCoroutine);
        }
        
        // Принудительная очистка
        CleanupResources();
    }
    
    void CleanupResources()
    {
        foreach (var resource in managedResources)
        {
            resource?.Dispose();
        }
        managedResources.Clear();
        
        foreach (var obj in spawnedObjects)
        {
            if (obj != null) Destroy(obj);
        }
        spawnedObjects.Clear();
    }
}
Особую осторожность нужно проявлять при работе со StopAllCoroutines(). Этот метод останавливает все корутины на MonoBehaviour, но не предоставляет никакого контроля над процессом очистки. В сложных системах это может привести к катастрофическим утечкам памяти.

На практике я видел проекты, где неправильное использование StopCoroutine приводило к постепенному росту потребления памяти игрой. Каждый уровень оставлял за собой "хвосты" из незавершенных ресурсов, и через несколько часов игры память заканчивалась.

Debugging вложенных корутин и инструмены профилирования



Отладка вложенных корутин в Unity - это отдельный вид искусства, который требует нестандартного подхода. Стандартные инструменты отладки работают плохо с корутинами, а глубоко вложенные структуры превращают поиск проблем в настоящий квест. Основная сложность заключается в том, что Unity Profiler видит корутины как отдельные объекты, но не показывает их иерархические связи. Когда корутина A запускает корутину B, которая создает корутину C, профилировщик отображает их как независимые задачи. Понять, что они связаны между собой, практически невозможно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CoroutineDebugger : MonoBehaviour
{
    private static Dictionary<string, float> coroutineTimings = new Dictionary<string, float>();
    private static Dictionary<string, int> coroutineDepth = new Dictionary<string, int>();
    
    public static IEnumerator TrackedCoroutine(string name, IEnumerator coroutine, int depth = 0)
    {
        float startTime = Time.realtimeSinceStartup;
        coroutineDepth[name] = depth;
        
        Debug.Log($"[{depth}] Staring coroutine: {name}");
        
        yield return coroutine;
        
        float endTime = Time.realtimeSinceStartup;
        float duration = endTime - startTime;
        coroutineTimings[name] = duration;
        
        Debug.Log($"[{depth}] Finished coroutine: {name}, duration: {duration:F3}s");
    }
}
Проблема усугубляется тем, что исключения в корутинах не прерывают выполнение программы, а просто логируются в консоль. При глубокой вложенности найти источник ошибки становится чрезвычайно сложно. Обычный stack trace показывает только место непосредственного возникновения исключения, но не цепочку корутин, которая к нему привела.
Для эффективной отладки приходится создавать собственную систему трассировки. Я использую паттерн "корутина-обертка", которая логирует каждый вход и выход из корутины, включая параметры и результаты.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
IEnumerator DebuggableNestedCoroutines()
{
    yield return TrackedCoroutine("MainFlow", MainWorkflow(), 0);
}
 
IEnumerator MainWorkflow()
{
    yield return TrackedCoroutine("Phase1", Phase1(), 1);
    yield return TrackedCoroutine("Phase2", Phase2(), 1);
    yield return TrackedCoroutine("Phase3", Phase3(), 1);
}
 
IEnumerator Phase2()
{
    yield return TrackedCoroutine("SubPhase2A", SubPhase2A(), 2);
    yield return TrackedCoroutine("SubPhase2B", SubPhase2B(), 2);
}
Unity Profiler показывает время выполнения корутин, но не учитывает время ожидания. Если корутина выполняет yield return new WaitForSeconds(5f), профилировщик засчитает ей только время непосредственного выполнения кода, а не общее время жизни. При анализе производительности это создает ложное представление о реальной нагрузке.
Для детального понимания поведения корутин я создаю собственные инструменты мониторинга. Они отслеживают не только время выполнения, но и количество активных корутин, глубину вложенности и частоту yield-вызовов.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CoroutineMonitor : MonoBehaviour
{
    public static CoroutineMonitor Instance;
    
    private List<CoroutineInfo> activeCoroutines = new List<CoroutineInfo>();
    
    void Awake()
    {
        Instance = this;
    }
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.F1))
        {
            LogCoroutineStats();
        }
    }
    
    void LogCoroutineStats()
    {
        Debug.Log($"Active coroutines: {activeCoroutines.Count}");
        for (int i = 0; i < activeCoroutines.Count; i++)
        {
            var info = activeCoroutines[i];
            float runTime = Time.realtimeSinceStartup - info.startTime;
            Debug.Log($"  [{i}] {info.name} - running for {runTime:F2}s, depth: {info.depth}");
        }
    }
}
 
[System.Serializable]
public class CoroutineInfo
{
    public string name;
    public float startTime;
    public int depth;
    public bool isActive;
}
Memory Profiler Unity тоже не очень помогает с корутинами. Он показывает объекты итераторов, но не связывает их с конкретными корутинами. Особенно это заметно при утечках памяти - видно, что итераторов много, но понять, какие именно корутины их создают, невозможно.

Самый эффективный подход к отладке - это создание визуализатора состояния корутин. Простое окно в Editor, которое показывает дерево активных корутин с их текущим состоянием. Такой инструмент позволяет видеть всю иерархию в реальном времени и быстро находить зависшие или проблемные ветки. Frame Debugger Unity частично помогает при отладке корутин, связанных с рендерингом, но бесполезен для логических корутин. А Console Window засоряется логами от множества корутин, что затрудняет поиск важной информации. Наиболее coварные баги в корутинах связаны с timing issues. Корутина может работать правильно на мощном компьютере разработчика, но зависать на слабом устройстве test пользователя. Разность в производительности приводит к изменению порядка выполнения, что вызывает race conditions между корутинами.

Практические паттерны синхронизации



Синхронизация корутин в реальных проектах требует системного подхода, а не хаотичного размещения yield return по коду. За годы работы я выработал несколько проверенных паттернов, которые помогают создавать надежные и предсказуемые системы корутин.

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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class CoroutineCoordinator : MonoBehaviour
{
private Dictionary<string, Coroutine> managedCoroutines = new Dictionary<string, Coroutine>();
private Dictionary<string, bool> completionFlags = new Dictionary<string, bool>();
 
public IEnumerator ExecuteSequence(string sequenceName, params IEnumerator[] coroutines)
{
    completionFlags[sequenceName] = false;
    
    for (int i = 0; i < coroutines.Length; i++)
    {
        string stepName = $"{sequenceName}_Step{i}";
        managedCoroutines[stepName] = StartCoroutine(coroutines[i]);
        
        yield return managedCoroutines[stepName];
        
        if (this == null) yield break; // Защита от уничтожения
    }
    
    completionFlags[sequenceName] = true;
}
 
public IEnumerator ExecuteParallel(string groupName, params IEnumerator[] coroutines)
{
    var parallelCoroutines = new Coroutine[coroutines.Length];
    
    // Запускаем все корутины одновременно
    for (int i = 0; i < coroutines.Length; i++)
    {
        string taskName = $"{groupName}_Task{i}";
        parallelCoroutines[i] = StartCoroutine(coroutines[i]);
        managedCoroutines[taskName] = parallelCoroutines[i];
    }
    
    // Ждем завершения всех
    foreach (var coroutine in parallelCoroutines)
    {
        yield return coroutine;
    }
    
    completionFlags[groupName] = true;
}
 
public void ForceStopGroup(string groupName)
{
    var keysToRemove = new List<string>();
    
    foreach (var kvp in managedCoroutines)
    {
        if (kvp.Key.StartsWith(groupName))
        {
            if (kvp.Value != null) StopCoroutine(kvp.Value);
            keysToRemove.Add(kvp.Key);
        }
    }
    
    keysToRemove.ForEach(key => managedCoroutines.Remove(key));
    completionFlags[groupName] = false;
}
}
Паттерн "Состояние ожидания" использует флаги и условия для синхронизации корутин без прямых ссылок друг на друга. Это особенно полезно когда корутины находятся на разных GameObject или должны синхронизироваться асинхронно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class CoroutineStateManager : MonoBehaviour
{
private static Dictionary<string, bool> globalStates = new Dictionary<string, bool>();
private static Dictionary<string, object> sharedData = new Dictionary<string, object>();
 
public static void SetState(string stateName, bool value)
{
    globalStates[stateName] = value;
    Debug.Log($"State {stateName} set to {value}");
}
 
public static bool GetState(string stateName)
{
    return globalStates.ContainsKey(stateName) && globalStates[stateName];
}
 
public static void SetData<T>(string dataKey, T value)
{
    sharedData[dataKey] = value;
}
 
public static T GetData<T>(string dataKey)
{
    if (sharedData.ContainsKey(dataKey) && sharedData[dataKey] is T)
    {
        return (T)sharedData[dataKey];
    }
    return default(T);
}
 
public static IEnumerator WaitForState(string stateName, bool expectedValue = true)
{
    while (GetState(stateName) != expectedValue)
    {
        yield return new WaitForSeconds(0.1f); // Проверяем каждые 100ms
    }
}
 
public static IEnumerator WaitForMultipleStates(params string[] stateNames)
{
    bool allStatesReached = false;
    
    while (!allStatesReached)
    {
        allStatesReached = true;
        
        foreach (string stateName in stateNames)
        {
            if (!GetState(stateName))
            {
                allStatesReached = false;
                break;
            }
        }
        
        yield return new WaitForSeconds(0.1f);
    }
}
}
Паттерн "Временное окно" позволяет корутинам синхронизироваться в определенные моменты времени, не дожидаясь завершения друг друга. Это полезно для создания анимаций, которые должны стартовать с задержками, но выполняться параллельно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class TimedCoroutineManager : MonoBehaviour
{
private class TimedExecution
{
    public float executeAt;
    public IEnumerator coroutine;
    public string name;
}
 
private List<TimedExecution> scheduledCoroutines = new List<TimedExecution>();
private float timelineStart;
 
public void ScheduleCoroutine(string name, IEnumerator coroutine, float delay)
{
    scheduledCoroutines.Add(new TimedExecution
    {
        executeAt = timelineStart + delay,
        coroutine = coroutine,
        name = name
    });
}
 
public IEnumerator ExecuteTimeline()
{
    timelineStart = Time.time;
    var executedCoroutines = new List<Coroutine>();
    
    scheduledCoroutines = scheduledCoroutines.OrderBy(x => x.executeAt).ToList();
    
    foreach (var scheduled in scheduledCoroutines)
    {
        // Дождаться нужного времени
        while (Time.time < scheduled.executeAt)
        {
            yield return null;
        }
        
        // Запустить корутину
        Debug.Log($"Executing scheduled coroutine: {scheduled.name}");
        var coroutineRef = StartCoroutine(scheduled.coroutine);
        executedCoroutines.Add(coroutineRef);
    }
    
    // Дождаться завершения всех запущенных корутин
    foreach (var coroutine in executedCoroutines)
    {
        yield return coroutine;
    }
    
    scheduledCoroutines.Clear();
}
}
Паттерн "Приоритетная очередь" решает проблему конкуренции ресурсов между корутинами. Когда несколько корутин пытаются получить доступ к ограниченному ресурсу, очередь определяет порядок выполнения.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class PriorityCoroutineQueue : MonoBehaviour
{
private class QueuedCoroutine
{
    public IEnumerator coroutine;
    public int priority;
    public string name;
    public System.Action<bool> onComplete;
}
 
private List<QueuedCoroutine> queue = new List<QueuedCoroutine>();
private bool isProcessing = false;
 
public void EnqueueCoroutine(string name, IEnumerator coroutine, int priority = 0, System.Action<bool> onComplete = null)
{
    queue.Add(new QueuedCoroutine
    {
        coroutine = coroutine,
        priority = priority,
        name = name,
        onComplete = onComplete
    });
    
    // Сортируем по приоритету (больший приоритет = выше в очереди)
    queue = queue.OrderByDescending(x => x.priority).ToList();
    
    if (!isProcessing)
    {
        StartCoroutine(ProcessQueue());
    }
}
 
IEnumerator ProcessQueue()
{
    isProcessing = true;
    
    while (queue.Count > 0)
    {
        var current = queue[0];
        queue.RemoveAt(0);
        
        Debug.Log($"Processing coroutine: {current.name} (priority: {current.priority})");
        
        bool completed = false;
        try
        {
            yield return StartCoroutine(current.coroutine);
            completed = true;
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Coroutine {current.name} failed: {e.Message}");
        }
        
        current.onComplete?.Invoke(completed);
    }
    
    isProcessing = false;
}
}
Эти паттерны можно комбинировать между собой для создания сложных систем синхронизации. В одном проекте я использовал координатор для управления последовательными фазами загрузки уровня, состояние ожидания для синхронизации между UI и геймплейными корутинами, временное окно для оркестровки вступительных анимаций, и приоритетную очередь для управления доступом к системе сохранения данных.

Использование yield return для ожидания завершения



Конструкция yield return StartCoroutine() кажется интуитивно понятной, но скрывает множество нюансов, которые могут превратить простой код в источник головной боли. Основная проблема заключается в том, что разработчики часто не понимают, что именно возвращает StartCoroutine и как Unity интерпретирует различные типы объектов после yield return. Когда мы пишем yield return StartCoroutine(SomeMethod()), Unity создает объект типа Coroutine и возвращает ссылку на него. Эта ссылка становится "токеном ожидания" - пока корутина не завершится, управление не вернется к следующей строке кода. Но что происходит, если мы попытаемся дождаться завершения корутины другими способами?

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IEnumerator DifferentWaitingApproaches()
{
    // Способ 1: Стандартное ожидание через StartCoroutine
    yield return StartCoroutine(LongRunningTask());
    
    // Способ 2: Прямой yield return IEnumerator (работает в Unity 5.3+)
    yield return LongRunningTask();
    
    // Способ 3: Сохранение ссылки для управления
    Coroutine taskRef = StartCoroutine(LongRunningTask());
    yield return taskRef;
    
    Debug.Log("Все подходы завершены");
}
 
IEnumerator LongRunningTask()
{
    for (int i = 0; i < 5; i++)
    {
        Debug.Log($"Task step {i + 1}");
        yield return new WaitForSeconds(0.5f);
    }
}
Удивительно, но все три подхода ведут себя по-разному в зависимости от версии Unity и контекста выполнения. Второй способ работает только в новых версиях движка и может привести к unexpected behavior в старых проектах. Третий способ дает максимальный контроль, но требует аккуратного управления ссылками. Критическая особенность yield return заключается в том, что он не гарантирует выполнение кода после завершения ожидаемой корутины. Если GameObject, на котором выполняется родительская корутина, уничтожается, то код после yield return никогда не выполнится, даже если дочерняя корутина завершилась успешно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IEnumerator FragileWaiting()
{
    Debug.Log("Запускаем долгую задачу");
    
    yield return StartCoroutine(VeryLongTask());
    
    // Этот код может никогда не выполниться!
    Debug.Log("Задача завершена");
    CleanupResources();
}
 
IEnumerator VeryLongTask()
{
    yield return new WaitForSeconds(10f);
    
    // Задача завершается, но родительская корутина может быть мертвой
    Debug.Log("Долгая задача завершена");
}
Еще одна коварная проблема связана с исключениями в дочерних корутинах. Unity не пробрасывает исключения из дочерних корутин в родительские. Если дочерняя корутина завершается с ошибкой, Unity просто логирует исключение, но возвращает управление родительской корутине, как будто все прошло успешно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
IEnumerator UnreliableErrorHandling()
{
    try
    {
        yield return StartCoroutine(ProblematicTask());
        Debug.Log("Задача 'завершена' успешно"); // Выполнится даже при ошибке
    }
    catch (System.Exception e)
    {
        // Этот блок НИКОГДА не выполнится для ошибок в дочерней корутине
        Debug.LogError($"Поймано исключение: {e.Message}");
    }
}
 
IEnumerator ProblematicTask()
{
    yield return new WaitForSeconds(1f);
    
    throw new System.InvalidOperationException("Что-то пошло не так!");
    // Исключение будет залогировано, но не пробросится в родительскую корутину
}
Для надежного ожидания завершения корутин с обработкой ошибок приходится создавать собственные обертки. Паттерн "Результат корутины" позволяет передавать как данные, так и информацию об ошибках между корутинами.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class CoroutineResult<T>
{
    public T Value { get; set; }
    public bool IsSuccess { get; set; }
    public string ErrorMessage { get; set; }
    public System.Exception Exception { get; set; }
}
 
IEnumerator ReliableWaiting()
{
    var result = new CoroutineResult<string>();
    
    yield return StartCoroutine(SafeTask(result));
    
    if (result.IsSuccess)
    {
        Debug.Log($"Получен результат: {result.Value}");
    }
    else
    {
        Debug.LogError($"Задача завершилась с ошибкой: {result.ErrorMessage}");
    }
}
 
IEnumerator SafeTask(CoroutineResult<string> result)
{
    try
    {
        yield return new WaitForSeconds(2f);
        
        // Симуляция случайной ошибки
        if (UnityEngine.Random.Range(0, 2) == 0)
        {
            throw new System.Exception("Случайная ошибка");
        }
        
        result.Value = "Задача выполнена успешно";
        result.IsSuccess = true;
    }
    catch (System.Exception e)
    {
        result.IsSuccess = false;
        result.ErrorMessage = e.Message;
        result.Exception = e;
    }
}
Таймауты представляют особую сложность при работе с yield return. Unity не предоставляет встроенного механизма для ограничения времени ожидания корутины. Если дочерняя корутина зависла, родительская будет ждать её бесконечно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IEnumerator WaitWithTimeout(IEnumerator targetCoroutine, float timeout)
{
    float startTime = Time.time;
    Coroutine runningCoroutine = StartCoroutine(targetCoroutine);
    
    while (runningCoroutine != null && Time.time - startTime < timeout)
    {
        yield return null;
    }
    
    if (Time.time - startTime >= timeout)
    {
        Debug.LogWarning("Корутина превысила лимит времени выполнения");
        StopCoroutine(runningCoroutine);
    }
}
Проблемы с yield return особенно заметны при работе с множественными корутинами. Когда нужно дождаться завершения нескольких корутин одновременно, простой подход через несколько yield return создает последовательное выполнение, а не параллельное.

Наконец, важно понимать, что yield return работает только в контексте IEnumerator. Попытка использовать его в обычном методе приведет к ошибке компиляции. Это ограничение создает проблемы при интеграции корутин с обычным кодом, особенно при работе с системами, которые ожидают синхронных возвращаемых значений.

Координация множественных корутин через флаги состояния



Флаги состояния представляют собой один из наиболее надежных способов синхронизации корутин, когда прямое ожидание невозможно или нежелательно. В отличие от yield return StartCoroutine(), флаги позволяют корутинам работать независимо, но координировать свои действия через общие точки синхронизации. Основная идея заключается в том, что корутины устанавливают булевы флаги при достижении определенных состояний, а другие корутины проверяют эти флаги перед выполнением своих действий. Такой подход особенно эффективен в играх, где множественные системы должны синхронизироваться без жесткой привязки друг к другу.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class GameStateFlags : MonoBehaviour
{
    private static Dictionary<string, bool> stateFlags = new Dictionary<string, bool>();
    private static Dictionary<string, List<System.Action>> stateCallbacks = new Dictionary<string, List<System.Action>>();
 
    public static void SetFlag(string flagName, bool value)
    {
        bool oldValue = GetFlag(flagName);
        stateFlags[flagName] = value;
 
        if (oldValue != value && value)
        {
            TriggerCallbacks(flagName);
        }
 
        Debug.Log($"Flag {flagName} set to {value}");
    }
 
    public static bool GetFlag(string flagName)
    {
        return stateFlags.ContainsKey(flagName) && stateFlags[flagName];
    }
 
    public static void WaitForFlag(string flagName, System.Action callback)
    {
        if (GetFlag(flagName))
        {
            callback?.Invoke();
            return;
        }
 
        if (!stateCallbacks.ContainsKey(flagName))
        {
            stateCallbacks[flagName] = new List<System.Action>();
        }
 
        stateCallbacks[flagName].Add(callback);
    }
 
    private static void TriggerCallbacks(string flagName)
    {
        if (!stateCallbacks.ContainsKey(flagName)) return;
 
        foreach (var callback in stateCallbacks[flagName])
        {
            callback?.Invoke();
        }
 
        stateCallbacks[flagName].Clear();
    }
}
Практическое применение флагов особенно заметно в системах загрузки уровня. Различные подсистемы - AI, аудио, графика, UI - должны инициализироваться независимо, но игра может начаться только когда все готовы.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
IEnumerator LoadingCoordinator()
{
    Debug.Log("Начинаем загрузку уровня");
 
    // Запускаем независимые загрузчики
    StartCoroutine(AudioLoader());
    StartCoroutine(GraphicsLoader());
    StartCoroutine(AISystemLoader());
    StartCoroutine(UILoader());
 
    // Ждем готовности всех систем
    yield return StartCoroutine(WaitForAllSystems());
 
    GameStateFlags.SetFlag("Level_Ready", true);
    Debug.Log("Уровень полностью загружен");
}
 
IEnumerator AudioLoader()
{
    yield return new WaitForSeconds(UnityEngine.Random.Range(1f, 3f));
    Debug.Log("Аудио система готова");
    GameStateFlags.SetFlag("Audio_Ready", true);
}
 
IEnumerator GraphicsLoader()
{
    yield return new WaitForSeconds(UnityEngine.Random.Range(2f, 4f));
    Debug.Log("Графика готова");
    GameStateFlags.SetFlag("Graphics_Ready", true);
}
 
IEnumerator WaitForAllSystems()
{
    string[] requiredFlags = { "Audio_Ready", "Graphics_Ready", "AI_Ready", "UI_Ready" };
 
    while (true)
    {
        bool allReady = true;
        foreach (string flag in requiredFlags)
        {
            if (!GameStateFlags.GetFlag(flag))
            {
                allReady = false;
                break;
            }
        }
 
        if (allReady) break;
        yield return new WaitForSeconds(0.1f);
    }
}
Флаги состояния решают проблему циклических зависимостей между корутинами. В традиционном подходе корутина A не может ждать корутину B, если B тоже ожидает A. Флаги разрывают эту связь, позволяя корутинам реагировать на состояния, а не на конкретные корутины.
Усложненная версия флагов включает счетчики вместо простых булевых значений. Это полезно когда нужно дождаться завершения определенного количества однотипных задач.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class CounterFlags : MonoBehaviour
{
    private static Dictionary<string, int> counters = new Dictionary<string, int>();
    private static Dictionary<string, int> targets = new Dictionary<string, int>();
 
    public static void SetTargetCount(string counterName, int target)
    {
        targets[counterName] = target;
        if (!counters.ContainsKey(counterName))
        {
            counters[counterName] = 0;
        }
    }
 
    public static void IncrementCounter(string counterName)
    {
        if (!counters.ContainsKey(counterName))
        {
            counters[counterName] = 0;
        }
 
        counters[counterName]++;
        Debug.Log($"Counter {counterName}: {counters[counterName]}/{targets.GetValueOrDefault(counterName, 0)}");
    }
 
    public static bool IsTargetReached(string counterName)
    {
        return counters.GetValueOrDefault(counterName, 0) >= targets.GetValueOrDefault(counterName, 1);
    }
 
    public static IEnumerator WaitForTarget(string counterName)
    {
        while (!IsTargetReached(counterName))
        {
            yield return new WaitForSeconds(0.1f);
        }
    }
}
Этот паттерн отлично работает для систем с переменным количеством участников - например, ожидание готовности всех игроков в мультиплеере или завершения всех вражеских ИИ на уровне.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
IEnumerator EnemySpawner()
{
    int enemyCount = UnityEngine.Random.Range(5, 10);
    CounterFlags.SetTargetCount("Enemies_Defeated", enemyCount);
 
    for (int i = 0; i < enemyCount; i++)
    {
        StartCoroutine(SpawnAndManageEnemy(i));
        yield return new WaitForSeconds(0.5f);
    }
 
    Debug.Log($"Заспавнено {enemyCount} врагов");
    yield return CounterFlags.WaitForTarget("Enemies_Defeated");
    
    Debug.Log("Все враги повержены!");
    GameStateFlags.SetFlag("Wave_Completed", true);
}
 
IEnumerator SpawnAndManageEnemy(int enemyId)
{
    GameObject enemy = Instantiate(enemyPrefab);
    enemy.name = $"Enemy_{enemyId}";
 
    // Симуляция жизни врага
    float lifeTime = UnityEngine.Random.Range(3f, 8f);
    yield return new WaitForSeconds(lifeTime);
 
    Destroy(enemy);
    CounterFlags.IncrementCounter("Enemies_Defeated");
    Debug.Log($"Враг {enemyId} уничтожен");
}
Важное преимущество флагов состояния заключается в их персистентности. В отличие от корутин, которые завершаются и исчезают, флаги остаются доступными до сброса. Это позволяет корутинам, запущенным позже, проверить состояние системы и принять соответствующие решения.

Однако флаги требуют осторожного управления памятью. Накопление неиспользуемых флагов может приводить к утечкам памяти, особенно в играх с длительными сессиями. Разумная практика включает periodic очистку устаревших флагов и группировку флагов по контексту выполнения.

Паттерн корутинного пула для переиспользования экземпляров



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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class CoroutinePool : MonoBehaviour
{
    private class PooledCoroutine
    {
        public bool isActive;
        public IEnumerator currentTask;
        public Coroutine unityCoroutine;
        public System.Action<PooledCoroutine> onComplete;
        public string taskName;
    }
 
    private List<PooledCoroutine> pool = new List<PooledCoroutine>();
    private Queue<PooledCoroutine> availableCoroutines = new Queue<PooledCoroutine>();
    
    [SerializeField] private int poolSize = 20;
 
    void Awake()
    {
        InitializePool();
    }
 
    private void InitializePool()
    {
        for (int i = 0; i < poolSize; i++)
        {
            var pooledCoroutine = new PooledCoroutine();
            pool.Add(pooledCoroutine);
            availableCoroutines.Enqueue(pooledCoroutine);
        }
    }
 
    public bool TryExecuteTask(string taskName, IEnumerator task, System.Action onComplete = null)
    {
        if (availableCoroutines.Count == 0)
        {
            Debug.LogWarning($"Пул корутин исчерпан. Задача {taskName} отклонена");
            return false;
        }
 
        var pooledCoroutine = availableCoroutines.Dequeue();
        pooledCoroutine.isActive = true;
        pooledCoroutine.currentTask = task;
        pooledCoroutine.taskName = taskName;
        pooledCoroutine.onComplete = _ => {
            onComplete?.Invoke();
            ReturnToPool(pooledCoroutine);
        };
 
        pooledCoroutine.unityCoroutine = StartCoroutine(ExecutePooledTask(pooledCoroutine));
        return true;
    }
 
    private IEnumerator ExecutePooledTask(PooledCoroutine pooledCoroutine)
    {
        try
        {
            yield return StartCoroutine(pooledCoroutine.currentTask);
        }
        finally
        {
            pooledCoroutine.onComplete?.Invoke(pooledCoroutine);
        }
    }
 
    private void ReturnToPool(PooledCoroutine pooledCoroutine)
    {
        pooledCoroutine.isActive = false;
        pooledCoroutine.currentTask = null;
        pooledCoroutine.taskName = null;
        pooledCoroutine.unityCoroutine = null;
        pooledCoroutine.onComplete = null;
 
        availableCoroutines.Enqueue(pooledCoroutine);
    }
}
Этот паттерн особенно эффективен для часто повторяющихся задач - анимации UI-элементов, эффекты частиц, периодические проверки состояния. Вместо создания сотен одинаковых корутин для анимации кнопок в меню, можно использовать пул из 5-10 корутин.
Продвинутая версия пула включает приоритизацию задач и автоматическое расширение при нехватке ресурсов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class AdvancedCoroutinePool : MonoBehaviour
{
    private class PriorityTask
    {
        public string name;
        public IEnumerator task;
        public int priority;
        public System.Action onComplete;
        public float submittedAt;
    }
 
    private List<PooledCoroutine> pool = new List<PooledCoroutine>();
    private Queue<PooledCoroutine> availableCoroutines = new Queue<PooledCoroutine>();
    private Queue<PriorityTask> waitingTasks = new Queue<PriorityTask>();
 
    [SerializeField] private int initialPoolSize = 10;
    [SerializeField] private int maxPoolSize = 50;
    [SerializeField] private bool allowDynamicExpansion = true;
 
    public bool ExecuteWithPriority(string taskName, IEnumerator task, int priority = 0, System.Action onComplete = null)
    {
        if (TryGetAvailableCoroutine(out var pooledCoroutine))
        {
            AssignTask(pooledCoroutine, taskName, task, onComplete);
            return true;
        }
 
        if (allowDynamicExpansion && pool.Count < maxPoolSize)
        {
            ExpandPool(1);
            return ExecuteWithPriority(taskName, task, priority, onComplete);
        }
 
        // Добавляем в очередь ожидания
        waitingTasks.Enqueue(new PriorityTask
        {
            name = taskName,
            task = task,
            priority = priority,
            onComplete = onComplete,
            submittedAt = Time.time
        });
 
        Debug.LogWarning($"Задача {taskName} добавлена в очередь ожидания");
        return false;
    }
 
    private bool TryGetAvailableCoroutine(out PooledCoroutine pooledCoroutine)
    {
        if (availableCoroutines.Count > 0)
        {
            pooledCoroutine = availableCoroutines.Dequeue();
            return true;
        }
 
        pooledCoroutine = null;
        return false;
    }
 
    private void ExpandPool(int count)
    {
        for (int i = 0; i < count; i++)
        {
            var newPooledCoroutine = new PooledCoroutine();
            pool.Add(newPooledCoroutine);
            availableCoroutines.Enqueue(newPooledCoroutine);
        }
 
        Debug.Log($"Пул расширен на {count} корутин. Текущий размер: {pool.Count}");
    }
 
    private void ProcessWaitingQueue()
    {
        while (waitingTasks.Count > 0 && availableCoroutines.Count > 0)
        {
            var task = waitingTasks.Dequeue();
            var pooledCoroutine = availableCoroutines.Dequeue();
            AssignTask(pooledCoroutine, task.name, task.task, task.onComplete);
        }
    }
 
    void Update()
    {
        ProcessWaitingQueue();
    }
}
Ключевая особенность пула заключается в том, что он значительно снижает garbage collection pressure. Вместо создания новых объектов каждый раз, система переиспользует существующие. В проектах с интенсивным использованием корутин это может дать прирост производительности до 40%.

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

Обработка исключений во вложенных структурах



Корутины в Unity имеют крайне нестандартное поведение при возникновении исключений, что делает отладку вложенных структур особенно сложной задачей. В отличие от обычного кода C#, где исключения поднимаются по стеку вызовов и могут быть перехвачены try-catch блоками, корутины "съедают" исключения и не передают их родительским корутинам. Когда исключение возникает внутри корутины, Unity автоматически логирует его в консоль, но корутина просто завершается, как будто выполнилась до конца. Родительская корутина, ожидающая завершения через yield return StartCoroutine(), получает управление обратно и продолжает выполнение, не подозревая о произошедшей ошибке.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IEnumerator DeceptiveErrorHandling()
{
try
{
    Debug.Log("Запускаем потенциально проблемную задачу");
    yield return StartCoroutine(FailingCoroutine());
    
    // Этот код выполнится даже если дочерняя корутина упала с ошибкой!
    Debug.Log("Задача 'успешно' завершена");
}
catch (System.Exception e)
{
    // Этот блок НИКОГДА не сработает для исключений из дочерних корутин
    Debug.LogError($"Поймано исключение: {e.Message}");
}
}
 
IEnumerator FailingCoroutine()
{
yield return new WaitForSeconds(1f);
throw new System.ArgumentNullException("Симуляция ошибки");
// Исключение будет залогировано Unity, но не дойдет до родительской корутины
}
Это поведение становится критической проблемой в сложных системах, где цепочки корутин выполняют последовательные операции. Если одно из звеньев завершается с ошибкой, последующие операции продолжат выполняться с некорректными данными или в неопределенном состоянии.

Для корректной обработки ошибок во вложенных корутинах необходимо создавать явные механизмы передачи информации о состоянии. Паттерн "обертка результата" позволяет корутинам сообщать о своем статусе завершения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class CoroutineResult
{
public bool IsSuccess { get; set; } = true;
public string ErrorMessage { get; set; }
public System.Exception Exception { get; set; }
public object Data { get; set; }
}
 
IEnumerator SafeNestedExecution()
{
var result = new CoroutineResult();
 
yield return StartCoroutine(SafeOperation(result));
 
if (result.IsSuccess)
{
    Debug.Log($"Операция завершена успешно: {result.Data}");
}
else
{
    Debug.LogError($"Операция завершилась с ошибкой: {result.ErrorMessage}");
    HandleOperationFailure(result.Exception);
}
}
 
IEnumerator SafeOperation(CoroutineResult result)
{
try
{
    yield return new WaitForSeconds(1f);
    
    // Симуляция потенциальной ошибки
    if (UnityEngine.Random.Range(0, 2) == 0)
    {
        throw new System.InvalidOperationException("Случайная ошибка");
    }
    
    result.Data = "Успешный результат";
}
catch (System.Exception e)
{
    result.IsSuccess = false;
    result.ErrorMessage = e.Message;
    result.Exception = e;
}
}
Более сложная система включает типизированные результаты и cascading обработку ошибок:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IEnumerator ComplexNestingWithErrorHandling()
{
var phase1Result = new CoroutineResult();
yield return StartCoroutine(Phase1(phase1Result));
 
if (!phase1Result.IsSuccess)
{
    Debug.LogError("Phase1 failed, aborting sequence");
    yield return StartCoroutine(Cleanup());
    yield break;
}
 
var phase2Result = new CoroutineResult();
yield return StartCoroutine(Phase2(phase2Result, phase1Result.Data));
 
if (!phase2Result.IsSuccess)
{
    Debug.LogError("Phase2 failed, rolling back Phase1");
    yield return StartCoroutine(RollbackPhase1(phase1Result.Data));
    yield break;
}
 
Debug.Log("Вся последовательность выполнена успешно");
}
Особую сложность представляет обработка исключений в параллельных корутинах. Когда несколько корутин выполняются одновременно, failure одной из них должен правильно обрабатываться без влияния на другие:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
IEnumerator ParallelExecutionWithErrorHandling()
{
var results = new CoroutineResult[3];
for (int i = 0; i < results.Length; i++)
{
    results[i] = new CoroutineResult();
}
 
// Запускаем параллельные задачи
var coroutines = new Coroutine[]
{
    StartCoroutine(ParallelTask(results[0], "Task1")),
    StartCoroutine(ParallelTask(results[1], "Task2")), 
    StartCoroutine(ParallelTask(results[2], "Task3"))
};
 
// Ждем завершения всех
foreach (var coroutine in coroutines)
{
    yield return coroutine;
}
 
// Анализируем результаты
int successCount = 0;
for (int i = 0; i < results.Length; i++)
{
    if (results[i].IsSuccess)
    {
        successCount++;
    }
    else
    {
        Debug.LogError($"Task{i+1} failed: {results[i].ErrorMessage}");
    }
}
 
Debug.Log($"Завершилось успешно задач: {successCount}/{results.Length}");
}
Критически важно также учитывать memory leaks при обработке ошибок в корутинах. Если корутина создала ресурсы, но завершилась с исключением, эти ресурсы должны быть правильно освобождены даже при ошибке.

Таймауты и принудительное завершение зависших корутин



Зависшие корутины - это настоящий кошмар для разработчика. Они невидимо расходуют ресурсы, блокируют логику игры и создают впечатление, что приложение "подвисло". Особенно коварно это проявляется в корутинах, ожидающих внешние события - сетевые запросы, пользовательский ввод или завершение анимаций, которые могут никогда не произойти.

Unity не предоставляет встроенных механизмов для автоматического завершения корутин по таймауту. Если корутина ждет событие, которое никогда не случится, она будет висеть до уничтожения GameObject или остановки всего приложения. Это создает особые сложности при работе с нестабильными API или пользовательским вводом.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class TimeoutManager : MonoBehaviour
{
public static IEnumerator WithTimeout(IEnumerator coroutine, float timeoutSeconds, System.Action onTimeout = null)
{
float startTime = Time.time;
Coroutine runningCoroutine = Instance.StartCoroutine(coroutine);
bool completed = false;
 
// Корутина-наблюдатель для отслеживания завершения
Instance.StartCoroutine(TrackCompletion(() => completed = true));
 
while (!completed && Time.time - startTime < timeoutSeconds)
{
    yield return null;
}
 
if (!completed)
{
    Debug.LogWarning($"Корутина превысила таймаут {timeoutSeconds}с");
    Instance.StopCoroutine(runningCoroutine);
    onTimeout?.Invoke();
}
 
IEnumerator TrackCompletion(System.Action onComplete)
{
    yield return runningCoroutine;
    onComplete?.Invoke();
}
}
 
public static TimeoutManager Instance { get; private set; }
 
void Awake()
{
Instance = this;
}
}
Более сложная система таймаутов включает возможность продления времени ожидания и graceful shutdown. Иногда корутина почти завершилась, но нуждается в дополнительном времени:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class AdvancedTimeoutCoroutine : MonoBehaviour
{
private class TimeoutContext
{
    public float originalTimeout;
    public float currentTimeout;
    public bool canExtend;
    public int extensionsUsed;
    public int maxExtensions;
    public System.Action<string> onTimeout;
    public System.Action onSuccess;
}
 
public static IEnumerator ExecuteWithDynamicTimeout(string taskName, IEnumerator task, float baseTimeout, int maxExtensions = 2)
{
var context = new TimeoutContext
{
    originalTimeout = baseTimeout,
    currentTimeout = baseTimeout,
    canExtend = maxExtensions > 0,
    maxExtensions = maxExtensions
};
 
float startTime = Time.time;
bool taskCompleted = false;
Coroutine taskCoroutine = Instance.StartCoroutine(WrapTask(task, () => taskCompleted = true));
 
while (!taskCompleted)
{
    float elapsed = Time.time - startTime;
    
    if (elapsed >= context.currentTimeout)
    {
        if (context.canExtend && context.extensionsUsed < context.maxExtensions)
        {
            Debug.LogWarning($"Задача {taskName} превысила таймаут, продлеваем на {baseTimeout}с");
            context.currentTimeout += baseTimeout;
            context.extensionsUsed++;
        }
        else
        {
            Debug.LogError($"Задача {taskName} окончательно превысила таймаут");
            Instance.StopCoroutine(taskCoroutine);
            yield break;
        }
    }
    
    yield return null;
}
 
Debug.Log($"Задача {taskName} завершена за {Time.time - startTime:F2}с");
}
 
private static IEnumerator WrapTask(IEnumerator task, System.Action onComplete)
{
yield return task;
onComplete?.Invoke();
}
}
Принудительное завершение корутин требует особой осторожности при управлении ресурсами. StopCoroutine не выполняет cleanup-код, поэтому зависшие корутины могут оставлять за собой "мусор" в виде открытых файлов, сетевых соединений или неосвобожденной памяти.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
IEnumerator ResourceAwareOperation()
{
List<System.IDisposable> resources = new List<System.IDisposable>();
bool operationCompleted = false;
 
try
{
    // Регистрируем ресурсы для автоматической очистки
    var networkStream = OpenNetworkConnection();
    resources.Add(networkStream);
    
    var tempFile = CreateTemporaryFile();
    resources.Add(tempFile);
    
    // Основная работа с возможным зависанием
    yield return StartCoroutine(UnreliableNetworkOperation(networkStream));
    yield return StartCoroutine(ProcessTempFile(tempFile));
    
    operationCompleted = true;
}
finally
{
    // Этот блок может НЕ выполниться при принудительной остановке
    if (!operationCompleted)
    {
        Debug.LogWarning("Операция была прервана, но finally может не сработать");
    }
    
    CleanupResources(resources);
}
}
 
void CleanupResources(List<System.IDisposable> resources)
{
foreach (var resource in resources)
{
    try
    {
        resource?.Dispose();
    }
    catch (System.Exception e)
    {
        Debug.LogError($"Ошибка при освобождении ресурса: {e.Message}");
    }
}
}
Практический approach к управлению таймаутами включает создание системы приоритетов. Критически важные корутины получают больше времени на выполнение, а фоновые задачи завершаются быстрее при превышении лимитов.

Особенно важны таймауты при работе с пользовательским интерфейсом. Если анимация меню зависла, пользователь не может продолжить игру. Грамотно настроенные timeout'ы позволяют системе восстановиться и продолжить работу даже при сбоях в отдельных компонентах.

Продвинутые техники управления



Управление сложными архитектурами корутин требует выхода за рамки стандартных инструментов Unity. Я сталкивался с проектами, где одновременно выполнялось до 500 корутин различного типа - от UI-анимаций до сетевых запросов. Стандартные подходы в таких условиях просто не работают. Ключевая проблема заключается в отсутствии централизованного контроля. Unity предоставляет StartCoroutine и StopCoroutine, но если у вас есть десятки объектов, каждый из которых управляет своими корутинами, система быстро становится неуправляемой. Корутины могут конфликтовать между собой, дублировать работу или зависать в неизвестном состоянии.
Решение строится вокруг создания dedicated менеджера корутин, который централизует все операции и предоставляет расширенные возможности управления:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
public class AdvancedCoroutineManager : MonoBehaviour
{
private static AdvancedCoroutineManager instance;
public static AdvancedCoroutineManager Instance => instance;
 
private Dictionary<string, ManagedCoroutineGroup> coroutineGroups = new Dictionary<string, ManagedCoroutineGroup>();
private List<ManagedCoroutine> allCoroutines = new List<ManagedCoroutine>();
 
private class ManagedCoroutine
{
public string id;
public string groupName;
public Coroutine unityCoroutine;
public float startTime;
public float timeout;
public bool hasTimeout;
public System.Action<bool> onComplete;
public CoroutineStatus status;
}
 
private class ManagedCoroutineGroup
{
public string name;
public List<string> coroutineIds;
public int maxConcurrent;
public Queue<System.Func<IEnumerator>> pendingTasks;
public bool isPaused;
}
 
public enum CoroutineStatus
{
Running,
Paused,
Completed,
Failed,
TimedOut
}
 
void Awake()
{
instance = this;
StartCoroutine(MonitorCoroutines());
}
 
public string ExecuteManagedCoroutine(string groupName, IEnumerator coroutine, float timeout = 0f, System.Action<bool> onComplete = null)
{
string coroutineId = System.Guid.NewGuid().ToString();
 
if (!coroutineGroups.ContainsKey(groupName))
{
CreateGroup(groupName);
}
 
var group = coroutineGroups[groupName];
 
if (group.isPaused || (group.maxConcurrent > 0 && GetActiveCoroutinesInGroup(groupName) >= group.maxConcurrent))
{
group.pendingTasks.Enqueue(() => coroutine);
return coroutineId;
}
 
var managedCoroutine = new ManagedCoroutine
{
id = coroutineId,
groupName = groupName,
startTime = Time.time,
timeout = timeout,
hasTimeout = timeout > 0f,
onComplete = onComplete,
status = CoroutineStatus.Running
};
 
managedCoroutine.unityCoroutine = StartCoroutine(ExecuteWithManagement(managedCoroutine, coroutine));
allCoroutines.Add(managedCoroutine);
group.coroutineIds.Add(coroutineId);
 
return coroutineId;
}
 
public void CreateGroup(string groupName, int maxConcurrent = 0)
{
if (!coroutineGroups.ContainsKey(groupName))
{
coroutineGroups[groupName] = new ManagedCoroutineGroup
{
    name = groupName,
    coroutineIds = new List<string>(),
    maxConcurrent = maxConcurrent,
    pendingTasks = new Queue<System.Func<IEnumerator>>(),
    isPaused = false
};
}
}
 
public void PauseGroup(string groupName)
{
if (coroutineGroups.ContainsKey(groupName))
{
coroutineGroups[groupName].isPaused = true;
 
foreach (string coroutineId in coroutineGroups[groupName].coroutineIds)
{
    var coroutine = allCoroutines.Find(c => c.id == coroutineId);
    if (coroutine?.status == CoroutineStatus.Running)
    {
        coroutine.status = CoroutineStatus.Paused;
        StopCoroutine(coroutine.unityCoroutine);
    }
}
}
}
 
private IEnumerator ExecuteWithManagement(ManagedCoroutine managedCoroutine, IEnumerator coroutine)
{
bool completed = false;
 
try
{
yield return StartCoroutine(coroutine);
completed = true;
managedCoroutine.status = CoroutineStatus.Completed;
}
catch (System.Exception e)
{
Debug.LogError($"Корутина {managedCoroutine.id} завершилась с ошибкой: {e.Message}");
managedCoroutine.status = CoroutineStatus.Failed;
}
finally
{
managedCoroutine.onComplete?.Invoke(completed);
CleanupCoroutine(managedCoroutine.id);
ProcessPendingTasks(managedCoroutine.groupName);
}
}
 
private IEnumerator MonitorCoroutines()
{
while (true)
{
yield return new WaitForSeconds(1f);
 
for (int i = allCoroutines.Count - 1; i >= 0; i--)
{
    var coroutine = allCoroutines[i];
    
    if (coroutine.hasTimeout && coroutine.status == CoroutineStatus.Running)
    {
        float elapsed = Time.time - coroutine.startTime;
        if (elapsed >= coroutine.timeout)
        {
            Debug.LogWarning($"Корутина {coroutine.id} превысила таймаут {coroutine.timeout}с");
            coroutine.status = CoroutineStatus.TimedOut;
            StopCoroutine(coroutine.unityCoroutine);
            coroutine.onComplete?.Invoke(false);
            CleanupCoroutine(coroutine.id);
        }
    }
}
}
}
 
private int GetActiveCoroutinesInGroup(string groupName)
{
if (!coroutineGroups.ContainsKey(groupName)) return 0;
 
return coroutineGroups[groupName].coroutineIds.Count(id => 
    allCoroutines.Any(c => c.id == id && 
        (c.status == CoroutineStatus.Running || c.status == CoroutineStatus.Paused)));
}
 
private void CleanupCoroutine(string coroutineId)
{
allCoroutines.RemoveAll(c => c.id == coroutineId);
 
foreach (var group in coroutineGroups.Values)
{
group.coroutineIds.RemoveAll(id => id == coroutineId);
}
}
 
private void ProcessPendingTasks(string groupName)
{
if (!coroutineGroups.ContainsKey(groupName)) return;
 
var group = coroutineGroups[groupName];
 
while (group.pendingTasks.Count > 0 && !group.isPaused && 
       GetActiveCoroutinesInGroup(groupName) < group.maxConcurrent)
{
var taskFactory = group.pendingTasks.Dequeue();
ExecuteManagedCoroutine(groupName, taskFactory.Invoke());
}
}
}
Этот approach решает несколько критических проблем одновременно. Во-первых, он предоставляет централизованный контроль над всеми корутинами в приложении. Во-вторых, автоматически обрабатывает таймауты и cleanup. В-третьих, позволяет ограничивать количество одновременно выполняющихся корутин в группе, что критично для производительности.

Группировка корутин особенно полезна в играх с четко разделенными системами. Можно создать группу "UI_Animations" с лимитом в 10 одновременных корутин, группу "Network_Requests" с лимитом в 3, и группу "Background_Tasks" без ограничений. Это предотвращает ситуации, когда одна система monopolizes все ресурсы.

Создание менеджера корутин для сложных сценариев



Разработка собственного менеджера корутин становится необходимостью в проектах, где стандартные инструменты Unity показывают свою ограниченность. Я столкнулся с этой потребностью при работе над мобильной RPG, где одновременно выполнялись корутины для боевой системы, анимации интерфейса, сетевой синхронизации и фоновой загрузки ресурсов. Основная архитектурная идея менеджера заключается в создании слоя абстракции над стандартными корутинами Unity. Вместо прямого использования StartCoroutine, все операции проходят через централизованную систему, которая обеспечивает контроль, мониторинг и координацию.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
public class ScenarioCoroutineManager : MonoBehaviour
{
private static ScenarioCoroutineManager _instance;
public static ScenarioCoroutineManager Instance
{
    get
    {
        if (_instance == null)
        {
            var go = new GameObject("ScenarioCoroutineManager");
            _instance = go.AddComponent<ScenarioCoroutineManager>();
            DontDestroyOnLoad(go);
        }
        return _instance;
    }
}
 
private Dictionary<string, ScenarioContext> scenarios = new Dictionary<string, ScenarioContext>();
private List<CoroutineNode> activeNodes = new List<CoroutineNode>();
 
private class ScenarioContext
{
    public string name;
    public Dictionary<string, object> sharedData;
    public List<string> nodeIds;
    public ScenarioStatus status;
    public float startTime;
    public System.Action<ScenarioContext> onComplete;
    public int priority;
}
 
private class CoroutineNode
{
    public string id;
    public string scenarioName;
    public string nodeName;
    public Coroutine coroutine;
    public NodeStatus status;
    public List<string> dependencies;
    public List<string> dependents;
    public System.Func<ScenarioContext, IEnumerator> task;
    public float timeout;
    public int retryCount;
    public int maxRetries;
}
 
public enum ScenarioStatus { Pending, Running, Completed, Failed, Cancelled }
public enum NodeStatus { Waiting, Running, Completed, Failed, Retrying }
 
public string CreateScenario(string scenarioName, int priority = 0)
{
    var context = new ScenarioContext
    {
        name = scenarioName,
        sharedData = new Dictionary<string, object>(),
        nodeIds = new List<string>(),
        status = ScenarioStatus.Pending,
        startTime = Time.time,
        priority = priority
    };
 
    scenarios[scenarioName] = context;
    return scenarioName;
}
 
public string AddNode(string scenarioName, string nodeName, System.Func<ScenarioContext, IEnumerator> task,
                      string[] dependencies = null, float timeout = 0f, int maxRetries = 0)
{
    if (!scenarios.ContainsKey(scenarioName))
        throw new System.ArgumentException($"Сценарий {scenarioName} не найден");
 
    string nodeId = $"{scenarioName}_{nodeName}_{System.Guid.NewGuid().ToString("N")[..8]}";
 
    var node = new CoroutineNode
    {
        id = nodeId,
        scenarioName = scenarioName,
        nodeName = nodeName,
        status = NodeStatus.Waiting,
        dependencies = dependencies?.ToList() ?? new List<string>(),
        dependents = new List<string>(),
        task = task,
        timeout = timeout,
        maxRetries = maxRetries
    };
 
    activeNodes.Add(node);
    scenarios[scenarioName].nodeIds.Add(nodeId);
 
    // Обновляем связи зависимостей
    foreach (string dependency in node.dependencies)
    {
        var depNode = activeNodes.Find(n => n.nodeName == dependency && n.scenarioName == scenarioName);
        if (depNode != null)
        {
            depNode.dependents.Add(nodeId);
        }
    }
 
    return nodeId;
}
 
public void ExecuteScenario(string scenarioName, System.Action<ScenarioContext> onComplete = null)
{
    if (!scenarios.ContainsKey(scenarioName))
        return;
 
    var context = scenarios[scenarioName];
    context.onComplete = onComplete;
    context.status = ScenarioStatus.Running;
 
    StartCoroutine(RunScenario(context));
}
 
private IEnumerator RunScenario(ScenarioContext context)
{
    var scenarioNodes = activeNodes.Where(n => n.scenarioName == context.name).ToList();
 
    while (scenarioNodes.Any(n => n.status == NodeStatus.Waiting || n.status == NodeStatus.Running || n.status == NodeStatus.Retrying))
    {
        // Запускаем ноды, готовые к выполнению
        var readyNodes = scenarioNodes.Where(n => n.status == NodeStatus.Waiting && 
                                                   AreAllDependenciesSatisfied(n, scenarioNodes)).ToList();
 
        foreach (var node in readyNodes)
        {
            node.status = NodeStatus.Running;
            node.coroutine = StartCoroutine(ExecuteNode(node, context));
        }
 
        yield return new WaitForSeconds(0.1f);
 
        // Проверяем статус завершения
        bool allCompleted = scenarioNodes.All(n => n.status == NodeStatus.Completed);
        bool anyFailed = scenarioNodes.Any(n => n.status == NodeStatus.Failed);
 
        if (allCompleted)
        {
            context.status = ScenarioStatus.Completed;
            break;
        }
 
        if (anyFailed && !scenarioNodes.Any(n => n.status == NodeStatus.Retrying))
        {
            context.status = ScenarioStatus.Failed;
            break;
        }
    }
 
    context.onComplete?.Invoke(context);
    CleanupScenario(context.name);
}
 
private bool AreAllDependenciesSatisfied(CoroutineNode node, List<CoroutineNode> scenarioNodes)
{
    return node.dependencies.All(dep => 
        scenarioNodes.Any(n => n.nodeName == dep && n.status == NodeStatus.Completed));
}
 
private IEnumerator ExecuteNode(CoroutineNode node, ScenarioContext context)
{
    float startTime = Time.time;
    bool nodeCompleted = false;
 
    try
    {
        // Выполняем задачу ноды с учетом таймаута
        if (node.timeout > 0)
        {
            yield return StartCoroutine(ExecuteWithTimeout(node.task(context), node.timeout, 
                () => nodeCompleted = true));
        }
        else
        {
            yield return StartCoroutine(node.task(context));
            nodeCompleted = true;
        }
 
        if (nodeCompleted)
        {
            node.status = NodeStatus.Completed;
            Debug.Log($"Нода {node.nodeName} в сценарии {context.name} завершена успешно");
        }
        else
        {
            throw new System.TimeoutException($"Нода {node.nodeName} превысила таймаут {node.timeout}с");
        }
    }
    catch (System.Exception e)
    {
        Debug.LogError($"Ошибка в ноде {node.nodeName}: {e.Message}");
 
        if (node.retryCount < node.maxRetries)
        {
            node.retryCount++;
            node.status = NodeStatus.Retrying;
            Debug.Log($"Повторная попытка {node.retryCount}/{node.maxRetries} для ноды {node.nodeName}");
            
            yield return new WaitForSeconds(Mathf.Pow(2, node.retryCount)); // Экспоненциальная задержка
            node.status = NodeStatus.Waiting;
        }
        else
        {
            node.status = NodeStatus.Failed;
        }
    }
}
 
private IEnumerator ExecuteWithTimeout(IEnumerator task, float timeout, System.Action onComplete)
{
    float startTime = Time.time;
    Coroutine taskCoroutine = StartCoroutine(task);
    bool completed = false;
 
    StartCoroutine(TrackCompletion());
 
    while (!completed && Time.time - startTime < timeout)
    {
        yield return null;
    }
 
    if (!completed)
    {
        StopCoroutine(taskCoroutine);
    }
 
    IEnumerator TrackCompletion()
    {
        yield return taskCoroutine;
        completed = true;
        onComplete?.Invoke();
    }
}
 
public void SetSharedData<T>(string scenarioName, string key, T value)
{
    if (scenarios.ContainsKey(scenarioName))
    {
        scenarios[scenarioName].sharedData[key] = value;
    }
}
 
public T GetSharedData<T>(string scenarioName, string key, T defaultValue = default(T))
{
    if (scenarios.ContainsKey(scenarioName) && 
        scenarios[scenarioName].sharedData.ContainsKey(key) &&
        scenarios[scenarioName].sharedData[key] is T)
    {
        return (T)scenarios[scenarioName].sharedData[key];
    }
    return defaultValue;
}
 
private void CleanupScenario(string scenarioName)
{
    if (scenarios.ContainsKey(scenarioName))
    {
        var context = scenarios[scenarioName];
        activeNodes.RemoveAll(n => context.nodeIds.Contains(n.id));
        scenarios.Remove(scenarioName);
    }
}
}
Этот менеджер предоставляет серьезные возможности для управления сложными сценариями. Основное преимущество заключается в декларативном описании зависимостей между задачами. Вместо ручного управления порядком выполнения корутин, вы описываете, какие задачи от каких зависят, а система сама определяет оптимальный порядок запуска.

Система retry с экспоненциальным backoff решает проблему нестабильных операций - сетевых запросов, файлового доступа, внешних API. Shared data между нодами позволяет передавать результаты вычислений без создания сложных цепочек параметров. Таймауты на уровне отдельных нод предотвращают зависание всего сценария из-за одной проблемной задачи.

Интеграция с системой событий Unity для реактивного программирования



Реактивное программирование с корутинами открывает совершенно новый уровень архитектурных возможностей. Вместо жестких связей между компонентами через прямые вызовы методов, система событий позволяет создавать слабосвязанную архитектуру, где корутины реагируют на события асинхронно и независимо.

Основная идея заключается в том, что корутины подписываются на определенные события и активируются только при их возникновении. Это кардинально отличается от традиционного подхода, где корутины выполняются по таймеру или в ответ на direct calls.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class ReactiveCoroutineSystem : MonoBehaviour
{
private static ReactiveCoroutineSystem _instance;
public static ReactiveCoroutineSystem Instance => _instance;
 
private Dictionary<string, List<System.Func<object, IEnumerator>>> eventCoroutines = new Dictionary<string, List<System.Func<object, IEnumerator>>>();
private Dictionary<string, Queue<object>> eventQueue = new Dictionary<string, Queue<object>>();
private Dictionary<string, bool> isProcessingEvent = new Dictionary<string, bool>();
 
void Awake()
{
_instance = this;
StartCoroutine(ProcessEventQueue());
}
 
public void SubscribeCoroutine(string eventName, System.Func<object, IEnumerator> coroutineFactory)
{
if (!eventCoroutines.ContainsKey(eventName))
{
    eventCoroutines[eventName] = new List<System.Func<object, IEnumerator>>();
    eventQueue[eventName] = new Queue<object>();
    isProcessingEvent[eventName] = false;
}
 
eventCoroutines[eventName].Add(coroutineFactory);
Debug.Log($"Корутина подписана на событие {eventName}");
}
 
public void PublishEvent(string eventName, object eventData = null)
{
if (!eventQueue.ContainsKey(eventName)) return;
 
eventQueue[eventName].Enqueue(eventData ?? new object());
Debug.Log($"Событие {eventName} добавлено в очередь");
}
 
private IEnumerator ProcessEventQueue()
{
while (true)
{
    foreach (var eventType in eventQueue.Keys.ToList())
    {
        if (eventQueue[eventType].Count > 0 && !isProcessingEvent[eventType])
        {
            yield return StartCoroutine(ProcessSingleEvent(eventType));
        }
    }
    
    yield return new WaitForSeconds(0.05f); // Проверяем события 20 раз в секунду
}
}
 
private IEnumerator ProcessSingleEvent(string eventName)
{
isProcessingEvent[eventName] = true;
 
while (eventQueue[eventName].Count > 0)
{
    var eventData = eventQueue[eventName].Dequeue();
    
    if (eventCoroutines.ContainsKey(eventName))
    {
        var coroutines = new List<Coroutine>();
        
        // Запускаем все подписанные корутины параллельно
        foreach (var coroutineFactory in eventCoroutines[eventName])
        {
            var coroutine = StartCoroutine(SafeExecuteReactiveCoroutine(coroutineFactory, eventData, eventName));
            coroutines.Add(coroutine);
        }
        
        // Ждем завершения всех корутин для этого события
        foreach (var coroutine in coroutines)
        {
            yield return coroutine;
        }
    }
}
 
isProcessingEvent[eventName] = false;
}
 
private IEnumerator SafeExecuteReactiveCoroutine(System.Func<object, IEnumerator> coroutineFactory, object eventData, string eventName)
{
try
{
    yield return StartCoroutine(coroutineFactory(eventData));
}
catch (System.Exception e)
{
    Debug.LogError($"Ошибка в реактивной корутине для события {eventName}: {e.Message}");
}
}
}
Практическое применение этой системы радикально упрощает управление игровыми состояниями. Вместо того чтобы manually координировать анимации, звуки и логику при изменении игрового состояния, каждая система подписывается на соответствующее событие и реагирует независимо.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class GameplayEventHandlers : MonoBehaviour
{
void Start()
{
// Подписываем корутины на игровые события
ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Player_LevelUp", HandleLevelUp);
ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Player_LevelUp", PlayLevelUpEffects);
ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Enemy_Defeated", HandleEnemyDefeat);
ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Item_Collected", ProcessItemCollection);
}
 
private IEnumerator HandleLevelUp(object eventData)
{
var levelData = eventData as PlayerLevelData;
if (levelData == null) yield break;
 
Debug.Log($"Игрок достиг уровня {levelData.newLevel}");
 
// Обновляем статистики
yield return StartCoroutine(UpdatePlayerStats(levelData));
 
// Сохраняем прогресс
yield return StartCoroutine(SavePlayerProgress(levelData));
 
// Уведомляем другие системы
ReactiveCoroutineSystem.Instance.PublishEvent("Stats_Updated", levelData);
}
 
private IEnumerator PlayLevelUpEffects(object eventData)
{
var levelData = eventData as PlayerLevelData;
 
// Параллельно запускаем эффекты
StartCoroutine(PlayLevelUpAnimation());
StartCoroutine(PlayLevelUpSound());
yield return StartCoroutine(ShowLevelUpUI(levelData?.newLevel ?? 1));
}
 
private IEnumerator HandleEnemyDefeat(object eventData)
{
var enemyData = eventData as EnemyDefeatData;
if (enemyData == null) yield break;
 
// Обрабатываем награды
yield return StartCoroutine(CalculateRewards(enemyData));
 
// Проверяем условия завершения уровня
if (AreAllEnemiesDefeated())
{
    ReactiveCoroutineSystem.Instance.PublishEvent("Level_Completed", null);
}
}
}
 
[System.Serializable]
public class PlayerLevelData
{
public int oldLevel;
public int newLevel;
public int experienceGained;
public Dictionary<string, int> statBonuses;
}
 
[System.Serializable]  
public class EnemyDefeatData
{
public string enemyType;
public int experienceReward;
public List<string> itemDrops;
public Vector3 defeatPosition;
}
Реактивная архитектура особенно мощно проявляет себя в системах с временными задержками. События могут происходить в любое время, а корутины обрабатывают их асинхронно без блокировки основного потока выполнения. Это критично важно для мобильных игр, где responsiveness интрефейса не должна страдать от сложных фоновых вычислений.
Система также support фильтрацию событий и conditional subscriptions. Корутина может подписаться на событие, но активироваться только при выполнении определенных условий:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void SubscribeConditionalCoroutine(string eventName, System.Func<object, IEnumerator> coroutineFactory, System.Func<object, bool> condition)
{
System.Func<object, IEnumerator> conditionalFactory = (eventData) =>
{
    if (condition(eventData))
    {
        return coroutineFactory(eventData);
    }
    else
    {
        // Возвращаем пустую корутину
        return EmptyCoroutine();
    }
};
 
SubscribeCoroutine(eventName, conditionalFactory);
}
 
private IEnumerator EmptyCoroutine()
{
yield break;
}
Интеграция событийной системы с корутинами создает архитектуру, которая естественно масштабируется. Добавление новой функциональности не требует modification существующего кода - достаточно создать новые обработчики событий. Это serious упрощает тестирование и debug сложных игровых систем.

Интеграция с async/await паттернами



Современное C# развитие активно движется в сторону async/await паттернов, которые обеспечивают более чистый и читаемый код для асинхронных операций. Unity корутины, хотя и решают схожие задачи, используют совершенно другой подход. Интеграция этих двух парадигм открывает новые возможности, но требует понимания их фундаментальных различий.

Основная проблема заключается в том, что корутины Unity работают в контексте главного потока и привязаны к MonoBehaviour, в то время как async/await могут выполняться в произвольных потоках. Прямое смешивание этих подходов часто приводит к race conditions и неожиданному поведению.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class AsyncCoroutineBridge : MonoBehaviour
{
    private static AsyncCoroutineBridge _instance;
    public static AsyncCoroutineBridge Instance
    {
        get
        {
            if (_instance == null)
            {
                var go = new GameObject("AsyncCoroutineBridge");
                _instance = go.AddComponent<AsyncCoroutineBridge>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }
 
    public async Task<T> RunCoroutineAsync<T>(System.Func<IEnumerator> coroutineFactory) where T : class
    {
        var taskCompletionSource = new TaskCompletionSource<T>();
 
        StartCoroutine(ExecuteCoroutineWithCallback(coroutineFactory(), result =>
        {
            if (result is T typedResult)
            {
                taskCompletionSource.SetResult(typedResult);
            }
            else
            {
                taskCompletionSource.SetResult(null);
            }
        }));
 
        return await taskCompletionSource.Task;
    }
 
    private IEnumerator ExecuteCoroutineWithCallback(IEnumerator coroutine, System.Action<object> onComplete)
    {
        object result = null;
 
        try
        {
            yield return StartCoroutine(coroutine);
            
            // Если корутина возвращает результат через специальный wrapper
            if (coroutine is ICoroutineWithResult withResult)
            {
                result = withResult.GetResult();
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Ошибка в корутине: {e.Message}");
        }
        finally
        {
            onComplete?.Invoke(result);
        }
    }
 
    public IEnumerator AwaitTask(Task task)
    {
        while (!task.IsCompleted)
        {
            yield return null;
        }
 
        if (task.IsFaulted)
        {
            Debug.LogError($"Task завершился с ошибкой: {task.Exception?.GetBaseException().Message}");
        }
    }
 
    public IEnumerator AwaitTask<T>(Task<T> task, System.Action<T> onResult)
    {
        while (!task.IsCompleted)
        {
            yield return null;
        }
 
        if (task.IsCompletedSuccessfully)
        {
            onResult?.Invoke(task.Result);
        }
        else if (task.IsFaulted)
        {
            Debug.LogError($"Task<T> завершился с ошибкой: {task.Exception?.GetBaseException().Message}");
        }
    }
}
Практическое применение такого bridge особенно полезно при работе с HTTP-запросами или файловыми операциями. Стандартные .NET классы как HttpClient естественно работают с async/await, но их результаты нужно интегрировать в игровую логику через корутины.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class NetworkManager : MonoBehaviour
{
    private HttpClient httpClient = new HttpClient();
 
    // Гибридный подход: async метод вызывается из корутины
    public IEnumerator LoadPlayerDataCoroutine(string playerId)
    {
        Task<PlayerData> loadTask = LoadPlayerDataAsync(playerId);
        
        yield return AsyncCoroutineBridge.Instance.AwaitTask(loadTask, data =>
        {
            if (data != null)
            {
                ApplyPlayerData(data);
                Debug.Log($"Данные игрока {playerId} загружены успешно");
            }
        });
    }
 
    private async Task<PlayerData> LoadPlayerDataAsync(string playerId)
    {
        try
        {
            var response = await httpClient.GetStringAsync($"https://api.game.com/players/{playerId}");
            return JsonUtility.FromJson<PlayerData>(response);
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Ошибка загрузки данных: {e.Message}");
            return null;
        }
    }
 
    // Корутина может запускать async операции и ждать их завершения
    public IEnumerator SaveGameStateCoroutine(GameState gameState)
    {
        Debug.Log("Начинаем сохранение состояния игры");
 
        // Показываем индикатор прогресса
        yield return StartCoroutine(ShowSavingIndicator(true));
 
        // Запускаем асинхронное сохранение
        Task<bool> saveTask = SaveGameStateAsync(gameState);
        
        bool saveResult = false;
        yield return AsyncCoroutineBridge.Instance.AwaitTask(saveTask, result => saveResult = result);
 
        // Скрываем индикатор
        yield return StartCoroutine(ShowSavingIndicator(false));
 
        if (saveResult)
        {
            yield return StartCoroutine(ShowSaveSuccessMessage());
        }
        else
        {
            yield return StartCoroutine(ShowSaveErrorMessage());
        }
    }
 
    private async Task<bool> SaveGameStateAsync(GameState gameState)
    {
        try
        {
            string jsonData = JsonUtility.ToJson(gameState);
            var content = new StringContent(jsonData, System.Text.Encoding.UTF8, "application/json");
            
            var response = await httpClient.PostAsync("https://api.game.com/save", content);
            return response.IsSuccessStatusCode;
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Ошибка сохранения: {e.Message}");
            return false;
        }
    }
 
    private IEnumerator ShowSavingIndicator(bool show)
    {
        // Анимация индикатора сохранения
        yield return new WaitForSeconds(0.3f);
    }
 
    private IEnumerator ShowSaveSuccessMessage()
    {
        Debug.Log("Игра сохранена успешно!");
        yield return new WaitForSeconds(2f);
    }
 
    private IEnumerator ShowSaveErrorMessage()
    {
        Debug.Log("Ошибка сохранения игры");
        yield return new WaitForSeconds(3f);
    }
}
Критически важно понимать, что async/await операции могут переключаться между потоками, что создает проблемы с Unity API. Большинство Unity методов можно вызывать только из главного потока, поэтому требуется careful синхронизация.

Другой подход заключается в создании custom awaitable объектов для корутин Unity. Это позволяет использовать await синтаксис с корутинами, делая код более последовательным:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static class CoroutineAwaiter
{
    public static CoroutineAwaitable ToAwaitable(this Coroutine coroutine)
    {
        return new CoroutineAwaitable(coroutine);
    }
}
 
public struct CoroutineAwaitable
{
    private readonly Coroutine coroutine;
 
    public CoroutineAwaitable(Coroutine coroutine)
    {
        this.coroutine = coroutine;
    }
 
    public CoroutineAwaiter GetAwaiter()
    {
        return new CoroutineAwaiter(coroutine);
    }
}
 
public struct CoroutineAwaiter : INotifyCompletion
{
    private readonly Coroutine coroutine;
 
    public CoroutineAwaiter(Coroutine coroutine)
    {
        this.coroutine = coroutine;
    }
 
    public bool IsCompleted => coroutine == null;
 
    public void GetResult() { }
 
    public void OnCompleted(System.Action continuation)
    {
        AsyncCoroutineBridge.Instance.StartCoroutine(WaitForCoroutine(continuation));
    }
 
    private IEnumerator WaitForCoroutine(System.Action continuation)
    {
        yield return coroutine;
        continuation?.Invoke();
    }
}
Интеграция async/await с корутинами требует осторожности, но открывает architectural possibilities для создания более maintainable кодовых баз, особенно при работе с внешними сервисами и сложной асинхронной логикой.

Гибридный подход с UniTask для высоконагруженных сценариев



UniTask радикально меняет подход к асинхронному программированию в Unity, предоставляя производительную альтернативу стандартным корутинам. Эта библиотека создана специально для solving performance bottlenecks, которые возникают при интенсивном использовании корутин в высоконагруженных сценариях. Основное преимущество UniTask заключается в том, что она практически не создает garbage collector pressure. В отличие от стандартных корутин, которые аллоцируют объекты итераторов для каждого yield, UniTask использует value-типы и структуры, что кардинально снижает нагрузку на память.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class UniTaskCoroutineHybrid : MonoBehaviour
{
    // Стандартный подход - создает GC аллокации
    IEnumerator TraditionalCoroutine()
    {
        for (int i = 0; i < 1000; i++)
        {
            yield return new WaitForSeconds(0.01f); // Аллокация каждую итерацию
        }
    }
 
    // UniTask подход - zero allocation
    async UniTask OptimizedTask()
    {
        for (int i = 0; i < 1000; i++)
        {
            await UniTask.Delay(10); // Без аллокаций
        }
    }
 
    // Гибридный подход для legacy кода
    IEnumerator HybridCoroutine()
    {
        // Запускаем heavy computation через UniTask
        var computationTask = HeavyComputationTask();
        
        // Возвращаемся в корутинный контекст для Unity-специфичной логики
        yield return computationTask.ToCoroutine();
        
        // Продолжаем в стандартной корутине
        yield return StartCoroutine(UnitySpecificAnimation());
    }
 
    private async UniTask HeavyComputationTask()
    {
        // Сложные вычисления без блокировки главного потока
        for (int i = 0; i < 10000; i++)
        {
            await UniTask.Yield(); // Передача управления без аллокаций
            PerformComplexCalculation(i);
        }
    }
 
    private void PerformComplexCalculation(int iteration)
    {
        // Симуляция вычислительно-интенсивной операции
        float result = Mathf.Sin(iteration * 0.1f) * Mathf.Cos(iteration * 0.2f);
    }
 
    private IEnumerator UnitySpecificAnimation()
    {
        // Логика, которая must stay в корутинах для compatibility
        yield return new WaitForSeconds(1f);
    }
}
Гибридная архитектура особенно эффективна в проектах, где уже существует большая кодовая база на корутинах, но появились performance requirements. Постепенная миграция критических участков на UniTask позволяет получить прирост производительности без полного переписывания системы.
UniTask предоставляет мощные инструменты для параллельного выполнения задач, что критично важно для высоконагруженных сценариев. В отличие от корутин, которые выполняются последовательно даже при "параллельном" запуске, UniTask может реально использовать многопоточность где это безопасно:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ParallelProcessingManager : MonoBehaviour
{
    async void Start()
    {
        await DemostrateParallelProcessing();
    }
 
    private async UniTask DemostrateParallelProcessing()
    {
        // Параллельное выполнение heavy operations
        var tasks = new UniTask[]
        {
            ProcessDataBatch(GenerateData(1000)),
            ProcessDataBatch(GenerateData(1500)),
            ProcessDataBatch(GenerateData(2000))
        };
 
        // Ожидаем завершения всех задач
        await UniTask.WhenAll(tasks);
 
        Debug.Log("Все батчи обработаны параллельно");
 
        // Комбинирование UniTask с корутинами для UI
        StartCoroutine(ShowCompletionAnimation());
    }
 
    private async UniTask ProcessDataBatch(int[] data)
    {
        // Тяжелые вычисления в background thread
        await UniTask.SwitchToThreadPool();
 
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = ExpensiveAlgorithm(data[i]);
 
            // Периодически возвращаемся к главному потоку для проверок
            if (i % 100 == 0)
            {
                await UniTask.SwitchToMainThread();
                
                if (this == null) // Проверка на уничтожение объекта
                {
                    Debug.Log("Обработка прервана - объект уничтожен");
                    return;
                }
                
                await UniTask.SwitchToThreadPool();
            }
        }
 
        // Возвращаемся к главному потоку для Unity API
        await UniTask.SwitchToMainThread();
        Debug.Log($"Batch из {data.Length} элементов обработан");
    }
 
    private int ExpensiveAlgorithm(int input)
    {
        // Симуляция дорогой операции
        return input * input + (int)(Mathf.Sin(input) * 1000);
    }
 
    private int[] GenerateData(int count)
    {
        var data = new int[count];
        for (int i = 0; i < count; i++)
        {
            data[i] = UnityEngine.Random.Range(1, 1000);
        }
        return data;
    }
 
    private IEnumerator ShowCompletionAnimation()
    {
        // UI анимация остается в корутинах для простоты
        for (float t = 0; t < 1f; t += Time.deltaTime)
        {
            // Анимация UI элементов
            yield return null;
        }
    }
}
Ключевая особенность UniTask заключается в её способности seamlessly переключаться между потоками. Это позволяет выносить CPU-intensive операции в background threads, оставляя главный поток свободным для обработки input и rendering. В высоконагруженных мобильных играх такой подход может дать прирост FPS на 15-20%.
UniTask также предоставляет продвинутые возможности для cancellation и timeout, которые работают значительно эффективнее чем аналогичные механизмы в корутинах:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class CancellationAwareManager : MonoBehaviour
{
    private CancellationTokenSource cancellationTokenSource;
 
    async void Start()
    {
        cancellationTokenSource = new CancellationTokenSource();
        
        try
        {
            await ProcessWithCancellation(cancellationTokenSource.Token);
        }
        catch (OperationCanceledException)
        {
            Debug.Log("Операция была отменена");
        }
    }
 
    private async UniTask ProcessWithCancellation(CancellationToken token)
    {
        // Комбинированная обработка с timeout и manual cancellation
        using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(token);
        timeoutSource.CancelAfter(TimeSpan.FromSeconds(30));
 
        var heavyTask = HeavyProcessingWithCancellation(timeoutSource.Token);
        var uiTask = UpdateProgressUI(timeoutSource.Token);
 
        // Выполняем задачи параллельно с возможностью отмены
        await UniTask.WhenAll(heavyTask, uiTask);
    }
 
    private async UniTask HeavyProcessingWithCancellation(CancellationToken token)
    {
        for (int i = 0; i < 10000; i++)
        {
            token.ThrowIfCancellationRequested();
 
            // Heavy computation
            await UniTask.Delay(1, cancellationToken: token);
            ProcessDataChunk(i);
 
            // Проверяем cancellation чаще для responsive отмены
            if (i % 10 == 0)
            {
                token.ThrowIfCancellationRequested();
            }
        }
    }
 
    private async UniTask UpdateProgressUI(CancellationToken token)
    {
        // UI обновления остаются на главном потоке
        while (!token.IsCancellationRequested)
        {
            // Обновление progress bar
            await UniTask.Delay(100, cancellationToken: token);
        }
    }
 
    private void ProcessDataChunk(int chunkIndex)
    {
        // Симуляция обработки данных
        float result = Mathf.Sqrt(chunkIndex * 1.5f);
    }
 
    void OnDestroy()
    {
        // Graceful shutdown при уничтожении объекта
        cancellationTokenSource?.Cancel();
        cancellationTokenSource?.Dispose();
    }
}
Интеграция UniTask с существующими корутинными системами требует осторожности, но предоставляет excellent migration path для legacy проектов. Critical performance bottlenecks можно переписать на UniTask, сохранив остальную архитектуру неизменной. Это особенно важно в коммерческих проектах, где полная refactoring невозможна по временным или бюджетным ограничениям.

Оптимизация производительности при глубокой вложенности



Глубокая вложенность корутин создает серьезные проблемы с производительностью, которые проявляются не сразу, а накапливаются постепенно. Я столкнулся с этим в проекте, где система квестов создавала цепочки из 12-15 уровней вложенности. На слабых мобильных устройствах это приводило к просадкам FPS до критических значений. Основная проблема заключается в том, что каждый уровень вложенности создает дополнительный объект итератора в памяти. Unity должен отслеживать состояние каждого из них, что создает overhead на каждом кадре. При глубокой вложенности количество таких объектов растет экспоненциально.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class OptimizedNestedManager : MonoBehaviour
{
    private Queue<System.Action> deferredActions = new Queue<System.Action>();
    private int maxDepth = 5;
    private int currentDepth = 0;
 
    // Вместо глубокой вложенности используем очередь выполнения
    public IEnumerator ExecuteSequentialTasks(IEnumerator[] tasks)
    {
        foreach (var task in tasks)
        {
            if (currentDepth >= maxDepth)
            {
                // Откладываем выполнение до разгрузки стека
                bool taskCompleted = false;
                deferredActions.Enqueue(() => StartCoroutine(ExecuteDeferred(task, () => taskCompleted = true)));
                
                yield return new WaitUntil(() => taskCompleted);
            }
            else
            {
                currentDepth++;
                yield return StartCoroutine(task);
                currentDepth--;
            }
        }
    }
 
    private IEnumerator ExecuteDeferred(IEnumerator task, System.Action onComplete)
    {
        yield return StartCoroutine(task);
        onComplete?.Invoke();
    }
 
    void Update()
    {
        // Обрабатываем отложенные задачи постепенно
        if (deferredActions.Count > 0 && currentDepth < maxDepth)
        {
            var action = deferredActions.Dequeue();
            action?.Invoke();
        }
    }
}
Критически важно понимать, что Unity Profiler не всегда точно отображает накладные расходы от корутин. Реальная нагрузка проявляется в виде увеличенного потребления памяти и более частых вызовов сборщика мусора. В профилировщике это выглядит как небольшие, но постоянные spike'и в разделе Scripting.
Другой подход заключается в использовании паттерна "корутина-диспетчер", которая управляет очередью задач вместо создания глубоко вложенных структур:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class FlattenedCoroutineDispatcher : MonoBehaviour
{
    private struct TaskNode
    {
        public IEnumerator task;
        public string id;
        public HashSet<string> dependencies;
        public bool isCompleted;
        public float startTime;
    }
 
    private List<TaskNode> taskGraph = new List<TaskNode>();
    private Dictionary<string, bool> completionStatus = new Dictionary<string, bool>();
 
    public string AddTask(string taskId, IEnumerator task, params string[] dependencies)
    {
        taskGraph.Add(new TaskNode
        {
            id = taskId,
            task = task,
            dependencies = new HashSet<string>(dependencies ?? new string[0]),
            isCompleted = false
        });
 
        return taskId;
    }
 
    public IEnumerator ExecuteAllTasks()
    {
        while (taskGraph.Any(t => !t.isCompleted))
        {
            var readyTasks = taskGraph.Where(t => 
                !t.isCompleted && 
                t.dependencies.All(dep => completionStatus.GetValueOrDefault(dep, false))
            ).ToList();
 
            if (readyTasks.Count == 0)
            {
                Debug.LogError("Обнаружена циклическая зависимость в графе задач");
                yield break;
            }
 
            // Выполняем готовые задачи параллельно, но без глубокой вложенности
            var runningTasks = new Dictionary<string, Coroutine>();
            
            foreach (var task in readyTasks)
            {
                var coroutine = StartCoroutine(ExecuteTaskWithTracking(task));
                runningTasks[task.id] = coroutine;
            }
 
            // Ждем завершения текущей партии
            foreach (var kvp in runningTasks)
            {
                yield return kvp.Value;
            }
 
            yield return null; // Даем время другим системам
        }
    }
 
    private IEnumerator ExecuteTaskWithTracking(TaskNode taskNode)
    {
        float startTime = Time.realtimeSinceStartup;
        
        try
        {
            yield return StartCoroutine(taskNode.task);
            completionStatus[taskNode.id] = true;
            
            // Обновляем статус в основном списке
            for (int i = 0; i < taskGraph.Count; i++)
            {
                if (taskGraph[i].id == taskNode.id)
                {
                    var updated = taskGraph[i];
                    updated.isCompleted = true;
                    taskGraph[i] = updated;
                    break;
                }
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Задача {taskNode.id} завершилась с ошибкой: {e.Message}");
            completionStatus[taskNode.id] = false;
        }
        
        float duration = Time.realtimeSinceStartup - startTime;
        if (duration > 0.1f) // Логируем долгие задачи
        {
            Debug.LogWarning($"Задача {taskNode.id} выполнялась {duration:F3}с");
        }
    }
}
Memory pooling становится критически важным при оптимизации глубоко вложенных структур. Вместо постоянного создания и уничтожения объектов итераторов, эффективнее переиспользовать готовые экземпляры:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class CoroutineIteratorPool : MonoBehaviour
{
    private Stack<IEnumerator> availableIterators = new Stack<IEnumerator>();
    private List<IEnumerator> activeIterators = new List<IEnumerator>();
 
    public IEnumerator GetPooledIterator(System.Func<IEnumerator> factory)
    {
        IEnumerator iterator;
        
        if (availableIterators.Count > 0)
        {
            iterator = availableIterators.Pop();
            ResetIterator(iterator);
        }
        else
        {
            iterator = factory();
        }
 
        activeIterators.Add(iterator);
        return iterator;
    }
 
    private void ResetIterator(IEnumerator iterator)
    {
        // Сброс состояния итератора для переиспользования
        iterator.Reset();
    }
 
    public void ReturnIterator(IEnumerator iterator)
    {
        activeIterators.Remove(iterator);
        availableIterators.Push(iterator);
    }
 
    void OnDestroy()
    {
        // Очистка пула при уничтожении
        availableIterators.Clear();
        activeIterators.Clear();
    }
}
Профилирование глубоко вложенных корутин require специальных техник, поскольку стандартные инструменты Unity не всегда могут точно измерить их impact. Я использую собственный легкий профайлер, который трекает время выполнения и аллокации памяти на каждом уровне вложенности.

Пример игрового сценария



Представим типичную RPG с пошаговой боевой системой, где игрок сражается против нескольких противников. Этот сценарий требует координации множества подсистем - анимации, звуков, UI, логики боя и AI противников. Основная архитектура строится вокруг центрального координатора боя, который управляет всеми участниками через систему событий и состояний. Каждый участник боя имеет свою корутину жизненного цикла, но все они синхронизируются через общие флаги состояния.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
public class CombatScenarioManager : MonoBehaviour
{
[Header("Combat Configuration")]
public float turnTimeout = 15f;
public int maxCombatRounds = 50;
public AnimationCurve difficultyScaling;
 
private CombatState currentState = CombatState.Initializing;
private List<CombatParticipant> allParticipants = new List<CombatParticipant>();
private Queue<CombatParticipant> turnQueue = new Queue<CombatParticipant>();
private int currentRound = 1;
private CombatResult finalResult;
 
private enum CombatState
{
    Initializing,
    WaitingForPlayerInput,
    ProcessingActions,
    RoundEnded,
    CombatFinished
}
 
public IEnumerator StartCombatScenario(List<GameObject> enemies, GameObject player)
{
    Debug.Log("=== Начало боевого сценария ===");
    
    // Инициализация всех систем в определенном порядке
    yield return StartCoroutine(InitializeCombatSystems());
    
    // Создаем участников боя
    yield return StartCoroutine(SetupParticipants(enemies, player));
    
    // Запускаем параллельные корутины мониторинга
    StartCoroutine(MonitorCombatTimeout());
    StartCoroutine(UpdateCombatUI());
    StartCoroutine(ProcessAudioCues());
    
    // Основной цикл боя
    while (currentState != CombatState.CombatFinished && currentRound <= maxCombatRounds)
    {
        yield return StartCoroutine(ExecuteCombatRound());
        currentRound++;
    }
    
    // Завершение боя
    yield return StartCoroutine(FinalizeCombat());
    
    Debug.Log($"=== Бой завершен. Результат: {finalResult} ===");
}
 
private IEnumerator InitializeCombatSystems()
{
    // Последовательная инициализация критически важных систем
    yield return StartCoroutine(InitializeAudioSystem());
    yield return StartCoroutine(InitializeUISystem());
    yield return StartCoroutine(InitializeEffectsSystem());
    
    GameStateFlags.SetFlag("Combat_Systems_Ready", true);
}
 
private IEnumerator SetupParticipants(List<GameObject> enemies, GameObject player)
{
    // Создаем участника-игрока
    var playerParticipant = new CombatParticipant(player, CombatParticipant.ParticipantType.Player);
    allParticipants.Add(playerParticipant);
    
    // Создаем противников с задержкой для анимации
    foreach (var enemy in enemies)
    {
        var enemyParticipant = new CombatParticipant(enemy, CombatParticipant.ParticipantType.Enemy);
        allParticipants.Add(enemyParticipant);
        
        StartCoroutine(enemyParticipant.InitializeAI());
        yield return new WaitForSeconds(0.3f); // Визуальная задержка появления
    }
    
    // Определяем порядок ходов на основе инициативы
    yield return StartCoroutine(CalculateInitiativeOrder());
    
    GameStateFlags.SetFlag("Participants_Ready", true);
}
 
private IEnumerator ExecuteCombatRound()
{
    Debug.Log($"--- Раунд {currentRound} ---");
    currentState = CombatState.WaitingForPlayerInput;
    
    // Обрабатываем ходы всех участников
    int participantIndex = 0;
    while (turnQueue.Count > 0)
    {
        var activeParticipant = turnQueue.Dequeue();
        
        if (!activeParticipant.IsAlive())
        {
            continue; // Пропускаем мертвых участников
        }
        
        Debug.Log($"Ход участника: {activeParticipant.Name}");
        
        // Обновляем UI для активного участника
        ReactiveCoroutineSystem.Instance.PublishEvent("Turn_Started", activeParticipant);
        
        // Выполняем действие участника с таймаутом
        bool actionCompleted = false;
        string timeoutTaskId = $"Turn_Timeout_{participantIndex}";
        
        var actionTask = activeParticipant.ExecuteAction();
        var timeoutTask = CreateTimeoutTask(timeoutTaskId, turnTimeout);
        
        // Запускаем задачи параллельно
        var actionCoroutine = StartCoroutine(TrackActionCompletion(actionTask, () => actionCompleted = true));
        var timeoutCoroutine = StartCoroutine(timeoutTask);
        
        // Ждем завершения любой из задач
        yield return new WaitUntil(() => actionCompleted || GameStateFlags.GetFlag($"{timeoutTaskId}_Expired"));
        
        if (!actionCompleted)
        {
            Debug.LogWarning($"Участник {activeParticipant.Name} превысил время хода");
            StopCoroutine(actionCoroutine);
            yield return StartCoroutine(activeParticipant.ForceDefaultAction());
        }
        else
        {
            StopCoroutine(timeoutCoroutine);
        }
        
        // Обрабатываем эффекты действия
        yield return StartCoroutine(ProcessActionEffects(activeParticipant));
        
        // Проверяем условия завершения боя
        if (IsCombatFinished())
        {
            break;
        }
        
        participantIndex++;
        yield return new WaitForSeconds(0.5f); // Пауза между ходами
    }
    
    // Восстанавливаем очередь для следующего раунда
    yield return StartCoroutine(RefreshTurnQueue());
    
    // Обрабатываем эффекты завершения раунда
    yield return StartCoroutine(ProcessEndOfRoundEffects());
}
 
private IEnumerator TrackActionCompletion(IEnumerator actionTask, System.Action onComplete)
{
    yield return StartCoroutine(actionTask);
    onComplete?.Invoke();
}
 
private IEnumerator CreateTimeoutTask(string taskId, float timeout)
{
    yield return new WaitForSeconds(timeout);
    GameStateFlags.SetFlag($"{taskId}_Expired", true);
}
 
private IEnumerator ProcessActionEffects(CombatParticipant actor)
{
    // Получаем список целей для обработки эффектов
    var targets = actor.GetCurrentTargets();
    
    if (targets == null || targets.Count == 0)
        yield break;
    
    // Обрабатываем эффекты для каждой цели параллельно
    var effectCoroutines = new List<Coroutine>();
    
    foreach (var target in targets)
    {
        if (target.IsAlive())
        {
            var effectCoroutine = StartCoroutine(ApplyEffectToTarget(actor, target));
            effectCoroutines.Add(effectCoroutine);
        }
    }
    
    // Ждем завершения всех эффектов
    foreach (var coroutine in effectCoroutines)
    {
        yield return coroutine;
    }
    
    // Публикуем событие завершения обработки эффектов
    ReactiveCoroutineSystem.Instance.PublishEvent("Effects_Processed", new { Actor = actor, Targets = targets });
}
 
private IEnumerator ApplyEffectToTarget(CombatParticipant actor, CombatParticipant target)
{
    // Рассчитываем урон или эффект
    var damage = CalculateDamage(actor, target);
    
    // Анимация атаки
    yield return StartCoroutine(PlayAttackAnimation(actor, target));
    
    // Применяем урон
    target.TakeDamage(damage);
    
    // Анимация получения урона
    yield return StartCoroutine(PlayDamageAnimation(target, damage));
    
    // Проверяем смерть цели
    if (!target.IsAlive())
    {
        yield return StartCoroutine(ProcessTargetDeath(target));
    }
}
 
private int CalculateDamage(CombatParticipant attacker, CombatParticipant target)
{
    int baseDamage = attacker.GetAttackPower();
    int defense = target.GetDefense();
    float difficultyModifier = difficultyScaling.Evaluate(currentRound / (float)maxCombatRounds);
    
    int finalDamage = Mathf.Max(1, Mathf.RoundToInt(baseDamage * difficultyModifier - defense));
    return finalDamage;
}
 
private IEnumerator PlayAttackAnimation(CombatParticipant attacker, CombatParticipant target)
{
    // Получаем компоненты анимации
    var attackerAnimator = attacker.GetAnimator();
    var targetAnimator = target.GetAnimator();
    
    if (attackerAnimator != null)
    {
        attackerAnimator.SetTrigger("Attack");
        
        // Ждем момента удара (обычно в середине анимации)
        yield return new WaitForSeconds(0.3f);
    }
    
    // Эффекты попадания
    yield return StartCoroutine(PlayHitEffects(target.transform.position));
}
 
private IEnumerator PlayDamageAnimation(CombatParticipant target, int damage)
{
    // Показываем число урона
    StartCoroutine(ShowFloatingDamageNumber(target.transform.position, damage));
    
    // Анимация получения урона
    var animator = target.GetAnimator();
    if (animator != null)
    {
        animator.SetTrigger("TakeDamage");
        yield return new WaitForSeconds(0.5f);
    }
    
    // Эффект встряски камеры для сильных ударов
    if (damage > target.GetMaxHealth() * 0.3f)
    {
        StartCoroutine(CameraShakeEffect(0.2f, 0.3f));
    }
}
 
private IEnumerator ShowFloatingDamageNumber(Vector3 position, int damage)
{
    // Создаем UI элемент для отображения урона
    var damageUI = FloatingTextPool.Instance.GetFloatingText();
    damageUI.SetText(damage.ToString());
    damageUI.transform.position = position;
    
    // Анимация всплытия и исчезновения
    for (float t = 0; t < 1.5f; t += Time.deltaTime)
    {
        damageUI.transform.position += Vector3.up * Time.deltaTime * 2f;
        
        float alpha = Mathf.Lerp(1f, 0f, t / 1.5f);
        damageUI.SetAlpha(alpha);
        
        yield return null;
    }
    
    FloatingTextPool.Instance.ReturnToPool(damageUI);
}
 
private IEnumerator ProcessTargetDeath(CombatParticipant target)
{
    Debug.Log($"{target.Name} побежден!");
    
    // Анимация смерти
    var animator = target.GetAnimator();
    if (animator != null)
    {
        animator.SetTrigger("Death");
        yield return new WaitForSeconds(1f);
    }
    
    // Эффекты смерти
    StartCoroutine(PlayDeathEffects(target.transform.position));
    
    // Удаляем из списка активных участников
    allParticipants.Remove(target);
    
    // Публикуем событие смерти
    ReactiveCoroutineSystem.Instance.PublishEvent("Participant_Defeated", target);
    
    yield return new WaitForSeconds(0.5f);
}
 
private bool IsCombatFinished()
{
    var aliveEnemies = allParticipants.Where(p => p.Type == CombatParticipant.ParticipantType.Enemy && p.IsAlive()).Count();
    var alivePlayers = allParticipants.Where(p => p.Type == CombatParticipant.ParticipantType.Player && p.IsAlive()).Count();
    
    if (aliveEnemies == 0)
    {
        finalResult = CombatResult.Victory;
        currentState = CombatState.CombatFinished;
        return true;
    }
    
    if (alivePlayers == 0)
    {
        finalResult = CombatResult.Defeat;
        currentState = CombatState.CombatFinished;
        return true;
    }
    
    return false;
}
 
private IEnumerator FinalizeCombat()
{
    // Останавливаем все фоновые корутины
    StopAllCoroutines();
    
    // Проигрываем финальную анимацию в зависимости от результата
    if (finalResult == CombatResult.Victory)
    {
        yield return StartCoroutine(PlayVictorySequence());
    }
    else
    {
        yield return StartCoroutine(PlayDefeatSequence());
    }
    
    // Очистка ресурсов
    yield return StartCoroutine(CleanupCombatResources());
}
 
private enum CombatResult
{
    Victory,
    Defeat,
    Timeout
}
}
Этот сценарий демонстрирует практическое применение всех рассмотренных техник. Корутины используются как для последовательных операций (инициализация систем), так и для параллельных (обработка эффектов). Таймауты предотвращают зависание при ожидании пользовательского ввода. Система событий координирует UI и игровую логику. Флаги состояния обеспечивают синхронизацию между различными подсистемами.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
public class CombatParticipant
{
    public enum ParticipantType { Player, Enemy }
    
    private GameObject gameObject;
    private ParticipantType type;
    private int currentHealth;
    private int maxHealth;
    private int attackPower;
    private int defense;
    private bool isAlive = true;
    private List<CombatParticipant> currentTargets = new List<CombatParticipant>();
    
    public string Name => gameObject.name;
    public ParticipantType Type => type;
    
    public CombatParticipant(GameObject obj, ParticipantType participantType)
    {
        gameObject = obj;
        type = participantType;
        
        // Инициализируем характеристики на основе типа
        if (type == ParticipantType.Player)
        {
            maxHealth = 100;
            attackPower = 25;
            defense = 8;
        }
        else
        {
            maxHealth = UnityEngine.Random.Range(60, 90);
            attackPower = UnityEngine.Random.Range(15, 30);
            defense = UnityEngine.Random.Range(3, 12);
        }
        
        currentHealth = maxHealth;
    }
    
    public IEnumerator InitializeAI()
    {
        if (type != ParticipantType.Enemy) yield break;
        
        // Симуляция инициализации AI с задержкой
        yield return new WaitForSeconds(UnityEngine.Random.Range(0.2f, 0.8f));
        
        var aiComponent = gameObject.GetComponent<EnemyAI>();
        if (aiComponent == null)
        {
            aiComponent = gameObject.AddComponent<EnemyAI>();
        }
        
        aiComponent.Initialize(this);
        Debug.Log($"AI для {Name} инициализирован");
    }
    
    public IEnumerator ExecuteAction()
    {
        if (type == ParticipantType.Player)
        {
            yield return StartCoroutine(WaitForPlayerInput());
        }
        else
        {
            yield return StartCoroutine(ExecuteAIAction());
        }
    }
    
    private IEnumerator WaitForPlayerInput()
    {
        Debug.Log("Ожидание выбора игрока...");
        
        // Активируем UI выбора действий
        ReactiveCoroutineSystem.Instance.PublishEvent("Show_Player_Actions", this);
        
        // Ждем пока игрок сделает выбор
        bool inputReceived = false;
        GameStateFlags.SetFlag("Player_Input_Required", true);
        
        ReactiveCoroutineSystem.Instance.WaitForFlag("Player_Action_Selected", () => inputReceived = true);
        
        yield return new WaitUntil(() => inputReceived);
        
        GameStateFlags.SetFlag("Player_Input_Required", false);
        ProcessPlayerChoice();
    }
    
    private IEnumerator ExecuteAIAction()
    {
        // Симуляция "размышлений" AI
        yield return new WaitForSeconds(UnityEngine.Random.Range(0.5f, 2f));
        
        var aiComponent = gameObject.GetComponent<EnemyAI>();
        if (aiComponent != null)
        {
            yield return StartCoroutine(aiComponent.SelectAction());
        }
        
        // Выбираем рандомную цель из живых игроков
        var potentialTargets = FindObjectsOfType<CombatParticipant>()
            .Where(p => p.Type == ParticipantType.Player && p.IsAlive())
            .ToList();
            
        if (potentialTargets.Count > 0)
        {
            var target = potentialTargets[UnityEngine.Random.Range(0, potentialTargets.Count)];
            currentTargets.Clear();
            currentTargets.Add(target);
        }
        
        Debug.Log($"{Name} выбрал цель для атаки");
    }
    
    public IEnumerator ForceDefaultAction()
    {
        Debug.Log($"{Name} выполняет действие по умолчанию (таймаут)");
        
        // Простейшее действие - атака первой доступной цели
        var enemies = FindObjectsOfType<CombatParticipant>()
            .Where(p => p.Type != this.type && p.IsAlive())
            .ToList();
            
        if (enemies.Count > 0)
        {
            currentTargets.Clear();
            currentTargets.Add(enemies[0]);
        }
        
        yield return new WaitForSeconds(0.3f);
    }
    
    private void ProcessPlayerChoice()
    {
        // В реальной игре здесь была бы обработка конкретного выбора игрока
        // Для демонстрации просто выбираем первого живого врага
        var enemies = FindObjectsOfType<CombatParticipant>()
            .Where(p => p.Type == ParticipantType.Enemy && p.IsAlive())
            .ToList();
            
        if (enemies.Count > 0)
        {
            currentTargets.Clear();
            currentTargets.Add(enemies[0]);
        }
    }
    
    public void TakeDamage(int damage)
    {
        currentHealth = Mathf.Max(0, currentHealth - damage);
        
        if (currentHealth <= 0)
        {
            isAlive = false;
        }
        
        Debug.Log($"{Name} получил {damage} урона. Здоровье: {currentHealth}/{maxHealth}");
        
        // Обновляем UI здоровья
        ReactiveCoroutineSystem.Instance.PublishEvent("Health_Changed", new { 
            Participant = this, 
            Damage = damage, 
            CurrentHealth = currentHealth 
        });
    }
    
    public bool IsAlive() => isAlive;
    public int GetAttackPower() => attackPower;
    public int GetDefense() => defense;
    public int GetMaxHealth() => maxHealth;
    public int GetCurrentHealth() => currentHealth;
    public Animator GetAnimator() => gameObject.GetComponent<Animator>();
    public List<CombatParticipant> GetCurrentTargets() => currentTargets;
}
 
public class EnemyAI : MonoBehaviour
{
    private CombatParticipant owner;
    private AIPersonality personality;
    
    public enum AIPersonality { Aggressive, Defensive, Balanced }
    
    public void Initialize(CombatParticipant participant)
    {
        owner = participant;
        personality = (AIPersonality)UnityEngine.Random.Range(0, 3);
        Debug.Log($"AI {owner.Name} имеет личность: {personality}");
    }
    
    public IEnumerator SelectAction()
    {
        // Анализируем ситуацию на поле боя
        yield return StartCoroutine(AnalyzeBattlefield());
        
        // Выбираем действие на основе личности AI
        switch (personality)
        {
            case AIPersonality.Aggressive:
                yield return StartCoroutine(SelectAggressiveAction());
                break;
            case AIPersonality.Defensive:
                yield return StartCoroutine(SelectDefensiveAction());
                break;
            default:
                yield return StartCoroutine(SelectBalancedAction());
                break;
        }
    }
    
    private IEnumerator AnalyzeBattlefield()
    {
        // Симуляция анализа ситуации
        yield return new WaitForSeconds(0.2f);
        
        var allParticipants = FindObjectsOfType<CombatParticipant>();
        var allies = allParticipants.Where(p => p.Type == owner.Type && p.IsAlive()).Count();
        var enemies = allParticipants.Where(p => p.Type != owner.Type && p.IsAlive()).Count();
        
        Debug.Log($"AI {owner.Name} анализирует: союзников {allies}, врагов {enemies}");
    }
    
    private IEnumerator SelectAggressiveAction()
    {
        yield return new WaitForSeconds(0.1f);
        Debug.Log($"{owner.Name} выбирает агрессивное действие");
    }
    
    private IEnumerator SelectDefensiveAction()
    {
        yield return new WaitForSeconds(0.3f);
        Debug.Log($"{owner.Name} выбирает защитное действие");
    }
    
    private IEnumerator SelectBalancedAction()
    {
        yield return new WaitForSeconds(0.2f);
        Debug.Log($"{owner.Name} выбирает сбалансированное действие");
    }
}
 
public class FloatingTextPool : MonoBehaviour
{
    public static FloatingTextPool Instance { get; private set; }
    
    [SerializeField] private FloatingText textPrefab;
    private Queue<FloatingText> pool = new Queue<FloatingText>();
    
    void Awake()
    {
        Instance = this;
        InitializePool(10);
    }
    
    private void InitializePool(int size)
    {
        for (int i = 0; i < size; i++)
        {
            var text = Instantiate(textPrefab, transform);
            text.gameObject.SetActive(false);
            pool.Enqueue(text);
        }
    }
    
    public FloatingText GetFloatingText()
    {
        if (pool.Count == 0)
        {
            return Instantiate(textPrefab, transform);
        }
        
        var text = pool.Dequeue();
        text.gameObject.SetActive(true);
        return text;
    }
    
    public void ReturnToPool(FloatingText text)
    {
        text.gameObject.SetActive(false);
        pool.Enqueue(text);
    }
}
 
public class FloatingText : MonoBehaviour
{
    [SerializeField] private Text textComponent;
    
    public void SetText(string text)
    {
        textComponent.text = text;
    }
    
    public void SetAlpha(float alpha)
    {
        var color = textComponent.color;
        color.a = alpha;
        textComponent.color = color;
    }
}
Дополнительные вспомогательные корутины завершают архитектуру боевой системы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
private IEnumerator MonitorCombatTimeout()
{
    float combatStartTime = Time.time;
    float maxCombatDuration = maxCombatRounds * 30f; // 30 секунд на раунд
    
    while (currentState != CombatState.CombatFinished)
    {
        if (Time.time - combatStartTime > maxCombatDuration)
        {
            Debug.LogWarning("Бой превысил максимальную длительность");
            finalResult = CombatResult.Timeout;
            currentState = CombatState.CombatFinished;
            break;
        }
        
        yield return new WaitForSeconds(5f); // Проверяем каждые 5 секунд
    }
}
 
private IEnumerator UpdateCombatUI()
{
    while (currentState != CombatState.CombatFinished)
    {
        // Обновляем информацию о текущем состоянии боя
        var uiData = new {
            Round = currentRound,
            State = currentState,
            Participants = allParticipants.Where(p => p.IsAlive()).ToList()
        };
        
        ReactiveCoroutineSystem.Instance.PublishEvent("Combat_UI_Update", uiData);
        yield return new WaitForSeconds(0.5f);
    }
}
 
private IEnumerator ProcessAudioCues()
{
    while (currentState != CombatState.CombatFinished)
    {
        // Проигрываем фоновую музыку в зависимости от интенсивности боя
        int aliveEnemies = allParticipants.Count(p => p.Type == CombatParticipant.ParticipantType.Enemy && p.IsAlive());
        
        if (aliveEnemies <= 1)
        {
            ReactiveCoroutineSystem.Instance.PublishEvent("Play_Victory_Music", null);
        }
        else if (allParticipants.Any(p => p.Type == CombatParticipant.ParticipantType.Player && 
                 p.GetCurrentHealth() < p.GetMaxHealth() * 0.3f))
        {
            ReactiveCoroutineSystem.Instance.PublishEvent("Play_Tension_Music", null);
        }
        
        yield return new WaitForSeconds(2f);
    }
}
 
private IEnumerator PlayHitEffects(Vector3 position)
{
    // Создание партицл эффектов попадания
    yield return new WaitForSeconds(0.1f);
    Debug.Log($"Эффект попадания в позиции {position}");
}
 
private IEnumerator CameraShakeEffect(float intensity, float duration)
{
    Vector3 originalPosition = Camera.main.transform.position;
    
    for (float t = 0; t < duration; t += Time.deltaTime)
    {
        Vector3 shakeOffset = UnityEngine.Random.insideUnitSphere * intensity;
        Camera.main.transform.position = originalPosition + shakeOffset;
        yield return null;
    }
    
    Camera.main.transform.position = originalPosition;
}
Критически важные вспомогательные системы обеспечивают стабильную работу всего боевого сценария:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
private IEnumerator InitializeAudioSystem()
{
    var audioManager = FindObjectOfType<AudioManager>();
    if (audioManager == null)
    {
        Debug.LogError("AudioManager не найден в сцене");
        yield break;
    }
 
    yield return StartCoroutine(audioManager.LoadCombatAudioClips());
    
    // Подписываемся на боевые события
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Play_Victory_Music", _ => audioManager.PlayVictoryMusic());
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Play_Tension_Music", _ => audioManager.PlayTensionMusic());
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Play_Attack_Sound", data => audioManager.PlayAttackSound(data));
 
    Debug.Log("Аудио система инициализирована");
}
 
private IEnumerator InitializeUISystem()
{
    var uiManager = FindObjectOfType<CombatUIManager>();
    if (uiManager == null)
    {
        Debug.LogError("CombatUIManager не найден");
        yield break;
    }
 
    yield return StartCoroutine(uiManager.InitializeCombatUI());
    
    // Подписываем UI на игровые события
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Health_Changed", data => uiManager.UpdateHealthDisplay(data));
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Turn_Started", data => uiManager.HighlightActiveParticipant(data));
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Show_Player_Actions", data => uiManager.ShowActionButtons(data));
    ReactiveCoroutineSystem.Instance.SubscribeCoroutine("Combat_UI_Update", data => uiManager.UpdateCombatState(data));
 
    Debug.Log("UI система готова к бою");
}
 
private IEnumerator CalculateInitiativeOrder()
{
    // Рассчитываем и сортируем участников по инициативе
    var initiativeList = new List<(CombatParticipant participant, int initiative)>();
 
    foreach (var participant in allParticipants)
    {
        int baseInitiative = participant.Type == CombatParticipant.ParticipantType.Player ? 15 : 10;
        int randomModifier = UnityEngine.Random.Range(-5, 6);
        int finalInitiative = baseInitiative + randomModifier;
        
        initiativeList.Add((participant, finalInitiative));
        yield return new WaitForSeconds(0.1f); // Небольшая задержка для visual feedback
    }
 
    // Сортируем по убыванию инициативы
    var sortedParticipants = initiativeList.OrderByDescending(x => x.initiative).Select(x => x.participant);
    
    turnQueue.Clear();
    foreach (var participant in sortedParticipants)
    {
        turnQueue.Enqueue(participant);
    }
 
    Debug.Log("Порядок ходов определен");
}
 
private IEnumerator RefreshTurnQueue()
{
    // Восстанавливаем очередь ходов для нового раунда
    var livingParticipants = allParticipants.Where(p => p.IsAlive()).ToList();
    
    turnQueue.Clear();
    foreach (var participant in livingParticipants)
    {
        turnQueue.Enqueue(participant);
    }
 
    yield return new WaitForSeconds(0.2f);
}
 
private IEnumerator ProcessEndOfRoundEffects()
{
    Debug.Log("Обработка эффектов завершения раунда");
    
    // Обрабатываем периодические эффекты (яд, регенерация и т.д.)
    var effectTasks = new List<Coroutine>();
    
    foreach (var participant in allParticipants.Where(p => p.IsAlive()))
    {
        var effectCoroutine = StartCoroutine(ProcessParticipantEndRoundEffects(participant));
        effectTasks.Add(effectCoroutine);
    }
    
    // Ждем завершения всех эффектов
    foreach (var task in effectTasks)
    {
        yield return task;
    }
 
    // Применяем модификаторы сложности для следующего раунда
    if (currentRound % 5 == 0) // Каждые 5 раундов
    {
        yield return StartCoroutine(ApplyDifficultyScaling());
    }
 
    ReactiveCoroutineSystem.Instance.PublishEvent("Round_Ended", new { Round = currentRound });
}
 
private IEnumerator ProcessParticipantEndRoundEffects(CombatParticipant participant)
{
    // Симуляция различных эффектов завершения раунда
    yield return new WaitForSeconds(0.1f);
    
    // Пример: восстановление небольшого количества здоровья
    if (participant.GetCurrentHealth() < participant.GetMaxHealth() * 0.5f)
    {
        int healAmount = Mathf.RoundToInt(participant.GetMaxHealth() * 0.05f);
        // participant.Heal(healAmount); - метод который нужно добавить
        Debug.Log($"{participant.Name} восстанавливает {healAmount} здоровья");
    }
}
 
private IEnumerator ApplyDifficultyScaling()
{
    Debug.Log("Применение масштабирования сложности");
    
    // Усиливаем выживших врагов
    foreach (var enemy in allParticipants.Where(p => p.Type == CombatParticipant.ParticipantType.Enemy && p.IsAlive()))
    {
        // enemy.ApplyStatBoost(1.1f); - метод для усиления характеристик
        yield return new WaitForSeconds(0.1f);
    }
}
 
private IEnumerator PlayVictorySequence()
{
    Debug.Log("Проигрываем последовательность победы");
    
    // Останавливаем боевую музыку
    ReactiveCoroutineSystem.Instance.PublishEvent("Stop_Combat_Music", null);
    
    yield return new WaitForSeconds(0.5f);
    
    // Анимация победы для игрока
    var player = allParticipants.FirstOrDefault(p => p.Type == CombatParticipant.ParticipantType.Player);
    if (player != null)
    {
        var animator = player.GetAnimator();
        if (animator != null)
        {
            animator.SetTrigger("Victory");
        }
    }
    
    // Запускаем победную музыку
    ReactiveCoroutineSystem.Instance.PublishEvent("Play_Victory_Music", null);
    
    yield return new WaitForSeconds(2f);
    
    // Показываем UI экран победы
    ReactiveCoroutineSystem.Instance.PublishEvent("Show_Victory_Screen", new { 
        Round = currentRound, 
        Experience = CalculateExperienceReward() 
    });
    
    yield return new WaitForSeconds(3f);
}
 
private IEnumerator PlayDefeatSequence()
{
    Debug.Log("Проигрываем последовательность поражения");
    
    // Затемнение экрана
    ReactiveCoroutineSystem.Instance.PublishEvent("Fade_Screen", true);
    
    yield return new WaitForSeconds(1f);
    
    // Печальная музыка
    ReactiveCoroutineSystem.Instance.PublishEvent("Play_Defeat_Music", null);
    
    yield return new WaitForSeconds(2f);
    
    // Экран поражения
    ReactiveCoroutineSystem.Instance.PublishEvent("Show_Defeat_Screen", new { 
        Round = currentRound,
        LastWords = "Попробуйте еще раз!" 
    });
    
    yield return new WaitForSeconds(3f);
}
 
private int CalculateExperienceReward()
{
    int baseExp = currentRound * 10;
    return baseExp + allParticipants.Where(p => !p.IsAlive()).Count() * 5;
}
 
private IEnumerator CleanupCombatResources()
{
    Debug.Log("Очистка ресурсов боевой системы");
    
    // Останавливаем все звуки
    ReactiveCoroutineSystem.Instance.PublishEvent("Stop_All_Audio", null);
    
    yield return new WaitForSeconds(0.2f);
    
    // Очищаем пулы объектов
    FloatingTextPool.Instance.ClearPool();
    
    // Сбрасываем флаги состояния
    GameStateFlags.SetFlag("Combat_Systems_Ready", false);
    GameStateFlags.SetFlag("Participants_Ready", false);
    
    // Удаляем временные эффекты
    var effectObjects = GameObject.FindGameObjectsWithTag("CombatEffect");
    foreach (var effect in effectObjects)
    {
        Destroy(effect);
    }
    
    allParticipants.Clear();
    turnQueue.Clear();
    
    yield return new WaitForSeconds(0.5f);
    
    Debug.Log("Очистка ресурсов завершена");
}
Система управления состояниями дополняет архитектуру, предоставляя централизованный контроль over всем боевым процессом. Каждая фаза боя четко разделена, что упрощает отладку и добавление новой функциональности. Реактивная система событий обеспечивает слабую связанность между компонентами, позволяя легко добавлять новые системы обработки событий без modification существующего кода.

Система загрузки уровня с анимациями и проверками



Рассмотрев теоретические аспекты, перейдем к практической реализации комплексной системы, объединяющей все изученные техники. Система загрузки уровня с проверками целостности и синхронизированными анимациями демонстрирует, как корутины могут элегантно решать сложные архитектурные задачи.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
public class LevelLoadingSystem : MonoBehaviour
{
    [Header("Loading Configuration")]
    public float maxLoadingTime = 120f;
    public int retryAttempts = 3;
    public AnimationCurve progressCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
    
    private LoadingContext currentContext;
    private Dictionary<string, LoadingPhase> loadingPhases = new Dictionary<string, LoadingPhase>();
    private Queue<LoadingTask> pendingTasks = new Queue<LoadingTask>();
    
    private class LoadingContext
    {
        public string sceneName;
        public Dictionary<string, object> sceneParameters;
        public float startTime;
        public int currentRetry;
        public bool isPreloading;
        public List<string> completedPhases;
        public LoadingResult result;
    }
    
    private class LoadingPhase
    {
        public string phaseName;
        public List<LoadingTask> tasks;
        public bool allowParallel;
        public float timeoutSeconds;
        public int priority;
        public System.Func<LoadingContext, bool> canExecute;
    }
    
    private class LoadingTask
    {
        public string taskName;
        public System.Func<LoadingContext, IEnumerator> taskCoroutine;
        public float estimatedDuration;
        public bool isCritical;
        public List<string> dependencies;
        public TaskStatus status;
    }
    
    private enum TaskStatus { Pending, Running, Completed, Failed, Skipped }
    private enum LoadingResult { Success, Failed, Cancelled, Timeout }
 
    void Awake()
    {
        InitializePredefinedPhases();
        DontDestroyOnLoad(gameObject);
    }
 
    public IEnumerator LoadSceneWithValidation(string sceneName, Dictionary<string, object> parameters = null)
    {
        Debug.Log($"Начинаем загрузку сцены: {sceneName}");
        
        currentContext = new LoadingContext
        {
            sceneName = sceneName,
            sceneParameters = parameters ?? new Dictionary<string, object>(),
            startTime = Time.realtimeSinceStartup,
            currentRetry = 0,
            isPreloading = false,
            completedPhases = new List<string>()
        };
 
        // Запускаем параллельные корутины мониторинга
        StartCoroutine(MonitorLoadingProgress());
        StartCoroutine(UpdateLoadingAnimations());
        StartCoroutine(ValidateLoadingIntegrity());
 
        bool loadingSuccessful = false;
        
        // Основной цикл загрузки с повторными попытками
        while (currentContext.currentRetry <= retryAttempts && !loadingSuccessful)
        {
            try
            {
                yield return StartCoroutine(ExecuteLoadingSequence());
                loadingSuccessful = (currentContext.result == LoadingResult.Success);
            }
            catch (System.Exception e)
            {
                Debug.LogError($"Критическая ошибка загрузки: {e.Message}");
                currentContext.currentRetry++;
                
                if (currentContext.currentRetry <= retryAttempts)
                {
                    Debug.Log($"Повторная попытка {currentContext.currentRetry}/{retryAttempts}");
                    yield return StartCoroutine(CleanupFailedLoading());
                    yield return new WaitForSeconds(Mathf.Pow(2, currentContext.currentRetry)); // Экспоненциальная задержка
                }
            }
        }
 
        yield return StartCoroutine(FinalizeLoading(loadingSuccessful));
        
        Debug.Log($"Загрузка завершена. Результат: {currentContext.result}");
    }
 
    private void InitializePredefinedPhases()
    {
        // Фаза предварительной проверки
        loadingPhases["PreValidation"] = new LoadingPhase
        {
            phaseName = "PreValidation",
            tasks = new List<LoadingTask>
            {
                new LoadingTask { taskName = "CheckDiskSpace", taskCoroutine = CheckAvailableDiskSpace, estimatedDuration = 0.2f, isCritical = true },
                new LoadingTask { taskName = "ValidateSceneExists", taskCoroutine = ValidateSceneFile, estimatedDuration = 0.1f, isCritical = true },
                new LoadingTask { taskName = "CheckMemory", taskCoroutine = ValidateAvailableMemory, estimatedDuration = 0.1f, isCritical = false }
            },
            allowParallel = true,
            timeoutSeconds = 5f,
            priority = 100,
            canExecute = context => !context.isPreloading
        };
 
        // Фаза загрузки данных
        loadingPhases["DataLoading"] = new LoadingPhase
        {
            phaseName = "DataLoading",
            tasks = new List<LoadingTask>
            {
                new LoadingTask { taskName = "LoadPlayerData", taskCoroutine = LoadPlayerData, estimatedDuration = 1.5f, isCritical = true, dependencies = new List<string>{"PreValidation"} },
                new LoadingTask { taskName = "LoadAssets", taskCoroutine = LoadSceneAssets, estimatedDuration = 3.0f, isCritical = true, dependencies = new List<string>{"PreValidation"} },
                new LoadingTask { taskName = "LoadAudio", taskCoroutine = LoadAudioResources, estimatedDuration = 2.0f, isCritical = false, dependencies = new List<string>{"LoadAssets"} }
            },
            allowParallel = false,
            timeoutSeconds = 30f,
            priority = 90
        };
 
        // Фаза инициализации
        loadingPhases["Initialization"] = new LoadingPhase
        {
            phaseName = "Initialization",
            tasks = new List<LoadingTask>
            {
                new LoadingTask { taskName = "InitializeGameSystems", taskCoroutine = InitializeGameSystems, estimatedDuration = 2.5f, isCritical = true, dependencies = new List<string>{"DataLoading"} },
                new LoadingTask { taskName = "SetupUI", taskCoroutine = SetupUserInterface, estimatedDuration = 1.0f, isCritical = true, dependencies = new List<string>{"InitializeGameSystems"} },
                new LoadingTask { taskName = "FinalValidation", taskCoroutine = PerformFinalValidation, estimatedDuration = 0.5f, isCritical = true, dependencies = new List<string>{"SetupUI"} }
            },
            allowParallel = false,
            timeoutSeconds = 15f,
            priority = 80
        };
    }
 
    private IEnumerator ExecuteLoadingSequence()
    {
        var sortedPhases = loadingPhases.Values.OrderByDescending(p => p.priority).ToList();
        
        foreach (var phase in sortedPhases)
        {
            if (phase.canExecute?.Invoke(currentContext) == false)
            {
                Debug.Log($"Пропускаем фазу {phase.phaseName} - условие не выполнено");
                continue;
            }
 
            Debug.Log($"Выполняем фазу загрузки: {phase.phaseName}");
            ReactiveCoroutineSystem.Instance.PublishEvent("Loading_Phase_Started", new { Phase = phase.phaseName, Context = currentContext });
 
            bool phaseCompleted = false;
            string timeoutTaskId = $"Phase_Timeout_{phase.phaseName}";
 
            var phaseCoroutine = StartCoroutine(ExecuteLoadingPhase(phase, () => phaseCompleted = true));
            var timeoutCoroutine = StartCoroutine(CreatePhaseTimeout(timeoutTaskId, phase.timeoutSeconds));
 
            // Ожидаем завершения фазы или таймаута
            yield return new WaitUntil(() => phaseCompleted || GameStateFlags.GetFlag($"{timeoutTaskId}_Expired"));
 
            if (!phaseCompleted)
            {
                Debug.LogError($"Фаза {phase.phaseName} превысила время выполнения");
                StopCoroutine(phaseCoroutine);
                throw new System.TimeoutException($"Loading phase {phase.phaseName} timeout");
            }
            else
            {
                StopCoroutine(timeoutCoroutine);
                currentContext.completedPhases.Add(phase.phaseName);
                ReactiveCoroutineSystem.Instance.PublishEvent("Loading_Phase_Completed", new { Phase = phase.phaseName });
            }
 
            // Проверяем критические ошибки после каждой фазы
            if (HasCriticalFailures(phase))
            {
                throw new System.Exception($"Critical failures in phase {phase.phaseName}");
            }
 
            yield return new WaitForSeconds(0.1f); // Небольшая пауза между фазами
        }
 
        currentContext.result = LoadingResult.Success;
    }
 
    private IEnumerator ExecuteLoadingPhase(LoadingPhase phase, System.Action onComplete)
    {
        var readyTasks = phase.tasks.Where(t => AreDependenciesSatisfied(t)).ToList();
        var runningTasks = new List<Coroutine>();
 
        if (phase.allowParallel)
        {
            // Параллельное выполнение задач
            foreach (var task in readyTasks)
            {
                task.status = TaskStatus.Running;
                var taskCoroutine = StartCoroutine(ExecuteLoadingTask(task, currentContext));
                runningTasks.Add(taskCoroutine);
            }
 
            // Ожидаем завершения всех параллельных задач
            foreach (var taskCoroutine in runningTasks)
            {
                yield return taskCoroutine;
            }
        }
        else
        {
            // Последовательное выполнение задач
            foreach (var task in readyTasks)
            {
                yield return StartCoroutine(ExecuteLoadingTask(task, currentContext));
 
                // Прерываем последовательность при критической ошибке
                if (task.isCritical && task.status == TaskStatus.Failed)
                {
                    Debug.LogError($"Критическая задача {task.taskName} провалена");
                    break;
                }
            }
        }
 
        onComplete?.Invoke();
    }
 
    private IEnumerator ExecuteLoadingTask(LoadingTask task, LoadingContext context)
    {
        Debug.Log($"Выполняем задачу загрузки: {task.taskName}");
        task.status = TaskStatus.Running;
 
        float taskStartTime = Time.realtimeSinceStartup;
 
        try
        {
            yield return StartCoroutine(task.taskCoroutine(context));
            task.status = TaskStatus.Completed;
 
            float actualDuration = Time.realtimeSinceStartup - taskStartTime;
            if (actualDuration > task.estimatedDuration * 2f)
            {
                Debug.LogWarning($"Задача {task.taskName} выполнялась дольше ожидаемого: {actualDuration:F2}с vs {task.estimatedDuration:F2}с");
            }
        }
        catch (System.Exception e)
        {
            task.status = TaskStatus.Failed;
            Debug.LogError($"Ошибка в задаче {task.taskName}: {e.Message}");
 
            if (task.isCritical)
            {
                throw;
            }
        }
 
        ReactiveCoroutineSystem.Instance.PublishEvent("Loading_Task_Completed", new { Task = task.taskName, Status = task.status });
    }
 
    private bool AreDependenciesSatisfied(LoadingTask task)
    {
        if (task.dependencies == null || task.dependencies.Count == 0)
            return true;
 
        return task.dependencies.All(dep => currentContext.completedPhases.Contains(dep));
    }
 
    private bool HasCriticalFailures(LoadingPhase phase)
    {
        return phase.tasks.Any(t => t.isCritical && t.status == TaskStatus.Failed);
    }
 
    private IEnumerator CreatePhaseTimeout(string timeoutId, float timeoutSeconds)
    {
        yield return new WaitForSeconds(timeoutSeconds);
        GameStateFlags.SetFlag($"{timeoutId}_Expired", true);
    }
 
    private IEnumerator MonitorLoadingProgress()
    {
        while (currentContext != null && currentContext.result == LoadingResult.Success)
        {
            // Рассчитываем прогресс на основе завершенных задач
            float totalTasks = loadingPhases.Values.SelectMany(p => p.tasks).Count();
            float completedTasks = loadingPhases.Values.SelectMany(p => p.tasks).Count(t => t.status == TaskStatus.Completed);
 
            float rawProgress = completedTasks / totalTasks;
            float smoothProgress = progressCurve.Evaluate(rawProgress);
 
            ReactiveCoroutineSystem.Instance.PublishEvent("Loading_Progress_Updated", new { Progress = smoothProgress, Context = currentContext });
 
            // Проверяем общий таймаут загрузки
            float elapsedTime = Time.realtimeSinceStartup - currentContext.startTime;
            if (elapsedTime > maxLoadingTime)
            {
                Debug.LogError("Превышено максимальное время загрузки");
                currentContext.result = LoadingResult.Timeout;
                break;
            }
 
            yield return new WaitForSeconds(0.1f);
        }
    }
 
    private IEnumerator UpdateLoadingAnimations()
    {
        while (currentContext != null && currentContext.result == LoadingResult.Success)
        {
            // Обновляем анимации загрузочного экрана
            var animationData = new
            {
                CurrentPhase = currentContext.completedPhases.LastOrDefault(),
                ElapsedTime = Time.realtimeSinceStartup - currentContext.startTime,
                RetryAttempt = currentContext.currentRetry
            };
 
            ReactiveCoroutineSystem.Instance.PublishEvent("Update_Loading_Animation", animationData);
            yield return new WaitForSeconds(0.05f); // 20 FPS для анимаций
        }
    }
 
    private IEnumerator ValidateLoadingIntegrity()
    {
        while (currentContext != null && currentContext.result == LoadingResult.Success)
        {
            // Периодически проверяем целостность загрузки
            yield return new WaitForSeconds(2f);
 
            bool integrityOk = true;
 
            // Проверяем критические системы
            if (FindObjectOfType<GameStateManager>() == null && currentContext.completedPhases.Contains("Initialization"))
            {
                Debug.LogError("GameStateManager не найден после инициализации");
                integrityOk = false;
            }
 
            // Проверяем память
            if (System.GC.GetTotalMemory(false) > 500 * 1024 * 1024) // 500MB threshold
            {
                Debug.LogWarning("Высокое потребление памяти во время загрузки");
                System.GC.Collect(); // Принудительная сборка мусора
            }
 
            // Проверяем производительность
            if (Time.deltaTime > 0.033f) // Ниже 30 FPS
            {
                Debug.LogWarning("Низкая производительность во время загрузки");
            }
 
            if (!integrityOk)
            {
                currentContext.result = LoadingResult.Failed;
                break;
            }
        }
    }
 
    private IEnumerator CleanupFailedLoading()
    {
        Debug.Log("Очистка после неудачной загрузки");
 
        // Останавливаем все активные корутины загрузки
        StopAllCoroutines();
 
        // Очищаем загруженные ресурсы
        Resources.UnloadUnusedAssets();
        yield return new WaitForSeconds(0.5f);
 
        System.GC.Collect();
        yield return new WaitForSeconds(0.5f);
 
        // Сбрасываем состояние задач
        foreach (var phase in loadingPhases.Values)
        {
            foreach (var task in phase.tasks)
            {
                if (task.status != TaskStatus.Completed)
                {
                    task.status = TaskStatus.Pending;
                }
            }
        }
 
        // Сбрасываем флаги состояния
        GameStateFlags.SetFlag("Loading_Systems_Ready", false);
 
        currentContext.completedPhases.Clear();
        ReactiveCoroutineSystem.Instance.PublishEvent("Loading_Cleanup_Completed", currentContext);
    }
 
    private IEnumerator FinalizeLoading(bool successful)
    {
        if (successful)
        {
            Debug.Log("Финализация успешной загрузки");
            yield return StartCoroutine(PlaySuccessAnimation());
            
            GameStateFlags.SetFlag("Scene_Fully_Loaded", true);
            ReactiveCoroutineSystem.Instance.PublishEvent("Scene_Load_Success", currentContext);
        }
        else
        {
            Debug.LogError("Финализация неудачной загрузки");
            yield return StartCoroutine(PlayErrorAnimation());
            
            ReactiveCoroutineSystem.Instance.PublishEvent("Scene_Load_Failed", currentContext);
        }
 
        // Очистка контекста загрузки
        yield return new WaitForSeconds(1f);
        currentContext = null;
    }
 
    // Конкретные задачи загрузки
    private IEnumerator CheckAvailableDiskSpace(LoadingContext context)
    {
        yield return new WaitForSeconds(0.1f);
 
        long requiredSpace = 100 * 1024 * 1024; // 100MB
        long availableSpace = GetAvailableDiskSpace();
 
        if (availableSpace < requiredSpace)
        {
            throw new System.InvalidOperationException($"Недостаточно места на диске: {availableSpace / (1024 * 1024)}MB доступно, {requiredSpace / (1024 * 1024)}MB требуется");
        }
 
        Debug.Log("Проверка места на диске пройдена");
    }
 
    private IEnumerator ValidateSceneFile(LoadingContext context)
    {
        yield return new WaitForSeconds(0.05f);
 
        bool sceneExists = Application.CanStreamedLevelBeLoaded(context.sceneName);
        if (!sceneExists)
        {
            throw new System.IO.FileNotFoundException($"Сцена {context.sceneName} не найдена");
        }
 
        Debug.Log($"Сцена {context.sceneName} найдена и доступна");
    }
 
    private IEnumerator ValidateAvailableMemory(LoadingContext context)
    {
        yield return new WaitForSeconds(0.08f);
 
        long currentMemory = System.GC.GetTotalMemory(false);
        long maxMemory = 1000 * 1024 * 1024; // 1GB limit для мобильных устройств
 
        if (currentMemory > maxMemory * 0.8f)
        {
            Debug.LogWarning($"Высокое потребление памяти: {currentMemory / (1024 * 1024)}MB");
            System.GC.Collect();
            yield return new WaitForSeconds(0.2f);
        }
 
        Debug.Log("Проверка памяти завершена");
    }
 
    private IEnumerator LoadPlayerData(LoadingContext context)
    {
        Debug.Log("Загружаем данные игрока");
        
        for (float t = 0; t < 1.5f; t += Time.deltaTime)
        {
            float progress = t / 1.5f;
            ReactiveCoroutineSystem.Instance.PublishEvent("Player_Data_Progress", progress);
            yield return null;
        }
 
        // Симуляция возможной ошибки загрузки данных
        if (UnityEngine.Random.Range(0, 10) < 1) // 10% шанс ошибки
        {
            throw new System.Exception("Ошибка загрузки данных игрока с сервера");
        }
 
        context.sceneParameters["PlayerDataLoaded"] = true;
        Debug.Log("Данные игрока загружены");
    }
 
    private IEnumerator LoadSceneAssets(LoadingContext context)
    {
        Debug.Log("Загружаем ассеты сцены");
 
        // Асинхронная загрузка сцены
        AsyncOperation sceneLoad = SceneManager.LoadSceneAsync(context.sceneName, LoadSceneMode.Additive);
        sceneLoad.allowSceneActivation = false;
 
        while (sceneLoad.progress < 0.9f)
        {
            ReactiveCoroutineSystem.Instance.PublishEvent("Asset_Load_Progress", sceneLoad.progress);
            yield return null;
        }
 
        // Дополнительная проверка перед активацией
        yield return new WaitForSeconds(0.5f);
 
        sceneLoad.allowSceneActivation = true;
        yield return sceneLoad;
 
        context.sceneParameters["AssetsLoaded"] = true;
        Debug.Log("Ассеты сцены загружены");
    }
 
    private IEnumerator LoadAudioResources(LoadingContext context)
    {
        Debug.Log("Загружаем аудио ресурсы");
 
        // Симуляция загрузки различных аудио файлов
        string[] audioFiles = { "background_music", "sfx_pack", "voice_lines" };
 
        for (int i = 0; i < audioFiles.Length; i++)
        {
            yield return new WaitForSeconds(UnityEngine.Random.Range(0.3f, 0.8f));
            ReactiveCoroutineSystem.Instance.PublishEvent("Audio_File_Loaded", audioFiles[i]);
        }
 
        context.sceneParameters["AudioLoaded"] = true;
        Debug.Log("Аудио ресурсы загружены");
    }
 
    private IEnumerator InitializeGameSystems(LoadingContext context)
    {
        Debug.Log("Инициализируем игровые системы");
 
        // Последовательная инициализация критических систем
        yield return StartCoroutine(InitializeSystem("GameStateManager", 0.5f));
        yield return StartCoroutine(InitializeSystem("InputManager", 0.3f));
        yield return StartCoroutine(InitializeSystem("AudioManager", 0.4f));
        yield return StartCoroutine(InitializeSystem("UIManager", 0.6f));
 
        context.sceneParameters["SystemsInitialized"] = true;
        Debug.Log("Игровые системы инициализированы");
    }
 
    private IEnumerator InitializeSystem(string systemName, float duration)
    {
        Debug.Log($"Инициализируем {systemName}");
        yield return new WaitForSeconds(duration);
        ReactiveCoroutineSystem.Instance.PublishEvent("System_Initialized", systemName);
    }
 
    private IEnumerator SetupUserInterface(LoadingContext context)
    {
        Debug.Log("Настраиваем пользовательский интерфейс");
 
        // Поиск и инициализация UI компонентов
        var canvases = FindObjectsOfType<Canvas>();
        foreach (var canvas in canvases)
        {
            yield return new WaitForSeconds(0.1f);
            canvas.gameObject.SetActive(true);
        }
 
        yield return new WaitForSeconds(0.5f);
 
        context.sceneParameters["UIReady"] = true;
        ReactiveCoroutineSystem.Instance.PublishEvent("UI_Setup_Complete", null);
    }
 
    private IEnumerator PerformFinalValidation(LoadingContext context)
    {
        Debug.Log("Выполняем финальную валидацию");
 
        // Проверяем что все критические параметры установлены
        string[] requiredParameters = { "PlayerDataLoaded", "AssetsLoaded", "SystemsInitialized", "UIReady" };
 
        foreach (var param in requiredParameters)
        {
            if (!context.sceneParameters.ContainsKey(param) || !(bool)context.sceneParameters[param])
            {
                throw new System.Exception($"Критический параметр {param} не установлен");
            }
            yield return new WaitForSeconds(0.05f);
        }
 
        // Финальная проверка производительности
        float frameTime = Time.deltaTime;
        if (frameTime > 0.05f) // хуже 20 FPS
        {
            Debug.LogWarning($"Низкая производительность после загрузки: {1f/frameTime:F1} FPS");
        }
 
        Debug.Log("Финальная валидация пройдена");
    }
 
    private IEnumerator PlaySuccessAnimation()
    {
        ReactiveCoroutineSystem.Instance.PublishEvent("Play_Success_Animation", null);
        yield return new WaitForSeconds(2f);
    }
 
    private IEnumerator PlayErrorAnimation()
    {
        ReactiveCoroutineSystem.Instance.PublishEvent("Play_Error_Animation", null);
        yield return new WaitForSeconds(1.5f);
    }
 
    private long GetAvailableDiskSpace()
    {
        // Simplified implementation - в реальном проекте нужно использовать platform-specific код
        return 1024L * 1024L * 1024L; // 1GB
    }
}
Эта система демонстрирует комплексное применение всех изученных техник. Фазированная загрузка с проверкой зависимостей обеспечивает надежность, корутины-мониторы отслеживают прогресс и производительность, retry logic восстанавливается после сбоев. Реактивная система событий связывает загрузчик с UI без жестких зависимостей.

Unity Coroutines. Одноразовое выполнение корутины
Здравствуйте, помогите реализовать корутину, чтобы она один раз проигрывалась. Так она...

Про корутины
Доброго времени! public void Start() { StartCoroutine(CoroutineOdin()); ...

Про корутины 2 часть
Приветствую! public void Start() { StartCoroutine(Time()); } public...

Корутины
Есть у меня такой код для движения и анимации персонажа: using System.Collections; using...

Не работают методы внутри корутины
Тут я запускаю корутину с списком команд в роли параметров public void Click3() { string...

Корутины
При запуске корутины она начинает идти паралельно основному процессу. Как сделать так, что бы...

Аналог корутины без параллельного выполнения
Здравствуйте, у меня появилась проблема. Необходимо сделать так, чтобы объект(мы говорим о 2d)...

Корутины ломают мобильную игру
Здравствуйте, товарищи программисты. Перейду сразу к делу. Я делаю мобильный кликер. Поначалу всё...

Как дождаться выполнения корутины?
Я хочу, чтобы json из запроса записался в строчку, затем через getJson взять готовую строку в...

При запуске корутины не сбрасываются локальные переменные
Обычно когда я работал с корутинами, то при каждом запуске локальные переменные равнялись тем...

Неправильная работа корутины
Всем доброго времени суток. Столкнулся со следующей проблемой: у меня есть 2 скрипта, которые...

Странное поведение корутины
Всем привет, на сей раз сложности с корутинами Юнити. Может, я не понимаю чего-то фундаментального...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Был там один разговор по поводу свободы в материальном мире.
kumehtar 19.08.2026
Суть: рассматривается живое существо, оказавшееся внутри довольно странной системы (этого мира) и пытающееся обустроить в ней свой кусок пространства. Жизнь действительно предъявляет каждому. . .
Когда логика программы не спасает от человеческих ошибок
Maks 18.08.2026
В последнее время всё чаще и чаще сталкиваюсь с таким явлением, как абсолютная невнимательность (или глупость) пользователей. Проявляется это чаще всего на работе в коллективе. Допустим, человек с. . .
Лето уходит
kumehtar 17.08.2026
Мысли в слух
kumehtar 17.08.2026
Забавно, насколько сейчас стала доступна информация. Например о магии, духовном развитии, медитациях, и других подобных направлениях, ранее зачастую тайных, передаваемых от учителя к ученику. Хотя. . .
Перемещение строк из ТЧ в другой документ с учетом текущего пробега
Maks 17.08.2026
Реализация из решения ниже выполнена на примере нетипового документа "Автозапчасти", с ТЧ "Шины". За основу взят алгоритм отсюда: https:/ / www. cyberforum. ru/ blogs/ 359708/ 10838. html Задача: . . .
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru