UniLoop - библиотека для выполнения C# логики напрямую в игровом цикле Unity (PlayerLoop) без использования MonoBehaviour.
- Update вне MonoBehaviour — работа в чистых C# классах и сервисах без объектов в сцене
- Zero Allocation — при регистрации, выполнении и завершении процесса
- Отсутствие «пиков» нагрузки на всех этапах жизненного цикла задач
- Max Performance — прямая инъекция в PlayerLoop
- Безопасность добавления и остановки задач во время итерации цикла
- Выполнение задач строго в порядке их регистрации
- Поддержка CancellationToken.
- IDisposable хэндлы для контроля и управления запущенным процессом
- Безопасность в Editor - автоматическая очистка систем при выходе из PlayMode
- Zero-Alloc State - передача параметров в колбек без замканий и упаковки
Развернуть
Данная библиотека не пытается заменить Update или обогнать его по скорости. Она появилась по простому списку причин:
- Лишние MonoBehaviour: Надоело превращать обычные C# классы в MonoBehaviour или тянуть их внутрь только ради одного цикла.
- Цикл без посредников: Синглтоны в сцене — решение стандартное, но зачем стучаться в игровой цикл через посредника, если есть прямой доступ?
- Удобство управления: UniTask.Yield решает задачу, но им не всегда удобно управлять, да и по эффективности он уступает.
В Unreal Engine можно запускать логику в цикле без привязки к объектам — и мне это кажется логичным. В UniLoop я постарался совместить подход без аллокаций от UniTask и удобство Fluent API от DOTween.
P.S. Внутри библиотеки есть несколько интересных решений, например, специализированные коллекции и работа через хэндлы. Буду рад, если они пригодятся вам отдельно в ваших проектах. Надеюсь, UniLoop немного упростит вам жизнь! :)
- Использование
- Устройство и API
- Showcase
- Продвинутая оптимизация
- Производительность
- Установка
- Лицензия
// --- 1. Бесконечный цикл (Loop) ---
// Fluent API: [Timing] -> [Phase] -> [Type] -> [Schedule]
LoopHandle handle = UniLoop.Update.Loop.Schedule(() => Debug.Log("Tick"));
handle.Dispose(); // Остановка процесса через дескриптор (handle)
// Регистрация через универсальный метод Schedule
LoopHandle handle = UniLoop.Schedule(() => Debug.Log("Tick"), PlayerLoopTiming.Update, PlayerLoopPhase.Early);
// Добавление колбэка при остановке процесса
LoopHandle handle = UniLoop.FixedUpdate.Loop.Schedule(() => Debug.Log("Tick"))
.SetOnCanceled(() => Debug.Log("Canceled"));
// Регистрация с отменой по CancellationToken
UniLoop.Update.Loop.Schedule(() => Debug.Log("Tick"), _cts.Token);
_cts.Cancel();
// --- 2. Цикл по условию (While) ---
// Выполняется, пока предикат возвращает true
WhileHandle handle = UniLoop.Update.While.Schedule(() =>
{
_interval -= Time.deltaTime;
return _interval > 0;
});
// Регистрация While с поддержкой событий завершения и отмены
UniLoop.Update.While.Schedule(predicateFunc, _cts.Token)
.SetOnCanceled(() => Debug.Log("Canceled")) // При отмене/Dispose
.SetOnCompleted(() => Debug.Log("Completed")); // Когда условие стало false
// Использование using гарантирует остановку при выходе из области видимости (scope)
using (var handle = UniLoop.Update.Loop.Schedule(() => Debug.Log("Tick")))
{
await DoSomethingAsync();
}
UniLoop — единая точка входа для запуска логики в PlayerLoop через комбинацию Таймингов, Фаз и Типов процессов.
Loop- бесконечный цикл. - Выполняется каждый кадр до ручной остановки или отмены токеномWhile- цикл по условию. - Выполняется пока предикат возвращаетtrue. Также можно отменить или завершить.
Выбор конкретного момента в игровом цикле Unity:
PlayerLoopTiming- выбор игрового цикла: Update, FixedUpdate, LateUpdatePlayerLoopPhase- в начале или конце цикла: Early/Late
Очередность выполнения типов операций в пределах одной фазы:
- Slim Loop - Легкий цикл. В отличие от остальных типов, модификация коллекции (добавление/удаление) во время итерации не безопасна.
- Loop - Бесконечный цикл.
- Loop (with CancellationToken) - Бесконечный цикл с поддержкой CancellationToken.
- While - Цикл по условию.
- While (with CancellationToken) - Цикл по условию с поддержкой CancellationToken.
- Fluent API: — последовательный выбор через цепочку:
UniLoop.Update.Loop.Schedule(() => ...)
- Универсальный метод
Schedule- запуск через передачу параметровPlayerLoopTimingиPlayerLoopPhase.
Каждый запуск возвращает struct handle — легковесный дескриптор для контроля процесса.
-
Для бесконечного цикла (Loop)
LoopHandle- хэндл с методом.Dispose()для ручной остановки.TokenLoopHandle- для процессов сCancellationTokenОба варианта поддерживают регистрацию колбэка отмены через.SetOnCanceled()
-
Для цикла по условию (While)
WhileHandle— хэндл с методом.Dispose()для ручной остановки.TokenWhileHandle- для процессов сCancellationTokenПоддержка подписки на окончание процесса по условию через.SetOnCompleted().
Пример контроллера TimeScale, который включает процесс расчёта множителя только при появлении первого модификатора и полностью отключает его, как только список пустеет.
Примечание
Код намеренно упрощен для демонстрации управления жизненным циклом через UniLoop.public class TimeScaleController
{
private readonly List<ITimeScaleModifier> _modifiers = new();
private readonly float originalTimeScale;
private LoopHandle _loopHandle;
public TimeScaleController() => originalTimeScale = Time.timeScale;
public void AddModifier(ITimeScaleModifier modifier)
{
_modifiers.Add(modifier);
// Если это первый модификатор — запускаем логику в цикле
if (_modifiers.Count != 0)
{
_loopHandle = UniLoop.Schedule(UpdateTimeScale, PlayerLoopTiming.Update, PlayerLoopPhase.Early)
.SetOnCanceled(() => Time.timeScale = originalTimeScale);
}
}
public void RemoveModifier(ITimeScaleModifier modifier)
{
if (_modifiers.Remove(modifier) && _modifiers.Count == 0)
{
// Если модификаторов не осталось — останавливаем цикл
_loopHandle.Dispose();
}
}
private void UpdateTimeScale()
{
// Вычисляем среднее значение на основе динамических данных от модификаторов
var averageMultiplier = _modifiers.Average(q => q.Multiplier);
Time.timeScale = originalTimeScale * averageMultiplier;
}
}
Облегченная версия цикла. Идеально для глобальных систем, работающих на протяжении всей жизни приложения.
- Самый быстрый способ итерации в PlayerLoop.
- Ограничение: При вызове
Dispose()внутри выполнения, задача удалится только в следующем кадре. - Регистрация через
UniLoop.Update.Loop.ScheduleSlim(action)илиUniLoop.ScheduleSlim(...).
Использование кэшированных делегатов и State (структур) исключает создание замыканий и boxing при регистрации процессов для достижения 0B GC Alloc.
public sealed class MyClass
{
private readonly struct PoolContext // Контекст как readonly структура
{
public readonly IObjectPool<GameObject> Pool;
public readonly GameObject Entity;
public PoolContext(IObjectPool<GameObject> pool, GameObject entity) => (Pool, Entity) = (pool, entity);
}
// Кэшируем ссылки на методы один раз при инициализации
private readonly Func<bool> _cachedPredicate;
private readonly Action<PoolContext> _cachedOnCompleted;
private IObjectPool<GameObject> _pool;
public MyClass()
{
_cachedPredicate = Predicate;
_cachedOnCompleted = OnCompleted;
}
public void Run()
{
var obj = _pool.Get();
// Передача PoolContext через State — без аллокаций
UniLoop.Update.While.Schedule(_cachedPredicate)
.SetOnCompleted(_cachedOnCompleted, new PoolContext(_pool, obj));
}
private void OnCompleted(PoolContext ctx) => ctx.Pool.Release(ctx.Entity);
private bool Predicate() => /* logic */ true;
}
Специализированная коллекция, лежащая в основе UniLoop.
- Dense Packing: Все активные задачи хранятся в памяти без «дыр». Несмотря на использование ссылочных типов, это гарантирует строгий порядок выполнения и максимально быструю итерацию по списку.
- Deferred Updates: добавление и удаление задач буферизируются и применяются только в безопасные моменты. Это гарантирует стабильность итерации без ошибок изменения коллекции.
Сценарии с постоянным созданием и завершением задач каждый кадр (конвейерная нагрузка).
| Метод | Запуск (ms) | Выполнение (ms) | Отмена (ms) | GC Alloc |
|---|---|---|---|---|
| UniLoop (No Alloc) | 0.10 | 0.55 | 4.31 | 0 B |
| UniLoop (Default) | 0.11 | 0.58 | 3.59 | 28.1 KB |
| Update (Baseline) | 0.07 | 0.59 | 3.25 | 28.1 KB |
| UniTask.Yield | 0.15 | 2.62 | 2.62 | 35.9 KB |
| Coroutine | 0.34 | 6.04 | 6.04 | 36.7 KB |
| UniTask.WaitUntil | 0.17 | 0.85 | 85.70 | 4.5 MB |
| Метод | Запуск (ms) | Выполнение (ms) | Отмена (ms) | GC Alloc |
|---|---|---|---|---|
| UniLoop (No Alloc) | 0.73 | 0.93 | 4.50 | 0 B |
| UniLoop (Default) | 0.80 | 0.93 | 3.29 | 281.2 KB |
| Update (Baseline) | 0.55 | 1.44 | 3.49 | 281.2 KB |
| UniTask.Yield | 1.43 | 3.17 | 3.17 | 359.4 KB |
| Coroutine | 2.23 | 6.17 | 6.17 | 367.2 KB |
| UniTask.WaitUntil | 1.70 | 2.21 | 90.02 | 4.9 MB |
Важно: Обратите внимание на
UniTask.WaitUntil. При массовой отмене задач он генерирует лавину исключений, что приводит к критическим скачкам аллокаций (4.9 MB) и фризам (90 ms). UniLoop в режиме No Alloc сохраняет стабильный FPS и нулевое выделение памяти.
- Регистрация — по затратам CPU сопоставима с добавлением в стандартные коллекции и запуском UniTask. Поддержка режима No Alloc позволяет полностью исключить нагрузку на память при создании задач.
- Выполнение — в динамических сценариях (постоянный приток и завершение задач) превосходит стандартную итерацию (foreach) и асинхронные методы. Использование Slim Loop позволяет сократить время выполнения в несколько раз относительно Update.
- Завершение и отмена — минимальное влияние на производительность кадра. В отличие от асинхронных решений, система сохраняет стабильный FPS и отсутствие аллокаций даже при одновременной остановке тысяч задач.
####Ключевые преимущества:
- Гарантированный порядок выполнения всех операций в кадре.
- Поддержка Zero-Alloc: возможность работы без аллокаций на протяжении всего жизненного цикла задачи.
- Гибкий контроль через IDisposable хэндлы или CancellationToken.
- Отсутствие «пиков» нагрузки на всех этапах жизненного цикла задач.
Вы можете установить UniLoop через Unity Package Manager (UPM), используя Git URL.
- Откройте окно Package Manager (
Window->Package Manager). - Нажмите на иконку "+" в левом верхнем углу.
- Выберите "Add package from git URL...".
- Вставьте следующую строку:
https://github.com/CatCodeGames/UniLoop.git?path=Assets/PlayerLoop
Этот проект распространяется под лицензией MIT.