ValkarnTaskSettings
ValkarnTaskSettings — это Unity ScriptableObject, управляющий поведением Valkarn Tasks во время выполнения, прежде всего пулом объектов, который переиспользует асинхронные конечные автоматы и promise-объекты, чтобы избежать мусора во время игры.
Создание ресурса настроек
- В окне Project щёлкните правой кнопкой мыши на папке
Resources(создайте её, если не существует). - Выберите Assets > Create > Valkarn Tasks > Task Settings.
- Назовите файл
ValkarnTaskSettingsи поместите его внутрь папкиResources.
Файл должен называться точно ValkarnTaskSettings и располагаться в папке с именем Resources в любом месте вашего проекта. Ресурс загружается во время выполнения с помощью Resources.Load<ValkarnTaskSettings>("ValkarnTaskSettings").
Если ресурс не найден, все настройки возвращаются к встроенным значениям по умолчанию. Библиотека работает корректно без ресурса — его создание необходимо только когда вы хотите изменить значения по умолчанию.
Доступ к настройкам во время выполнения
В сборках Unity настройки читаются из ресурса через кэшированный синглтон:
ValkarnTaskSettings settings = ValkarnTaskSettings.Instance;
Часто нужные параметры пула также открыты непосредственно на ValkarnTask для удобства:
int max = ValkarnTask.DefaultMaxPoolSize; // читает из ValkarnTaskSettings.Instance
int min = ValkarnTask.MinPoolSize;
int interval = ValkarnTask.TrimCheckInterval;
В сборках не-Unity (тесты, автономный .NET) ValkarnTaskSettings является статическим классом с изменяемыми свойствами, а не ScriptableObject. Применяются те же имена свойств, и они могут быть записаны напрямую:
// Только для сборок не-Unity / тестов
ValkarnTask.DefaultMaxPoolSize = 512;
ValkarnTask.TrimCheckInterval = 600;
ValkarnTask.MinPoolSize = 16;
Настраиваемые свойства
Конфигурация пула
DefaultMaxPoolSize
| Тип | int |
| По умолчанию | 256 |
| Допустимый диапазон | 8 – 1024 |
| Подсказка инспектора | "Maximum items per pool type. Excess items are trimmed." |
Максимальное количество объектов, хранимых в каждом пуле по типу. Каждая отдельная обобщённая инстанция (например, PooledPromise<int>, PooledPromise<string>) имеет свой собственный пул с этим ограничением.
Когда задача завершается и её внутренний объект возвращается в пул, если пул уже содержит DefaultMaxPoolSize элементов, возвращаемый объект отбрасывается (может быть собран GC). Это предотвращает неограниченный рост памяти после всплеска асинхронной активности.
Увеличьте это значение, если профилирование показывает частые аллокации GC во время устойчивых высокопроизводительных асинхронных рабочих нагрузок. Уменьшите, если давление на память является проблемой, а задачи не переиспользуются часто.
MinPoolSize
| Тип | int |
| По умолчанию | 8 |
| Допустимый диапазон | 1 – 64 |
| Подсказка инспектора | "Minimum pool size — never shrink below this." |
Проход обрезки пула никогда не уменьшит ни один пул ниже этого значения. Это гарантирует постоянную доступность тёплого базового пула, предотвращая всплески аллокаций после тихого периода, когда проход обрезки мог бы освободить всё.
TrimCheckInterval
| Тип | int |
| По умолчанию | 300 |
| Допустимый диапазон | 30 – 1000 |
| Подсказка инспектора | "Frames between trim checks. At 60fps, 300 ≈ 5 seconds." |
Сколько кадров проходит между проходами обрезки пула. Проход обрезки обходит все зарегистрированные пулы и освобождает избыточные объекты (те, что выше MinPoolSize), если пул постоянно переполнен.
При 60 fps значение по умолчанию 300 равно примерно 5 секундам между проверками. Уменьшите это значение для более агрессивной, частой обрезки (за счёт более частой работы по обрезке). Увеличьте, если проходы обрезки видны как всплески при профилировании.
TrimHysteresisCount
| Тип | int |
| По умолчанию | 2 |
| Допустимый диапазон | 1 – 10 |
| Подсказка инспектора | "Number of consecutive above-threshold checks before trimming." |
Пул обрезается только после того, как он наблюдается переполненным в течение этого количества последовательных циклов обрезки. Это предотвращает пульсацию — если в игре происходит кратковременный всплеск, за которым следует тихий период, счётчик гистерезиса 2 означает, что пул переживёт один тихий цикл прежде, чем начнёт освобождать объекты.
TrimReleaseRatio
| Тип | float |
| По умолчанию | 0.25 |
| Допустимый диапазон | 0.1 – 1.0 |
| Подсказка инспектора | "Fraction of excess to release per trim cycle (0.25 = 25%)." |
При обрезке пула эта доля избыточной ёмкости (элементов выше MinPoolSize) освобождается за цикл, а не всё сразу. Значение 0.25 означает, что каждый проход обрезки удаляет 25% излишка. Это постепенное освобождение предотвращает внезапное падение размера пула, которое могло бы вызвать всплеск аллокаций при восстановлении нагрузки.
Установите в 1.0, если хотите, чтобы весь излишек освобождался немедленно в каждом цикле.
Жизненный цикл
EnableAutoCancel
| Тип | bool |
| По умолчанию | true |
| Подсказка инспектора | "Auto-bind MonoBehaviour tasks to destroyCancellationToken." |
Если включено, задачи, запущенные из MonoBehaviour, автоматически связываются с destroyCancellationToken этого MonoBehaviour. Если MonoBehaviour уничтожается во время выполнения задачи, задача отменяется, а не продолжает выполняться против уничтоженного объекта.
Отключите это только если вы управляете отменой вручную и не хотите автоматической привязки.
Обработка ошибок
LogUnobservedCancellations
| Тип | bool |
| По умолчанию | false |
| Подсказка инспектора | "Log unobserved cancellations as warnings." |
По умолчанию задача, которая отменена, но никогда не ожидалась (необнаруженная отмена), молча игнорируется. Включите это для записи предупреждения при таком событии. Полезно при разработке для поиска fire-and-forget задач, которые молча отменяются.
Необнаруженные ошибки всегда сообщаются через событие ValkarnTask.UnobservedException независимо от этой настройки.
MaxExceptionLogsPerFrame
| Тип | int |
| По умолчанию | 10 |
| Допустимый диапазон | 1 – 100 |
| Подсказка инспектора | "Maximum exception logs per frame to prevent spam." |
Ограничивает количество записей в лог необнаруженных исключений за один кадр. Если многие задачи дают ошибки в одном кадре (например, после сетевого сбоя), это предотвращает переполнение консоли сотнями одинаковых стеков вызовов.
Как работает обрезка пула во время выполнения
При каждом обновлении PlayerLoop PlayerLoopHelper увеличивает счётчик кадров. Когда счётчик достигает TrimCheckInterval, он вызывает PoolRegistry.TrimAll(MinPoolSize). Каждый зарегистрированный пул проверяет, был ли он переполнен как минимум TrimHysteresisCount последовательных проверок. Если да, он освобождает TrimReleaseRatio своего излишка.
Пулы регистрируют себя автоматически при первом использовании. Метод ValkarnTask.GetPoolInfo() возвращает снимок всех зарегистрированных пулов с их типом, текущим размером и максимальным размером — это то, что отображает окно Task Tracker.
Кадр 0 ──────────────────────────────────────────────────────────
PlayerLoop выполняется
Пул A: размер 200, макс 256 — норма, обрезка не нужна
Кадр 300 ────────────────────────────────────────────────────────
TrimAll срабатывает
Пул A: размер 18, макс 256 — выше MinPoolSize(8), но первая проверка излишка
hysteresisCount для A = 1 (ещё не достигнут порог 2)
Кадр 600 ────────────────────────────────────────────────────────
TrimAll срабатывает
Пул A: размер 18, макс 256 — выше MinPoolSize(8), вторая проверка излишка
hysteresisCount для A = 2 — порог достигнут!
Излишек = 18 - 8 = 10; освободить 10 * 0.25 = 2 объекта
Пул A: размер 16
Окно Task Tracker
Откройте через Window > Valkarn Tasks > Task Tracker.
Task Tracker — это окно только для редактора, показывающее живое состояние пулов в режиме Play Mode. Оно обновляется с настраиваемым интервалом (по умолчанию 0.5 секунды, регулируется от 0.1 до 5 секунд через слайдер на панели инструментов).
Вкладка Pools
Перечисляет каждый тип пула, активный с момента последней перезагрузки домена, отсортированный по текущему размеру (самый большой первый). Каждая строка показывает:
| Столбец | Описание |
|---|---|
| Type | Имя типа пулируемого объекта с раскрытыми обобщёнными аргументами |
| Size | Текущее количество объектов в пуле |
| Max | Потолок DefaultMaxPoolSize для этого пула |
| Usage | Полоса прогресса, показывающая Size / Max в процентах |
Если пулы ещё не использовались, отображается сообщение "No pools active. Pools are created on first use."
Вкладка Config
Показывает три живых параметра пула, считанных из ValkarnTask.DefaultMaxPoolSize, ValkarnTask.TrimCheckInterval и ValkarnTask.MinPoolSize. Значения отображаются только в режиме Play Mode — в режиме Edit Mode отображается заметка с предложением войти в Play Mode для просмотра живых значений.
Вкладка Config также отображает ссылку на ресурс ValkarnTaskSettings (если он существует в Resources), так что вы можете перейти к нему для инспекции или изменения. Если ресурс не найден, предупреждение предлагает создать его.
Result<T>
Result<T> — это структура-дискриминированный союз для представления результата операции без бросания исключений. Это тип возврата, используемый комбинаторами WhenAll для сообщения результатов по каждой задаче, а также доступный как паттерн результата общего назначения.
public readonly struct Result<T>
{
public bool IsSuccess { get; }
public bool IsFailure { get; } // true при ошибке или отмене
public bool IsFaulted { get; }
public bool IsCanceled { get; }
public ValkarnTask.Status Status { get; }
public T Value { get; } // бросает если не succeeded
public Exception Error { get; } // null если не faulted
public static Result<T> Success(T value);
public static Result<T> Failure(string error); // оборачивает в InvalidOperationException
public static Result<T> Faulted(Exception error);
public static Result<T> Canceled(OperationCanceledException oce = null);
public static implicit operator bool(Result<T> r); // true если succeeded
}
Необобщённый Result существует для void-задач с той же формой, но без Value.
Методы расширения AsResult
Преобразуйте любой ValkarnTask или ValkarnTask<T> в Result без try/catch в вашем коде:
public static ValkarnTask<Result<T>> AsResult<T>(this ValkarnTask<T> task);
public static ValkarnTask<Result> AsResult(this ValkarnTask task);
Если базовая задача уже синхронно завершена (быстрый путь без аллокаций), AsResult возвращает немедленно без создания асинхронного конечного автомата. В противном случае он оборачивается в async-метод, перехватывающий OperationCanceledException и все другие исключения, переводя их в соответствующий вариант Result.
Когда использовать Result<T>
Используйте Result<T> вместо перехвата исключений, когда:
- Вы вызываете несколько задач параллельно и хотите результаты по каждой задаче без прерывания всей партии.
- Вы хотите выразить операции с возможными ошибками типобезопасным способом без управления потоком через исключения.
- Вы возвращаете из метода, который вызывающий, возможно, не хочет оборачивать в
try/catch.
// Запустить несколько задач, получить все результаты независимо от индивидуальных ошибок
Result<int>[] results = await ValkarnTask.WhenAll(
FetchScoreAsync(playerA).AsResult(),
FetchScoreAsync(playerB).AsResult(),
FetchScoreAsync(playerC).AsResult()
);
foreach (var r in results)
{
if (r.IsSuccess)
Debug.Log($"Score: {r.Value}");
else if (r.IsFaulted)
Debug.LogError($"Failed: {r.Error.Message}");
else
Debug.Log("Canceled");
}
Проверка IsSuccess или использование неявного оператора bool — предпочтительные способы ветвления по результату:
var result = await SomeOperationAsync().AsResult();
if (result)
{
Use(result.Value);
}
Обращение к result.Value когда IsSuccess равен false бросает InvalidOperationException.
Фабричные методы
| Метод | Устанавливаемый статус | Устанавливаемая ошибка |
|---|---|---|
Result<T>.Success(value) | Succeeded | нет |
Result<T>.Failure(message) | Faulted | new InvalidOperationException(message) |
Result<T>.Faulted(exception) | Faulted | предоставленное исключение |
Result<T>.Canceled(oce?) | Canceled | предоставленный OperationCanceledException, или null |
Свойство Succeeded существует как на Result, так и на Result<T>, но помечено [Obsolete] — вместо него используйте IsSuccess для единообразия с IsFailure, IsFaulted и IsCanceled.