В 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 — данный паттерн значительно упрощает архитектуру и делает код чище и надёжнее.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *