В Unity мы привыкли использовать корутины (StartCoroutine) для реализации таймеров и отложенных вызовов. Однако этот подход требует наличия MonoBehaviour, что не всегда удобно. Особенно это становится проблемой, когда логика располагается в static-классах, SDK, менеджерах инициализации, аналитике, рекламе и прочих системных модулях.
В этой заметке разберём, как реализовать таймеры, отменяемые таймеры и повторяющиеся таймеры в static-коде без корутин и без сторонних библиотек, используя только стандартные возможности C# и Unity.
Почему не корутины?
Корутины в Unity:
- требуют
MonoBehaviour - зависят от сцены и GameObject
- сложны для static-кода
- плохо подходят для SDK и инициализации
Пример типичной проблемы:
public static class SomeManager
{
public static void Init()
{
// Тут нельзя вызвать StartCoroutine
}
}Лучшее решение — async/await + Task.Delay
Unity корректно работает с async/await и сохраняет Unity main thread после await, поэтому мы можем безопасно обращаться к Unity API.
Простейший таймер
using System.Threading.Tasks;
using UnityEngine;
public static class StaticTimer
{
public static async void Delay(float seconds, System.Action callback)
{
await Task.Delay((int)(seconds * 1000));
callback?.Invoke();
}
}
Использование
StaticTimer.Delay(5f, () =>
{
Debug.Log("5 seconds passed");
});Отменяемый таймер (CancellationToken)
Часто возникает необходимость отменять таймер, например при закрытии экрана, уничтожении объекта или прерывании инициализации.
using System.Threading;
using System.Threading.Tasks;
public static class StaticTimer
{
public static async Task Delay(float seconds, CancellationToken token)
{
await Task.Delay((int)(seconds * 1000), token);
}
}Использование
CancellationTokenSource _cts;
async void StartTimer()
{
_cts = new CancellationTokenSource();
try
{
await StaticTimer.Delay(5f, _cts.Token);
Debug.Log("Timer fired");
}
catch (TaskCanceledException)
{
Debug.Log("Timer canceled");
}
}
void Cancel()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
Повторяющийся таймер (интервал)
Полный аналог InvokeRepeating и циклических корутин.
using System;
using System.Threading;
using System.Threading.Tasks;
public static class StaticTimer
{
public static async Task Repeat(
float intervalSeconds,
Action callback,
CancellationToken token)
{
int delay = (int)(intervalSeconds * 1000);
while (!token.IsCancellationRequested)
{
await Task.Delay(delay, token);
if (!token.IsCancellationRequested)
callback?.Invoke();
}
}
}Использование
CancellationTokenSource _loopCts;
void StartLoop()
{
_loopCts = new CancellationTokenSource();
StaticTimer.Repeat(1f, () =>
{
Debug.Log("Tick");
}, _loopCts.Token);
}
void StopLoop()
{
_loopCts?.Cancel();
}TimerManager — несколько таймеров в static-классе
Очень полезный паттерн для SDK, аналитики, рекламы и инициализации.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public static class TimerManager
{
private static readonly Dictionary<string, CancellationTokenSource> _timers =
new Dictionary<string, CancellationTokenSource>();
public static async void Start(string id, float delaySeconds, Action callback)
{
Stop(id);
var cts = new CancellationTokenSource();
_timers[id] = cts;
try
{
await Task.Delay((int)(delaySeconds * 1000), cts.Token);
if (!cts.IsCancellationRequested)
callback?.Invoke();
}
catch (TaskCanceledException) { }
}
public static void Stop(string id)
{
if (_timers.TryGetValue(id, out var cts))
{
cts.Cancel();
cts.Dispose();
_timers.Remove(id);
}
}
public static void StopAll()
{
foreach (var kv in _timers)
{
kv.Value.Cancel();
kv.Value.Dispose();
}
_timers.Clear();
}
}Использование
TimerManager.Start("app_open", 5f, ShowAppOpenAd);
TimerManager.Start("analytics", 10f, SendAnalytics);
TimerManager.Stop("app_open");
TimerManager.StopAll();Аналог InvokeRepeating с задержкой первого запуска
public static async Task RepeatDelayed(
float firstDelay,
float interval,
Action callback,
CancellationToken token)
{
await Task.Delay((int)(firstDelay * 1000), token);
while (!token.IsCancellationRequested)
{
callback?.Invoke();
await Task.Delay((int)(interval * 1000), token);
}
}Важные нюансы
Unity main thread сохраняется
await Task.Delay(1000);
Debug.Log("Unity API safe");После await код продолжает выполняться в основном Unity-потоке.
Нельзя блокировать поток
Task.Delay(5000).Wait(); // ЗАМОРОЗИТ UnityИспользуйте только await.
async void — только для entry point
Допустимо:
async void Init()Недопустимо:
async void SomeLogic() // должен быть TaskИтоги
Используя async/await + Task.Delay, мы получаем:
- простые таймеры
- отменяемые таймеры
- повторяющиеся таймеры
- управление множеством таймеров
без:
- корутин
- MonoBehaviour
- сторонних библиотек
Этот подход отлично подходит для SDK, рекламы, аналитики, инициализации и системных модулей.
Если вы активно используете static-код в Unity — данный паттерн значительно упрощает архитектуру и делает код чище и надёжнее.