Skip to content

Commit f143dda

Browse files
committed
README _Ru +
1 parent d35ebe9 commit f143dda

File tree

1 file changed

+287
-0
lines changed

1 file changed

+287
-0
lines changed

README_RU.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
## UniLoop
2+
`UniLoop` - библиотека для выполнения C# логики напрямую в игровом цикле Unity (`PlayerLoop`) без использования `MonoBehaviour`.
3+
4+
* Update вне MonoBehaviour — работа в чистых C# классах и сервисах без объектов в сцене
5+
* Zero Allocation — при регистрации, выполнении и завершении процесса
6+
* Max Performance — прямая инъекция в PlayerLoop.
7+
* Безопасность добавления и остановки задач во время итерации цикла
8+
* Выполнение задач строго в порядке их регистрации
9+
* Поддержка CancellationToken.
10+
* IDisposable хэндлы для контроля и управления запущенным процессом
11+
* Безопасность в Editor - автоматическая очистка систем при выходе из PlayMode
12+
* Zero-Alloc State - передача параметров в колбек без замканий и упаковки.
13+
14+
### От автора ( или Зачем это всё?)
15+
<details>
16+
<summary> Развернуть </summary>
17+
Данная библиотека не пытается заменить Update или обогнать его по скорости. Она появилась по простому списку причин:
18+
- Лишние MonoBehaviour: Надоело превращать обычные C# классы в MonoBehaviour или тянуть их внутрь только ради одного цикла.
19+
- Цикл без посредников: Синглтоны в сцене — решение стандартное, но зачем стучаться в игровой цикл через посредника, если есть прямой доступ?
20+
- Удобство управления: UniTask.Yield решает задачу, но им не всегда удобно управлять, да и по эффективности он уступает.
21+
22+
В **Unreal Engine** можно запускать логику в цикле без привязки к объектам — и мне это кажется логичным. В `UniLoop` я постарался совместить подход **без аллокаций** от **UniTask** и удобство **Fluent API** от **DOTween**.
23+
24+
P.S. Внутри библиотеки есть несколько интересных решений, например, специализированные коллекции и работа черех хэндлы. Буду рад, если они пригодятся вам отдельно в ваших проектах. Надеюсь, `UniLoop` немного упростит вам жизнь! :)
25+
</details>
26+
27+
28+
29+
## Оглавление
30+
- [Использование](#использование)
31+
- [Устройство и API](#устройство-и-api)
32+
- [Showcase](#showcase)
33+
- [Продвинутая оптимизация](#продвинутая-оптимизация)
34+
- [Производительность](#производительность)
35+
- [Установка](#установка)
36+
- [Лицензия](#лицензия)
37+
38+
39+
40+
41+
## Использование
42+
```cs
43+
44+
// --- 1. Бесконечный цикл (Loop) ---
45+
// Fluent API: [Timing] -> [Phase] -> [Type] -> [Schedule]
46+
LoopHandle handle = UniLoop.Update.Loop.Schedule(() => Debug.Log("Tick"));
47+
handle.Dispose(); // Остановка процесса через дескриптор (handle)
48+
49+
// Регистрация через универсальный метод Schedule
50+
LoopHandle handle = UniLoop.Schedule(() => Debug.Log("Tick"), PlayerLoopTiming.Update, PlayerLoopPhase.Early);
51+
52+
// Добавление колбэка при остановке процесса
53+
LoopHandle handle = UniLoop.FixedUpdate.Loop.Schedule(() => Debug.Log("Tick"))
54+
.SetOnCanceled(() => Debug.Log("Canceled"));
55+
56+
// Регистрация с отменой по CancellationToken
57+
UniLoop.Update.Loop.Schedule(() => Debug.Log("Tick"), _cts.Token);
58+
_cts.Cancel();
59+
60+
61+
// --- 2. Цикл по условию (While) ---
62+
// Выполняется, пока предикат возвращает true
63+
WhileHandle handle = UniLoop.Update.While.Schedule(() =>
64+
{
65+
_interval -= Time.deltaTime;
66+
return _interval > 0;
67+
});
68+
69+
// Регистрация While с поддержкой событий завершения и отмены
70+
UniLoop.Update.While.Schedule(predicateFunc, _cts.Token)
71+
.SetOnCanceled(() => Debug.Log("Canceled")) // При отмене/Dispose
72+
.SetOnCompleted(() => Debug.Log("Completed")); // Когда условие стало false
73+
74+
// Использование using гарантирует остановку при выходе из области видимости (scope)
75+
using (var handle = UniLoop.Update.Loop.Schedule(() => Debug.Log("Tick")))
76+
{
77+
await DoSomethingAsync();
78+
}
79+
```
80+
81+
82+
83+
## Устройство и API
84+
`UniLoop` — единая точка входа для запуска логики в PlayerLoop через комбинацию **Таймингов**, **Фаз** и **Типов** процессов.
85+
86+
87+
### Типы Процессов:
88+
- **`Loop` - бесконечный цикл.** - Выполняется каждый кадр до ручной остановки или отмены токеном
89+
- **`While` - цикл по условию.** - Выполняется пока предикат возвращает `true`. Также можно отменить или завершить.
90+
91+
### Точки выполнения (Timings & Phases)
92+
Выбор конкретного момента в игровом цикле Unity:
93+
- `PlayerLoopTiming` - выбор игрового цикла: Update, FixedUpdate, LateUpdate
94+
- `PlayerLoopPhase` - в начале или конце цикла: Early/Late
95+
96+
### Способы запуска
97+
98+
- **Fluent API**: — последовательный выбор через цепочку:
99+
```cs
100+
UniLoop.Update.Loop.Schedule(() => ...)
101+
```
102+
- **Универсальный метод `Schedule`** - запуск через передачу параметров `PlayerLoopTiming` и `PlayerLoopPhase`.
103+
104+
### Хэндлы и управление процессами
105+
Каждый запуск возвращает struct handle — легковесный дескриптор для контроля процесса.
106+
- Для бесконечного цикла (Loop)
107+
- `LoopHandle` - хэндл с методом `.Dispose()` для ручной остановки.
108+
- `TokenLoopHandle` - для процессов с `CancellationToken`
109+
Оба варианта поддерживают регистрацию колбэка отмены через `.SetOnCanceled()`
110+
111+
- Для цикла по условию (While)
112+
- `WhileHandle` — хэндл с методом `.Dispose()` для ручной остановки.
113+
- `TokenWhileHandle` - для процессов с `CancellationToken`
114+
Поддержка подписки на окончание процесса по условию через `.SetOnCompleted()`.
115+
116+
117+
118+
## Showcase
119+
Пример контроллера TimeScale, который включает процесс расчёта множителя только при появлении первого модификатора и полностью отключает его, как только список пустеет.
120+
<details>
121+
<summary>Примечание</summary>
122+
Код намеренно упрощен для демонстрации управления жизненным циклом через UniLoop.
123+
</details>
124+
125+
```cs
126+
public class TimeScaleController
127+
{
128+
private readonly List<ITimeScaleModifier> _modifiers = new();
129+
private readonly float originalTimeScale;
130+
private LoopHandle _loopHandle;
131+
132+
public TimeScaleController() => originalTimeScale = Time.timeScale;
133+
134+
public void AddModifier(ITimeScaleModifier modifier)
135+
{
136+
_modifiers.Add(modifier);
137+
138+
// Если это первый модификатор — запускаем логику в цикле
139+
if (_modifiers.Count != 0)
140+
{
141+
_loopHandle = UniLoop.Schedule(UpdateTimeScale, PlayerLoopTiming.Update, PlayerLoopPhase.Early)
142+
.SetOnCanceled(() => Time.timeScale = originalTimeScale);
143+
}
144+
}
145+
146+
public void RemoveModifier(ITimeScaleModifier modifier)
147+
{
148+
if (_modifiers.Remove(modifier) && _modifiers.Count == 0)
149+
{
150+
// Если модификаторов не осталось — останавливаем цикл
151+
_loopHandle.Dispose();
152+
}
153+
}
154+
155+
private void UpdateTimeScale()
156+
{
157+
// Вычисляем среднее значение на основе динамических данных от модификаторов
158+
var averageMultiplier = _modifiers.Average(q => q.Multiplier);
159+
Time.timeScale = originalTimeScale * averageMultiplier;
160+
}
161+
}
162+
```
163+
164+
165+
166+
## Продвинутая оптимизация
167+
### Slim Loop
168+
Облегченная версия цикла. Идеально для глобальных систем, работающих на протяжении всей жизни приложения.
169+
- Самый быстрый способ итерации в PlayerLoop.
170+
- Ограничение: При вызове `Dispose()` внутри выполнения, задача удалится только в следующем кадре.
171+
- Регистрация через `UniLoop.Update.Loop.ScheduleSlim(action)` или `UniLoop.ScheduleSlim(...)`.
172+
173+
### Zero-Alloc & Delegate Caching
174+
Использование кэшированных делегатов и State (структур) исключает создание замыканий и boxing при регистрации процессов для достижения 0B GC Alloc.
175+
176+
``` cs
177+
public sealed class MyClass
178+
{
179+
private readonly struct PoolContext // Контекст как readonly структура
180+
{
181+
public readonly IObjectPool<GameObject> Pool;
182+
public readonly GameObject Entity;
183+
public PoolContext(IObjectPool<GameObject> pool, GameObject entity) => (Pool, Entity) = (pool, entity);
184+
}
185+
186+
// Кэшируем ссылки на методы один раз при инициализации
187+
private readonly Func<bool> _cachedPredicate;
188+
private readonly Action<PoolContext> _cachedOnCompleted;
189+
private IObjectPool<GameObject> _pool;
190+
191+
public MyClass()
192+
{
193+
_cachedPredicate = Predicate;
194+
_cachedOnCompleted = OnCompleted;
195+
}
196+
197+
public void Run()
198+
{
199+
var obj = _pool.Get();
200+
// Передача PoolContext через State — без аллокаций
201+
UniLoop.Update.While.Schedule(_cachedPredicate)
202+
.SetOnCompleted(_cachedOnCompleted, new PoolContext(_pool, obj));
203+
}
204+
205+
private void OnCompleted(PoolContext ctx) => ctx.Pool.Release(ctx.Entity);
206+
private bool Predicate() => /* logic */ true;
207+
}
208+
```
209+
### DeferredDenseArray
210+
Специализированная коллекция, лежащая в основе UniLoop.
211+
- Dense Packing: Все активные задачи хранятся в памяти без «дыр». Несмотря на использование ссылочных типов, это гарантирует строгий порядок выполнения и максимально быструю итерацию по списку.
212+
- Deferred Updates: добавление и удаление задач буферизируются и применяются только в безопасные моменты. Это гарантирует стабильность итерации без ошибок изменения коллекции.
213+
214+
215+
216+
## Производительность
217+
### Бесконечный цикл (Loop)
218+
Сравнение при обработке 10,000 активных задач. За эталон (100%) принят один MonoBehaviour с циклом foreach по HashSet.
219+
| | Время (ms) | Скорость (%) |
220+
|--------------------|-----------:|-------------:|
221+
| **UniLoop (Slim)** | **2.70** | **157%** |
222+
| foreach в Update | 4.23 | 100% |
223+
| **UniLoop (Default)** | **4.36** | **97%** |
224+
| Coroutines | 10.44 | 40% |
225+
| UniTask (Yield) | 23.54 | 18% |
226+
227+
228+
<details>
229+
<summary>Пояснение к тесту</summary>
230+
- `Update`: Использовался MonoBehaviour, хранящий `Action` в `HashSet` (для простоты удаления). Добавление и удаление новых Action также отложенное.
231+
- `Coroutine`: Для каждого процесса запущена корутина с `yield return null`.
232+
- `UniTask`: Бесконечный цикл с использованием `UniTask.Yield`.
233+
</details>
234+
235+
### Цикл по условию (While)
236+
Каждый кадр запускалось по 100 новых процессов (время жизни ~1 сек).
237+
238+
- Регистрация процесса
239+
240+
| | Создание (ms) | Скорость (%) | Alloc (KB) |
241+
|-------------------|--------------:|-------------:|-------------------:|
242+
| foreach в Update | 0.31 | 100% | 28.1 |
243+
| **UniLoop (Default)** | **0.64** | **47%** | **28.1** |
244+
| Coroutine | 0.67 | 45% | 36.7 |
245+
| **UniLoop (NoAlloc)**| **0.88** | **35%** | **0** |
246+
| UniTask (Yield) | 0.94 | 32% | 35.9 |
247+
| UniTaskWaitUntil | 1.1 | 28% | 37.5 |
248+
249+
- Выполнение
250+
251+
| | Выполнение (ms) | Скорость (%) | Alloc (KB) |
252+
|-------------------|-----------------:|-------------:|----------------:|
253+
| foreach в Update | 2.02 | 100% | 0 |
254+
| **UniLoop (Default)** | 2.05 | 99% | 0 |
255+
| **UniLoop (NoAlloc)** | 2.05 | 99% | 0 |
256+
| WaitUntil | 2.12 |95% | 0 |
257+
| Coroutine | 3.18 | 63% | 0 |
258+
| UniTask (Yield) | 6.49 | 31% | 0 |
259+
260+
<details>
261+
<summary>Пояснение к тесту</summary>
262+
Каждому процессу передавался временный объект и два метода: предикат и колбэк завершения для возврат объекта в пул.
263+
`UniLoop` (NoAlloc) поддерживает передачу произвольного состояния (Generic State Support), что исключает замыкания (closures) и упаковку (boxing).
264+
В сочетании с предварительно закэшированными ссылками на методы, это обеспечивает полное отсутствие аллокаций в горячем цикле выполнения.
265+
</details>
266+
267+
### Честный итог
268+
- Скорость: UniLoop сопоставим с нативным циклом и значительно быстрее корутин или UniTask при массовых задачах.
269+
- Регистрация: Требует больше ресурсов CPU, чем ghjcnjt добавление в Hashset. В основном за счёт использования внутренних пулов.
270+
- Что взамен:
271+
- Гарантированный порядок
272+
- Управление через IDisposable или CancellationToken
273+
- Возможность работать полностью без аллокаций
274+
275+
## Установка
276+
277+
Вы можете установить **UniLoop** через Unity Package Manager (UPM), используя Git URL.
278+
279+
1. Откройте окно **Package Manager** (`Window` -> `Package Manager`).
280+
2. Нажмите на иконку **"+"** в левом верхнем углу.
281+
3. Выберите **"Add package from git URL..."**.
282+
4. Вставьте следующую строку:
283+
284+
`https://github.com/CatCodeGames/UniLoop.git?path=Assets/PlayerLoop`
285+
286+
## Лицензия
287+
Этот проект распространяется под лицензией **MIT**.

0 commit comments

Comments
 (0)