Форум программистов, компьютерный форум, киберфорум
GameUnited
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Реализации таймеров в Unity

Запись от GameUnited размещена 09.04.2025 в 18:20
Показов 5807 Комментарии 0
Метки .net, c#, unity

Нажмите на изображение для увеличения
Название: 15c194a9-0fdf-4a93-a24c-b7e882546f62.jpg
Просмотров: 301
Размер:	150.3 Кб
ID:	10564
Время — важный ресурс любой игры. Разработка качественных игровых механик невозможна без грамотного управления временем, а таймеры выступают ключевым инструментом этого управления. Представьте себе мир без кулдаунов способностей, без точного определения времени прохождения уровня, без корректного спавна противников — игровой опыт превратился бы в хаос. Таймеры создают структуру, которая заставляет игрока ждать, планировать, действовать и реагировать. От способности разработчика грамотно использовать механику времени зависит насколько затягивающим будет геймплей, насколько читаемым будет интерфейс, насколько справедливой будет соревновательная составляющая.

В Unity существует множество подходов к созданию таймеров. От примитивных решений с использованием Update() и счётчиков до сложных систем асинхронного управления временем. Эволюци этих методов шла бок о бок с развитием самого движка — от простой проверки "прошло ли достаточно времени?" в ранних версиях, до сложных асинхронных конструкций, использующих возможности современного C#.

Точность таймеров критически важна в определённых ситуациях. В многопользовательских соревновательных играх расхождение даже в десятые доли секунды может стать причиной фрустрации игроков. В жанре файтингов или платформеров, где важны фреймы, неточные таймеры способны разрушить игровой баланс. Системы партиклов, анимации, цепочки событий — все эти элементы зависят от корректной работы временных механизмов.

Интересно, что восприятие времени игроками субъективно и зависит от жанра. Пять секунд ожидания в динамичном шутере воспринимаются иначе чем те же пять секунд в пошаговой стратегии. Разработчики часто манипулируют этим восприятием, делая, например, таймеры перезарядки визуально быстрее в середине процесса, что психологически облегчает ожидание.

При использовании стандартных подходов к созданию таймеров разработчики сталкиваются с рядом проблем:
  1. Некорректная работа при изменении масштаба времени (Time.timeScale).
  2. Сложности с паузой и возобновлением работы таймеров.
  3. Накопление погрешностей при работе с плавающей точкой.
  4. Отсутствие удобных механизмов для создания повторяющихся таймеров.
  5. Проблемы с производительностью при большом количестве параллельных таймеров.
  6. Сложности интеграции с существующими архитектурами кода.

Хорошая реализация таймера должна решать эти проблемы, обеспечивая баланс между простотой использования и возможностями. Создать такую систему не всегда просто, особенно когда проект растёт в размерах и сложности. Современные подходы к реализации таймеров выходят за рамки просто вычитания Time.deltaTime из заданной длительности. Они включают в себя интеграцию с системами событий, применение паттернов проектирования и использование возможностей асинхронного программирования для создания элегантного и поддерживаемого кода.

Базовые методы реализации



Основа любого таймера в Unity – корректное измерение прошедшего времени. Существует несколько базовых подходов, каждый со своими преимуществами и недостатками. Начнём с простейшего и самого распространённого метода – использования функции Update(). В Unity метод Update() вызывается каждый кадр, что делает его естественным местом для обновления таймеров. Простейшая реализация таймера выглядит примерно так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private float timer = 0f;
private float duration = 3f;
private bool isComplete = false;
 
void Update()
{
    if (!isComplete)
    {
        timer += Time.deltaTime;
        if (timer >= duration)
        {
            isComplete = true;
            Debug.Log("Таймер завершился!");
        }
    }
}
Однако у этого подхода есть существенный недостаток – накопление погрешностей. При работе с числами с плавающей точкой многократное сложение мелких значений приводит к неточностям. Более надёжный подход – запоминать стартовое время и сравнивать текущее время с ожидаемым временем завершения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private float startTime;
private float duration = 3f;
private bool isRunning = false;
 
void Start()
{
    StartTimer();
}
 
void StartTimer()
{
    startTime = Time.time;
    isRunning = true;
}
 
void Update()
{
    if (isRunning && Time.time >= startTime + duration)
    {
        isRunning = false;
        Debug.Log("Таймер завершился!");
    }
}
Этот метод позволяет избежать накопления ошибок при продолжительной работе таймера.
Для более структурированного подхода можно реализовать минимальный таймер в виде структуры. Такой подход позволяет создавать лёгкие, не создающие мусора в памяти таймеры:

C#
1
2
3
4
5
6
7
8
public readonly struct MinimalTimer
{
    public static MinimalTimer Start(float duration) => new MinimalTimer(duration);
    public bool IsCompleted => Time.time >= _triggerTime && _triggerTime != 0;
 
    private readonly float _triggerTime;
    private MinimalTimer(float duration) => _triggerTime = Time.time + duration;
}
Использование такой структуры предельно просто:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private MinimalTimer myTimer;
 
void Start()
{
    myTimer = MinimalTimer.Start(3f);
}
 
void Update()
{
    if (myTimer.IsCompleted)
    {
        Debug.Log("Таймер завершился!");
    }
}
Если таймер нужен для физических вычислений, стоит использовать FixedUpdate() вместо Update(). Метод FixedUpdate() вызывается через фиксированные промежутки времени, что делает его более предсказуемым для физики и анимаций, зависящих от точных временных интервалов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
private float fixedTimer = 0f;
private float fixedDuration = 1f;
 
void FixedUpdate()
{
    fixedTimer += Time.fixedDeltaTime;
    if (fixedTimer >= fixedDuration)
    {
        // Выполнение физических действий
        fixedTimer = 0f; // Сброс для повторения
    }
}
Time.deltaTime – ключевой компонент для создания плавных анимаций и движений, не зависящих от частоты кадров. При использовании deltaTime объекты перемещаются на одинаковое расстояние за одинаковое время независимо от производительности устройства:

C#
1
2
3
4
5
void Update()
{
    float moveSpeed = 5f;
    transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
}
Тот же принцип применяется при реализации более сложных таймеров. Например, для создания постепенного заполнения полосы прогресса:

C#
1
2
3
4
5
6
7
8
9
10
11
private float fillAmount = 0f;
private float fillDuration = 2f;
 
void Update()
{
    if (fillAmount < 1f)
    {
        fillAmount += Time.deltaTime / fillDuration;
        progressBar.fillAmount = Mathf.Clamp01(fillAmount);
    }
}
Для обработки событий завершения таймера эффективно использовать паттерн наблюдателя (Observer Pattern). Этот паттерн позволяет различным частям программы реагировать на изменение состояния таймера без прямой зависимости:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SimpleTimer
{
    public event Action OnTimerCompleted;
    public float Duration { get; private set; }
    public bool IsRunning { get; private set; }
    
    private float startTime;
    
    public void Start(float duration)
    {
        Duration = duration;
        startTime = Time.time;
        IsRunning = true;
    }
    
    public void Update()
    {
        if (IsRunning && Time.time >= startTime + Duration)
        {
            IsRunning = false;
            OnTimerCompleted?.Invoke();
        }
    }
}
Использование этого класса позволяет легко подписываться на события таймера:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private SimpleTimer cooldownTimer = new SimpleTimer();
 
void Start()
{
    cooldownTimer.OnTimerCompleted += HandleCooldownComplete;
    cooldownTimer.Start(5f);
}
 
void Update()
{
    cooldownTimer.Update();
}
 
void HandleCooldownComplete()
{
    Debug.Log("Способность снова доступна!");
}
Корутины (Coroutines) представляют собой еще один мощный инструмент для реализации таймеров в Unity. В отличие от методов Update(), корутины позволяют выполнять код через определенные промежутки времени без необходимости отслеживания состояния в каждом кадре:

C#
1
2
3
4
5
6
7
8
9
10
private IEnumerator SimpleTimerCoroutine(float duration)
{
    yield return new WaitForSeconds(duration);
    Debug.Log("Корутина-таймер завершилась!");
}
 
void Start()
{
    StartCoroutine(SimpleTimerCoroutine(3f));
}
Преимущество этого подхода в его лаконичности и читаемости. Однако корутины имеют свои ограничения. Они привязаны к GameObject, на котором вызываются, и будут прерваны, если объект деактивирован или уничтожен. Кроме того, корутины сложнее остановить или перезапустить, чем обычные таймеры:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Coroutine timerCoroutine;
 
void StartMyTimer(float duration)
{
    // Останавливаем предыдущую корутину, если она существует
    if (timerCoroutine != null)
        StopCoroutine(timerCoroutine);
    
    // Запускаем новую
    timerCoroutine = StartCoroutine(SimpleTimerCoroutine(duration));
}
 
void StopMyTimer()
{
    if (timerCoroutine != null)
    {
        StopCoroutine(timerCoroutine);
        timerCoroutine = null;
    }
}
Корутины удобны для создания последовательностей действий с задержками. Например, можно организовать цепочку событий, происходящих одно за другим:

C#
1
2
3
4
5
6
7
8
9
10
11
private IEnumerator SequentialEvents()
{
    Debug.Log("Начало последовательности");
    yield return new WaitForSeconds(1f);
    
    Debug.Log("Прошла 1 секунда");
    yield return new WaitForSeconds(2f);
    
    Debug.Log("Прошло еще 2 секунды");
    // Выполнение действия в конце цепочки
}
Существуют различные типы ожидания в корутинах, которые можно использовать для разных сценариев:

C#
1
2
3
4
5
6
7
8
9
10
11
// Ожидание фиксированного времени (независимо от Time.timeScale)
yield return new WaitForSecondsRealtime(1.5f);
 
// Ожидание до конца кадра
yield return new WaitForEndOfFrame();
 
// Ожидание до следующего FixedUpdate
yield return new WaitForFixedUpdate();
 
// Ожидание, пока условие не станет истинным
yield return new WaitUntil(() => someCondition == true);
Для более гибкого управления таймерами можно создать класс с расширенной функциональностью. В исходной статье представлен пример такого класса, который мы модифицируем для наших целей:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class UpdateTimer
{
    public float Duration { get; private set; }
    public int Repeats { get; private set; }
    public bool IsActive { get; private set; }
    public float ElapsedTime => TimeRunning();
    public float TimeRemaining => _triggerTime + _pausedDuration - Time.time;
    public bool IsCompleted { get; private set; }
    public int CompletionsCount { get; private set; }
    public event Action CompletedEvent;
 
    private float _triggerTime;
    private float _pausedTime;
    private float _pausedDuration;
    private float _startTime;
    private bool _hasStarted;
    private bool _loop;
 
    public void Start(float duration) => Start(duration, 1, false);
    public void Start(float duration, bool loop) => Start(duration, 0, true);
    public void Start(float duration, int repeats) => Start(duration, repeats, false);
 
    public void Stop()
    {
        // Сброс всех параметров кроме подписчиков
        Duration = default;
        Repeats = default;
        IsActive = default;
        IsCompleted = default;
        CompletionsCount = default;
        _triggerTime = default;
        _pausedTime = default;
        _pausedDuration = default;
        _startTime = default;
        _hasStarted = default;
        _loop = default;
    }
 
    public void Pause()
    {
        if (!IsActive) return;
        
        IsActive = false;
        _pausedTime = Time.time;
    }
 
    public void UnPause()
    {
        if (IsActive || !_hasStarted) return;
        
        IsActive = true;
        _pausedDuration += Time.time - _pausedTime;
    }
 
    public void Tick()
    {
        if (IsDurationExpired() && IsActive)
        {
            IsCompleted = true;
            CompletionsCount++;
            CompletedEvent?.Invoke();
 
            if (CompletionsCount < Repeats || _loop)
                _triggerTime += Duration;
            else
                IsActive = false;
        }
    }
 
    private void Start(float duration, int repeats, bool loop)
    {
        if (_hasStarted)
        {
            Debug.LogWarning("Таймер уже запущен! Остановите его перед запуском нового.");
            return;
        }
        
        _loop = loop;
        Duration = duration;
        Repeats = repeats;
        Init();
    }
 
    private void Init()
    {
        IsActive = true;
        _startTime = Time.time;
        _hasStarted = true;
        _triggerTime = _startTime + Duration;
        Tick();
    }
 
    private bool IsDurationExpired() => Time.time >= _triggerTime + _pausedDuration;
    private float TimeRunning() => _hasStarted ? Time.time - _startTime - _pausedDuration : 0;
}
Этот таймер обладает рядом преимуществ:
  • Возможность паузы и возобновления.
  • Поддержка повторений (как фиксированного количества, так и бесконечного цикла).
  • Получение информации о прошедшем и оставшемся времени.
  • Событие по завершении таймера.
  • Отсутствие накопления погрешностей благодаря сравнению абсолютных временных меток.

Для использования этого таймера необходимо вызывать метод Tick() в Update():

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private UpdateTimer cooldownTimer = new UpdateTimer();
 
void Start()
{
    cooldownTimer.CompletedEvent += OnCooldownComplete;
    cooldownTimer.Start(5f, 3); // 5 секунд, повторить 3 раза
}
 
void Update()
{
    cooldownTimer.Tick();
    
    // Отображение прогресса, например
    if (cooldownTimer.IsActive)
    {
        float progress = 1f - (cooldownTimer.TimeRemaining / cooldownTimer.Duration);
        progressBar.fillAmount = Mathf.Clamp01(progress);
    }
}
 
void OnCooldownComplete()
{
    Debug.Log($"Завершение #{cooldownTimer.CompletionsCount}");
}
Еще один интересный подход — создание расширений для класса MonoBehaviour. Это позволяет добавить методы таймера к любому классу, наследующемуся от MonoBehaviour:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public static class MonoBehaviourExtensions
{
    public static Coroutine DelayedAction(this MonoBehaviour behaviour, float delay, Action action)
    {
        return behaviour.StartCoroutine(DelayedActionCoroutine(delay, action));
    }
 
    private static IEnumerator DelayedActionCoroutine(float delay, Action action)
    {
        yield return new WaitForSeconds(delay);
        action?.Invoke();
    }
}
Использовать такие расширения очень просто:

C#
1
2
3
4
5
6
7
8
9
10
void Start()
{
    // Вызывает метод ShowMessage через 2 секунды
    this.DelayedAction(2f, ShowMessage);
}
 
void ShowMessage()
{
    Debug.Log("Сообщение с задержкой");
}
При выборе способа реализации таймера следует учитывать конкретные требования проекта. Для простых сценариев достаточно базовых таймеров на Update() или корутин. Для более сложных случаев с множеством таймеров, которые требуют управления паузой, масштабированием времени и другой функциональностью, стоит рассмотреть создание специализированных классов таймеров или даже полноценной системы управления временем.

Unserialize не работает в реализации сохранения для игры на Unity
здрасти, я новичек, хочу сделать сохранялку для игры на юнити (там на си шарпе можно скриптить) ...

Ищу информацию о реализации кроссплатформенности в Unity
Всем привет. Пишу диплом по Unity, ищу статьи про кроссплатформенность в Unity. Любые статьи, на...

Ищу 3D-проектировщика для реализации модуля параметризации и изменения 3д модели эндопротеза в Unity с выгрузкой в stl
Здравствуйте! Ищу на сдельную работу 3D-проектировщика для реализации модуля параметризации и...

Передача значений по ip unity -> unity
Доброго времени суток вопрос: (мб простой) как передать например string значение между двумя unity...


Продвинутые паттерны



Базовые подходы хороши для простых сценариев, но по мере усложнения игры растут и требования к системе таймеров. Здесь на сцену выходят продвинутые архитектурные решения, которые позволяют эффективно управлять множеством временных процессов.

Timer Manager как системное решение



Для централизованного управления таймерами многие разработчики создают специальный Timer Manager — синглтон, который берёт на себя всю логику создания, обновления и уничтожения таймеров:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class TimerManager : MonoBehaviour
{
    private static TimerManager _instance;
    private List<UpdateTimer> _activeTimers = new List<UpdateTimer>();
    
    public static TimerManager Instance
    {
        get
        {
            if (_instance == null)
            {
                GameObject timerManagerObject = new GameObject("TimerManager");
                _instance = timerManagerObject.AddComponent<TimerManager>();
                DontDestroyOnLoad(timerManagerObject);
            }
            return _instance;
        }
    }
    
    public UpdateTimer CreateTimer(float duration, Action onComplete = null)
    {
        UpdateTimer timer = new UpdateTimer();
        if (onComplete != null)
            timer.CompletedEvent += onComplete;
        
        timer.Start(duration);
        _activeTimers.Add(timer);
        return timer;
    }
    
    private void Update()
    {
        for (int i = _activeTimers.Count - 1; i >= 0; i--)
        {
            _activeTimers[i].Tick();
            
            if (!_activeTimers[i].IsActive && _activeTimers[i].IsCompleted)
            {
                _activeTimers.RemoveAt(i);
            }
        }
    }
    
    public void PauseAllTimers()
    {
        foreach (var timer in _activeTimers)
        {
            timer.Pause();
        }
    }
    
    public void ResumeAllTimers()
    {
        foreach (var timer in _activeTimers)
        {
            timer.UnPause();
        }
    }
}
Такой подход имеет ряд преимуществ:
  • Единая точка обновления всех таймеров (экономия на вызовах Update).
  • Централизованное управление паузой/возобновлением.
  • Автоматическая очистка завершившихся таймеров.
  • Отсутствие зависимости от жизненного цикла конкретного GameObject.

Использование Timer Manager упрощает работу с таймерами в игре:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
void Start()
{
    // Создание таймера через менеджер
    TimerManager.Instance.CreateTimer(5f, () => {
        Debug.Log("Таймер завершился!");
    });
}
 
void OnGamePaused()
{
    // Простая пауза всех таймеров в игре
    TimerManager.Instance.PauseAllTimers();
}

Реактивные таймеры с использованием UniRx



UniRx (Reactive Extensions for Unity) — мощная библиотека для реактивного программирования в Unity. Она позволяет создавать таймеры в декларативном стиле:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UniRx;
 
void Start()
{
    // Таймер, запускающий действие через 3 секунды
    Observable.Timer(TimeSpan.FromSeconds(3))
        .Subscribe(_ => {
            Debug.Log("Прошло 3 секунды");
        }).AddTo(this);
        
    // Периодический таймер каждые 2 секунды
    Observable.Interval(TimeSpan.FromSeconds(2))
        .Subscribe(count => {
            Debug.Log($"Интервал сработал {count} раз");
        }).AddTo(this);
}
Основное преимущество реактивного подхода — простая композиция временных операций. Можно легко создавать сложные последовательности действий:

C#
1
2
3
4
5
6
7
// Цепочка таймеров с разными интервалами
Observable.Timer(TimeSpan.FromSeconds(1))
    .SelectMany(_ => Observable.Timer(TimeSpan.FromSeconds(0.5)))
    .SelectMany(_ => Observable.Timer(TimeSpan.FromSeconds(2)))
    .Subscribe(_ => {
        Debug.Log("Выполнено после 1с + 0.5с + 2с");
    }).AddTo(this);

Асинхронные таймеры с использованием async/await



Современный C# предоставляет мощный инструмент для асинхронного программирования — ключевые слова async/await. В Unity их можно использовать для создания элегантных таймеров без необходимости в корутинах:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AwaitableTimer
{
    public static async void StartTimer(float duration, Action onComplete)
    {
        float startTime = Time.time;
        float endTime = startTime + duration;
        
        while (Time.time < endTime)
        {
            await Task.Yield();
        }
        
        onComplete?.Invoke();
    }
}
Однако стандартный Task.Yield() не интегрируется с игровым циклом Unity идеально. Более правильный подход — использовать специальные асинхронные примитивы для Unity, например, через класс Awaitable:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Timer
{
    // ... (остальные свойства и методы из UpdateTimer)
    
    private CancellationTokenSource cts;
    
    private async void Tick()
    {
        do
        {
            await WaitForCompletion();
            
            if (cts.IsCancellationRequested)
                break;
            
            IsCompleted = true;
            CompletionsCount++;
            CompletedEvent?.Invoke();
            
            if (CompletionsCount < Repeats || _loop)
                _triggerTime += Duration;
                
        } while (!IsDurationExpired());
        
        IsActive = false;
    }
    
    private async Awaitable WaitForCompletion()
    {
        while ((!IsActive || !IsDurationExpired()) && !cts.IsCancellationRequested)
        {
            try
            {
                await Awaitable.NextFrameAsync(cts.Token);
            }
            catch when (cts.IsCancellationRequested)
            { }
            catch (Exception ex)
            {
                Debug.LogError(ex.Message);
            }
        }
    }
}
Этот таймер не требует вызова Tick() из Update(), так как использует Awaitable.NextFrameAsync для интеграции с игровым циклом Unity. Он работает в режиме "fire-and-forget", что делает его очень удобным в использовании:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private readonly Timer timer = new Timer();
 
private void Start()
{
    timer.CompletedEvent += OnTimerComplete;
    timer.Start(3f, 2); // 3 секунды, 2 повторения
}
 
// Метод Update больше не нужен!
 
private void OnTimerComplete()
{
    Debug.Log("Таймер завершился!");
}
Использование асинхронных таймеров значительно упрощает код, особенно когда требуется выполнить серию последовательных действий с задержками:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async void SequentialOperations()
{
    Debug.Log("Начало операций");
    
    await Awaitable.WaitForSecondsAsync(2f);
    Debug.Log("Прошло 2 секунды");
    
    await Awaitable.WaitForSecondsAsync(1.5f);
    Debug.Log("Прошло еще 1.5 секунды");
    
    // Выполнение сложной операции без блокировки игрового потока
    await SomeComplexOperationAsync();
    
    Debug.Log("Все операции завершены");
}

Событийно-ориентированная архитектура для временных систем



Продвинутые приложения часто используют событийно-ориентированную архитектуру, где таймеры становятся источниками событий, а не просто механизмами задержки. Такой подход позволяет разделить логику инициирования временных событий от реакции на них:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class TimeEvent
{
    public readonly string Id;
    public readonly object Payload;
    
    public TimeEvent(string id, object payload = null)
    {
        Id = id;
        Payload = payload;
    }
}
 
public class EventBasedTimer
{
    private readonly Dictionary<string, List<(float time, TimeEvent evt)>> _scheduledEvents = 
        new Dictionary<string, List<(float time, TimeEvent evt)>>();
    private readonly Dictionary<string, float> _categoryStartTimes = new Dictionary<string, float>();
    
    public static event Action<TimeEvent> OnTimeEvent;
    
    public void ScheduleEvent(string category, float delay, TimeEvent timeEvent)
    {
        if (!_scheduledEvents.ContainsKey(category))
        {
            _scheduledEvents[category] = new List<(float, TimeEvent)>();
            _categoryStartTimes[category] = Time.time;
        }
        
        _scheduledEvents[category].Add((Time.time + delay, timeEvent));
    }
    
    public void Update()
    {
        foreach (var category in _scheduledEvents.Keys.ToList())
        {
            var events = _scheduledEvents[category];
            for (int i = events.Count - 1; i >= 0; i--)
            {
                if (Time.time >= events[i].time)
                {
                    OnTimeEvent?.Invoke(events[i].evt);
                    events.RemoveAt(i);
                }
            }
        }
    }
    
    public void PauseCategory(string category)
    {
        // Сохранение времени паузы для возможности возобновления
    }
    
    public void ResumeCategory(string category)
    {
        // Пересчёт времён запланированных событий
    }
}
Используя такую систему, можно организовать взаимодействие между различными компонентами игры, не создавая прямых зависимостей:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Отправитель
void Start()
{
    var timerSystem = new EventBasedTimer();
    timerSystem.ScheduleEvent("Combat", 5f, new TimeEvent("CooldownComplete", "Fireball"));
}
 
// Получатель (в другом классе)
void OnEnable()
{
    EventBasedTimer.OnTimeEvent += HandleTimeEvent;
}
 
void HandleTimeEvent(TimeEvent evt)
{
    if (evt.Id == "CooldownComplete" && evt.Payload as string == "Fireball")
    {
        EnableFireballButton();
    }
}

Адаптация таймеров под различные платформы



Разные платформы имеют свои особенности, которые нужно учитывать при реализации таймеров. Например, на мобильных устройствах приложения могут уходить в фоновый режим, что влияет на работу времязависимых систем:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CrossPlatformTimer
{
    private float _pauseTime;
    private float _targetTime;
    private bool _usesRealTime;
    
    public CrossPlatformTimer(bool useRealTime = false)
    {
        _usesRealTime = useRealTime;
    }
    
    public void Start(float duration)
    {
        _targetTime = (_usesRealTime ? Time.realtimeSinceStartup : Time.time) + duration;
    }
    
    public bool IsCompleted()
    {
        float currentTime = _usesRealTime ? Time.realtimeSinceStartup : Time.time;
        return currentTime >= _targetTime;
    }
    
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            _pauseTime = _usesRealTime ? Time.realtimeSinceStartup : Time.time;
        }
        else
        {
            // Только для неустойчивых таймеров нужна корректировка
            if (!_usesRealTime)
            {
                float timePassed = Time.time - _pauseTime;
                _targetTime += timePassed;
            }
        }
    }
}
Time.realtimeSinceStartup не зависит от Time.timeScale и продолжает тикать даже когда приложение в фоне, что делает его полезным для таймеров реального мира.
Для веб-платформ может быть выгоднее полагаться на JavaScript-таймеры:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class WebGLTimer : MonoBehaviour
{
    [System.Runtime.InteropServices.DllImport("__Internal")]
    private static extern int StartTimer(float duration);
    
    [System.Runtime.InteropServices.DllImport("__Internal")]
    private static extern void CancelTimer(int id);
    
    public void ScheduleAction(float delay, System.Action callback)
    {
        #if UNITY_WEBGL && !UNITY_EDITOR
        int timerId = StartTimer(delay);
        // Регистрация коллбэка через JSlib
        #else
        StartCoroutine(DelayedActionCoroutine(delay, callback));
        #endif
    }
}

Объектно-ориентированные таймеры



Объектно-ориентированный подход к таймерам позволяет создавать гибкие иерархии, которые легко расширять под нужды проекта:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public abstract class TimerBase
{
    public abstract void Start(float duration);
    public abstract void Stop();
    public abstract void Tick();
    public abstract bool IsCompleted { get; }
}
 
public class OneShotTimer : TimerBase
{
    private float _duration;
    private float _startTime;
    private bool _isRunning;
    private bool _isCompleted;
    
    public override bool IsCompleted => _isCompleted;
    
    public override void Start(float duration)
    {
        _duration = duration;
        _startTime = Time.time;
        _isRunning = true;
        _isCompleted = false;
    }
    
    public override void Stop()
    {
        _isRunning = false;
    }
    
    public override void Tick()
    {
        if (!_isRunning || _isCompleted) return;
        
        if (Time.time >= _startTime + _duration)
        {
            _isCompleted = true;
            _isRunning = false;
        }
    }
}
 
public class RepeatingTimer : TimerBase
{
    private float _duration;
    private float _startTime;
    private float _lastTriggerTime;
    private bool _isRunning;
    private bool _isCompleted;
    private int _repeats;
    private int _currentRepeat;
    
    public event Action OnRepeat;
    
    public override bool IsCompleted => _isCompleted;
    
    public RepeatingTimer(int repeats = -1)
    {
        _repeats = repeats; // -1 означает бесконечный повтор
    }
    
    public override void Start(float duration)
    {
        _duration = duration;
        _startTime = Time.time;
        _lastTriggerTime = _startTime;
        _isRunning = true;
        _isCompleted = false;
        _currentRepeat = 0;
    }
    
    public override void Stop()
    {
        _isRunning = false;
    }
    
    public override void Tick()
    {
        if (!_isRunning) return;
        
        if (Time.time >= _lastTriggerTime + _duration)
        {
            _lastTriggerTime = Time.time;
            _currentRepeat++;
            OnRepeat?.Invoke();
            
            if (_repeats > 0 && _currentRepeat >= _repeats)
            {
                _isCompleted = true;
                _isRunning = false;
            }
        }
    }
}
Используя полиморфизм, можно легко добавлять новые типы таймеров с уникальным поведением, сохраняя общий интерфейс:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TimerFactory
{
    public static TimerBase CreateTimer(TimerType type, float duration)
    {
        TimerBase timer = null;
        switch (type)
        {
            case TimerType.OneShot:
                timer = new OneShotTimer();
                break;
            case TimerType.Repeating:
                timer = new RepeatingTimer();
                break;
            case TimerType.Countdown:
                timer = new CountdownTimer();
                break;
        }
        
        if (timer != null)
            timer.Start(duration);
            
        return timer;
    }
}
Такой подход особенно полезен в масштабных проектах, где требуется много различных типов таймеров с похожим, но всё же уникальным поведением. Он упрощает поддержку кода, делает его более модульным и позволяет избежать дублирования.

Практическая реализация



Теория создания таймеров имеет мало ценности без практического применения. Реальные проекты требуют интеграции систем времени с другими компонентами игры. Рассмотрим конкретные примеры и архитектурные решения для типичных игровых задач.

Примеры кода и архитектурные решения



Полноценная реализация таймера с поддержкой инспектора Unity может выглядеть так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
[System.Serializable]
public class SerializableTimer
{
    [SerializeField] private float duration = 1f;
    [SerializeField] private bool autoStart = false;
    [SerializeField] private bool loop = false;
    
    // Несериализуемые поля для работы таймера
    [System.NonSerialized] private float startTime;
    [System.NonSerialized] private float pausedTime;
    [System.NonSerialized] private float pausedDuration;
    [System.NonSerialized] private bool isRunning;
    [System.NonSerialized] private bool isPaused;
    
    // События
    public event Action OnComplete;
    public event Action<float> OnProgress; // 0f до 1f
    
    public bool IsRunning => isRunning && !isPaused;
    public float Progress => Mathf.Clamp01(isRunning ? (Time.time - startTime - pausedDuration) / duration : 0);
    public float RemainingTime => isRunning ? Mathf.Max(0, (startTime + duration + pausedDuration) - Time.time) : 0;
    
    // Инициализация при создании MonoBehaviour
    public void Initialize()
    {
        if (autoStart)
            Start();
    }
    
    public void Start()
    {
        startTime = Time.time;
        isRunning = true;
        isPaused = false;
        pausedDuration = 0f;
    }
    
    public void Pause()
    {
        if (!isRunning || isPaused) return;
        
        isPaused = true;
        pausedTime = Time.time;
    }
    
    public void Resume()
    {
        if (!isPaused) return;
        
        isPaused = false;
        pausedDuration += Time.time - pausedTime;
    }
    
    public void Stop()
    {
        isRunning = false;
        isPaused = false;
    }
    
    public void Update()
    {
        if (!isRunning || isPaused) return;
        
        // Вызов события обновления прогресса
        OnProgress?.Invoke(Progress);
        
        // Проверка завершения
        if (Time.time >= startTime + duration + pausedDuration)
        {
            OnComplete?.Invoke();
            
            if (loop)
            {
                startTime = Time.time;
                pausedDuration = 0f;
            }
            else
            {
                isRunning = false;
            }
        }
    }
}
Эту сериализуемую версию таймера легко интегрировать в компоненты и настраивать через инспектор Unity:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class AbilityCooldown : MonoBehaviour
{
    [SerializeField] private SerializableTimer cooldownTimer = new SerializableTimer();
    [SerializeField] private Button abilityButton;
    [SerializeField] private Image fillImage;
    
    private void Awake()
    {
        cooldownTimer.OnComplete += EnableAbility;
        cooldownTimer.OnProgress += UpdateVisual;
    }
    
    private void Update()
    {
        cooldownTimer.Update();
    }
    
    public void UseAbility()
    {
        // Логика применения способности
        
        // Запуск таймера перезарядки
        abilityButton.interactable = false;
        cooldownTimer.Start();
    }
    
    private void EnableAbility()
    {
        abilityButton.interactable = true;
    }
    
    private void UpdateVisual(float progress)
    {
        fillImage.fillAmount = 1f - progress; // Обратное заполнение для эффекта "истощения"
    }
}

Сериализация таймеров для сохранения состояния игры



При сохранении состояния игры важно иметь возможность сохранить и восстановить состояние таймеров. Для этого создадим сериализуемый контейнер:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[System.Serializable]
public class TimerSaveData
{
    public float remainingTime;
    public bool isRunning;
    public bool isPaused;
    
    public static TimerSaveData FromTimer(SerializableTimer timer)
    {
        return new TimerSaveData
        {
            remainingTime = timer.RemainingTime,
            isRunning = timer.IsRunning,
            isPaused = false // При загрузке всегда снимаем паузу
        };
    }
}
 
// Использование в системе сохранений
public void SaveGameState()
{
    var saveData = new GameSaveData();
    
    // Сохранение всех важных таймеров
    saveData.abilityCooldowns = new Dictionary<string, TimerSaveData>();
    saveData.abilityCooldowns["fireball"] = TimerSaveData.FromTimer(fireballCooldown);
    saveData.abilityCooldowns["shield"] = TimerSaveData.FromTimer(shieldCooldown);
    
    // Сериализация и сохранение в файл/PlayerPrefs
    string json = JsonUtility.ToJson(saveData);
    PlayerPrefs.SetString("SaveData", json);
}
 
public void LoadGameState()
{
    string json = PlayerPrefs.GetString("SaveData");
    var saveData = JsonUtility.FromJson<GameSaveData>(json);
    
    // Восстановление таймеров
    if (saveData.abilityCooldowns.TryGetValue("fireball", out var fbData))
    {
        if (fbData.isRunning)
        {
            fireballCooldown.Start();
            // Хитрость: изменяем startTime для имитации оставшегося времени
            var field = typeof(SerializableTimer).GetField("startTime", 
                          System.Reflection.BindingFlags.NonPublic | 
                          System.Reflection.BindingFlags.Instance);
            field.SetValue(fireballCooldown, Time.time - (fireballCooldown.duration - fbData.remainingTime));
        }
    }
}

Интеграция таймеров с системой UI



Визуальное отображение таймеров критически важно для игрового опыта. Реализуем компонент для отображения различных типов таймеров:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class TimerVisualizer : MonoBehaviour
{
    [SerializeField] private Image fillImage;
    [SerializeField] private Text timeText;
    [SerializeField] private TimerDisplayMode displayMode;
    
    private ITimer targetTimer; // Интерфейс для поддержки различных типов таймеров
    
    public enum TimerDisplayMode
    {
        FillOnly,
        TextOnly,
        FillAndText,
        FillAndTextCountdown
    }
    
    public void SetTimer(ITimer timer)
    {
        targetTimer = timer;
    }
    
    private void Update()
    {
        if (targetTimer == null) return;
        
        UpdateVisuals(targetTimer.Progress, targetTimer.RemainingTime);
    }
    
    private void UpdateVisuals(float progress, float remainingTime)
    {
        if (fillImage != null && (displayMode == TimerDisplayMode.FillOnly || 
                                 displayMode == TimerDisplayMode.FillAndText ||
                                 displayMode == TimerDisplayMode.FillAndTextCountdown))
        {
            fillImage.fillAmount = progress;
        }
        
        if (timeText != null && (displayMode == TimerDisplayMode.TextOnly || 
                                displayMode == TimerDisplayMode.FillAndText ||
                                displayMode == TimerDisplayMode.FillAndTextCountdown))
        {
            if (displayMode == TimerDisplayMode.FillAndTextCountdown)
            {
                timeText.text = Mathf.Ceil(remainingTime).ToString("0");
            }
            else
            {
                timeText.text = remainingTime.ToString("0.0") + "s";
            }
        }
    }
}
Такая система позволяет визуализировать любой тип таймера, реализующий интерфейс ITimer, что обеспечивает гибкость и переиспользуемость кода.

Визуальные интерфейсы для настройки таймеров в редакторе Unity



Эффективность работы с таймерами значительно повышается при наличии удобных инструментов редактирования в инспекторе Unity. Создадим пользовательский редактор для нашего сериализуемого таймера:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#if UNITY_EDITOR
using UnityEditor;
 
[CustomPropertyDrawer(typeof(SerializableTimer))]
public class SerializableTimerDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUIUtility.singleLineHeight * 4 + EditorGUIUtility.standardVerticalSpacing * 3;
    }
    
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        
        // Заголовок со сворачиванием
        var foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
        property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, label);
        
        if (property.isExpanded)
        {
            // Отступ для вложенных свойств
            EditorGUI.indentLevel++;
            
            // Расчет позиций для элементов
            float lineHeight = EditorGUIUtility.singleLineHeight;
            float spacing = EditorGUIUtility.standardVerticalSpacing;
            
            var durationRect = new Rect(position.x, position.y + lineHeight + spacing, position.width, lineHeight);
            var autoStartRect = new Rect(position.x, position.y + (lineHeight + spacing) * 2, position.width, lineHeight);
            var loopRect = new Rect(position.x, position.y + (lineHeight + spacing) * 3, position.width, lineHeight);
            
            // Отрисовка полей
            EditorGUI.PropertyField(durationRect, property.FindPropertyRelative("duration"), new GUIContent("Длительность"));
            EditorGUI.PropertyField(autoStartRect, property.FindPropertyRelative("autoStart"), new GUIContent("Автозапуск"));
            EditorGUI.PropertyField(loopRect, property.FindPropertyRelative("loop"), new GUIContent("Зациклить"));
            
            EditorGUI.indentLevel--;
        }
        
        EditorGUI.EndProperty();
    }
}
#endif
Такой редактор делает работу с таймерами в инспекторе более наглядной и удобной. Для более сложных таймеров можно расширить функциональность редактора, добавив предпросмотр временной шкалы или кнопки для тестирования таймера непосредственно в редакторе.

Обработка краевых случаев



При работе с таймерами часто возникают ситуации, требующие особого внимания. Рассмотрим несколько типичных проблем и их решения:

1. Изменение TimeScale



Если игра использует изменение Time.timeScale для эффектов замедления или ускорения времени, обычные таймеры могут работать некорректно:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TimeScaleIndependentTimer
{
    private float startTime;
    private float duration;
    private bool isRunning;
    private bool useUnscaledTime;
    
    public TimeScaleIndependentTimer(bool useUnscaledTime = false)
    {
        this.useUnscaledTime = useUnscaledTime;
    }
    
    public void Start(float duration)
    {
        this.duration = duration;
        startTime = useUnscaledTime ? Time.unscaledTime : Time.time;
        isRunning = true;
    }
    
    public bool IsCompleted()
    {
        if (!isRunning) return false;
        
        float currentTime = useUnscaledTime ? Time.unscaledTime : Time.time;
        return currentTime - startTime >= duration;
    }
    
    public float Progress()
    {
        if (!isRunning) return 0f;
        
        float currentTime = useUnscaledTime ? Time.unscaledTime : Time.time;
        return Mathf.Clamp01((currentTime - startTime) / duration);
    }
}

2. Обработка очень малых или больших значений



При работе с очень короткими или длинными таймерами могут возникать проблемы с точностью или переполнением:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class PrecisionTimer
{
    private double startTime;
    private double duration;
    
    public void Start(double durationInSeconds)
    {
        // Используем double для большей точности
        startTime = Time.timeAsDouble;
        duration = durationInSeconds;
    }
    
    public bool IsCompleted()
    {
        return Time.timeAsDouble - startTime >= duration;
    }
    
    // Защита от слишком коротких таймеров
    public static void ExecuteAfterMinimumTime(float minTime, System.Action action)
    {
        if (minTime < Time.deltaTime)
        {
            // Если время меньше одного кадра, выполняем сразу
            action?.Invoke();
        }
        else
        {
            // Иначе используем таймер
            var timer = new PrecisionTimer();
            timer.Start(minTime);
            // ... логика запуска действия при завершении таймера
        }
    }
}

3. Обработка прерываний и восстановлений



В мобильных играх приложение может быть приостановлено операционной системой. Для корректной работы таймеров в таких ситуациях используем комбинацию игрового и реального времени:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class RobustMobileTimer : MonoBehaviour
{
    private float pauseRealTime;
    private float gameTimer;
    private bool wasRunningBeforePause;
    
    void Start()
    {
        // Подписка на события приложения
        Application.focusChanged += OnApplicationFocusChanged;
    }
    
    void OnDestroy()
    {
        Application.focusChanged -= OnApplicationFocusChanged;
    }
    
    private void OnApplicationFocusChanged(bool hasFocus)
    {
        if (!hasFocus)
        {
            // Приложение потеряло фокус
            pauseRealTime = Time.realtimeSinceStartup;
            wasRunningBeforePause = gameTimer > 0;
        }
        else if (wasRunningBeforePause)
        {
            // Приложение восстановлено
            float pauseDuration = Time.realtimeSinceStartup - pauseRealTime;
            
            // Корректировка всех активных таймеров
            AdjustActiveTimers(pauseDuration);
        }
    }
    
    private void AdjustActiveTimers(float pauseDuration)
    {
        // Реализация корректировки таймеров
    }
}

4. Синхронизация в многопользовательских играх



Для сетевых игр часто требуется синхронизировать таймеры между клиентами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class NetworkSynchronizedTimer
{
    private double serverStartTime;
    private double duration;
    private double serverTimeOffset;
    
    public void InitFromServer(double serverStartTimeStamp, double durationInSeconds)
    {
        serverStartTime = serverStartTimeStamp;
        duration = durationInSeconds;
        
        // Разница между серверным и локальным временем
        serverTimeOffset = NetworkTime.ServerTimeAtLocalTime(Time.timeAsDouble) - Time.timeAsDouble;
    }
    
    public bool IsCompleted()
    {
        double currentServerTime = Time.timeAsDouble + serverTimeOffset;
        return currentServerTime - serverStartTime >= duration;
    }
    
    public float RemainingTime()
    {
        double currentServerTime = Time.timeAsDouble + serverTimeOffset;
        return (float)Math.Max(0, serverStartTime + duration - currentServerTime);
    }
}
Правильная обработка краевых случаев делает таймеры надёжными даже в нестандартных ситуациях. Это особенно важно для игр, которые должны стабильно работать на различных устройствах и в разных условиях. Тщательное тестирование таймеров под разными нагрузками и в различных сценариях использования — ключевой этап разработки надёжной системы управления временем.

Оптимизация и производительность



Системы таймеров, несмотря на свою кажущуюся простоту, могут существенно влиять на производительность игры. Особенно это заметно в проектах с большим количеством одновременно работающих таймеров, например, в стратегиях реального времени или симуляторах с множеством независимых процессов.

Влияние таймеров на производительность игры



Главная проблема производительности таймеров связана с выделением памяти. Разные подходы к реализации таймеров дают различную нагрузку на сборщик мусора:

1. Структуры vs классы: Структуры, как значимые типы, не создают нагрузки на сборщик мусора, что делает MinimalTimer максимально эффективным с точки зрения памяти:

C#
1
2
3
4
5
6
7
8
public readonly struct MinimalTimer
{
    public static MinimalTimer Start(float duration) => new MinimalTimer(duration);
    public bool IsCompleted => Time.time >= _triggerTime && _triggerTime != 0;
 
    private readonly float _triggerTime;
    private MinimalTimer(float duration) => _triggerTime = Time.time + duration;
}
2. Корутины: Генерируют мусор при создании из-за оборачивания в IEnumerator:

C#
1
2
3
4
5
6
7
8
// Создаёт мусор при запуске
StartCoroutine(WaitAndExecute(3f));
 
private IEnumerator WaitAndExecute(float time)
{
    yield return new WaitForSeconds(time); // Создаёт объект WaitForSeconds
    // Код выполняется после ожидания
}
3. Awaitable Таймеры: Генерируют минимальное количество мусора при запуске асинхронных операций:

C#
1
2
// Создаёт некоторый мусор из-за работы асинхронного механизма
await Awaitable.WaitForSecondsAsync(3f);
Сравнительные тесты показывают, что наименьшее влияние на производительность оказывают таймеры, реализованные как структуры с минимумом функциональности. Однако на практике выбор должен учитывать компромисс между производительностью и удобством использования.

Профилирование таймеров



Для анализа производительности таймеров в Unity используем Profiler, который позволяет точно отследить выделение памяти и время выполнения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void MeasureTimerPerformance()
{
    const int iterations = 10000;
    
    // Профилирование минимального таймера
    Profiler.BeginSample("MinimalTimer Creation");
    for (int i = 0; i < iterations; i++)
    {
        var timer = MinimalTimer.Start(1f);
    }
    Profiler.EndSample();
    
    // Профилирование Update таймера
    Profiler.BeginSample("UpdateTimer Creation");
    for (int i = 0; i < iterations; i++)
    {
        var timer = new UpdateTimer();
        timer.Start(1f);
    }
    Profiler.EndSample();
}
В реальности тестирование должно включать не только создание таймеров, но и их обновление, завершение и общую нагрузку на систему. Для этого создают тестовые сценарии с разным количеством таймеров и замеряют FPS, время на кадр и выделение памяти.

Методики снижения нагрузки



При большом количестве таймеров можно применить следующие оптимизации:

1. Пулинг таймеров — вместо создания и уничтожения используйте переиспользование:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TimerPool
{
    private readonly Stack<UpdateTimer> _pool = new Stack<UpdateTimer>();
    
    public UpdateTimer Get()
    {
        if (_pool.Count > 0)
            return _pool.Pop();
        return new UpdateTimer();
    }
    
    public void Return(UpdateTimer timer)
    {
        timer.Stop();
        _pool.Push(timer);
    }
}
2. Группировка по частоте обновления — не все таймеры требуют проверки каждый кадр:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TimerManager : MonoBehaviour
{
    private List<UpdateTimer> _highPriorityTimers = new List<UpdateTimer>();
    private List<UpdateTimer> _normalPriorityTimers = new List<UpdateTimer>();
    private int _normalUpdateCounter = 0;
    
    private void Update()
    {
        // Обновляем высокоприоритетные каждый кадр
        foreach (var timer in _highPriorityTimers)
            timer.Tick();
            
        // Обновляем обычные каждые 2 кадра
        _normalUpdateCounter++;
        if (_normalUpdateCounter >= 2)
        {
            foreach (var timer in _normalPriorityTimers)
                timer.Tick();
            _normalUpdateCounter = 0;
        }
    }
}
3. Иерархическая структура таймеров — связанные таймеры могут обновляться вместе:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TimerGroup
{
    private List<UpdateTimer> _childTimers = new List<UpdateTimer>();
    
    public void AddTimer(UpdateTimer timer)
    {
        _childTimers.Add(timer);
    }
    
    public void Tick()
    {
        for (int i = _childTimers.Count - 1; i >= 0; i--)
        {
            var timer = _childTimers[i];
            timer.Tick();
            
            if (!timer.IsActive && timer.IsCompleted)
                _childTimers.RemoveAt(i);
        }
    }
}

Масштабирование времени



Для создания эффектов замедления или ускорения игрового процесса часто изменяют Time.timeScale. Это влияет на все стандартные таймеры, что может быть нежелательно. Решение — создать таймеры с поддержкой нескольких шкал времени:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class MultiTimeScaleTimer
{
    public enum TimeScale { Game, UI, Physics, RealTime }
    
    private TimeScale _scale;
    private float _duration;
    private float _startTime;
    
    public MultiTimeScaleTimer(TimeScale scale)
    {
        _scale = scale;
    }
    
    public void Start(float duration)
    {
        _duration = duration;
        switch (_scale)
        {
            case TimeScale.Game:
                _startTime = Time.time;
                break;
            case TimeScale.UI:
                _startTime = Time.unscaledTime;
                break;
            case TimeScale.Physics:
                _startTime = Time.fixedTime;
                break;
            case TimeScale.RealTime:
                _startTime = Time.realtimeSinceStartup;
                break;
        }
    }
    
    public bool IsCompleted()
    {
        float currentTime;
        switch (_scale)
        {
            case TimeScale.Game:
                currentTime = Time.time;
                break;
            case TimeScale.UI:
                currentTime = Time.unscaledTime;
                break;
            case TimeScale.Physics:
                currentTime = Time.fixedTime;
                break;
            case TimeScale.RealTime:
                currentTime = Time.realtimeSinceStartup;
                break;
            default:
                currentTime = Time.time;
                break;
        }
        
        return currentTime - _startTime >= _duration;
    }
}
Такой подход позволяет разделить игровые системы, которые должны замедляться (например, физику, анимации), и те, которые должны работать в реальном времени (интерфейс, сетевые запросы).

Сравнительный анализ подходов



Для выбора оптимального решения важно сравнить различные подходы к реализации таймеров не только теоретически, но и на практике. Тестирование на реальных сценариях показывает следующую картину:

Code
1
2
3
4
5
6
| Тип таймера | Память | CPU | Удобство | Лучшие сценарии использования |
|-------------|--------|-----|----------|------------------------------|
| MinimalTimer (struct) | Минимальная | Минимальная | Низкое | Большое количество простых таймеров |
| UpdateTimer (class) | Низкая | Низкая | Среднее | Таймеры с управлением (пауза, перезапуск) |
| Awaitable Timer | Средняя | Средняя | Высокое | Асинхронные операции, скрипты без MonoBehaviour |
| Coroutine | Высокая | Средняя | Среднее | Последовательные операции с ожиданием |
Практические тесты показывают, что при создании 10 000 таймеров разница в потреблении памяти между структурным и объектным подходами может достигать 300-400 КБ, что критично для мобильных платформ с ограниченными ресурсами.

Оптимизация интерфейсных таймеров



Интерфейсные элементы часто требуют собственных таймеров для анимаций, обратных отсчётов и других визуальных эффектов. При большом количестве элементов UI это может создавать значительную нагрузку. Эффективный подход – централизованное управление UI-таймерами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class UITimerSystem : MonoBehaviour
{
private struct UITimerData
{
    public RectTransform target;
    public float duration;
    public float startTime;
    public Vector2 startPos;
    public Vector2 endPos;
    public Action onComplete;
}
 
private List<UITimerData> _activeTimers = new List<UITimerData>();
 
public void AnimatePosition(RectTransform target, Vector2 endPos, float duration, Action onComplete = null)
{
    _activeTimers.Add(new UITimerData
    {
        target = target,
        duration = duration,
        startTime = Time.unscaledTime,
        startPos = target.anchoredPosition,
        endPos = endPos,
        onComplete = onComplete
    });
}
 
private void Update()
{
    float currentTime = Time.unscaledTime;
    
    for (int i = _activeTimers.Count - 1; i >= 0; i--)
    {
        var timer = _activeTimers[i];
        float elapsed = currentTime - timer.startTime;
        float progress = Mathf.Clamp01(elapsed / timer.duration);
        
        timer.target.anchoredPosition = Vector2.Lerp(timer.startPos, timer.endPos, progress);
        
        if (progress >= 1f)
        {
            timer.onComplete?.Invoke();
            _activeTimers.RemoveAt(i);
        }
    }
}
}
Такой объединённый обработчик значительно эффективнее, чем отдельные компоненты на каждом элементе интерфейса.

Многопоточные таймеры



Для особо требовательных сценариев с тысячами независимых таймеров можно использовать многопоточную обработку:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public class ThreadedTimerSystem : MonoBehaviour
{
private class ThreadedTimer
{
    public float duration;
    public float startTime;
    public Action callback;
    public int threadId;
    public bool isCompleted;
}
 
private List<ThreadedTimer>[] _timerThreads;
private int _threadCount;
private volatile bool _isRunning = true;
 
private void Start()
{
    _threadCount = Mathf.Max(1, SystemInfo.processorCount - 1);
    _timerThreads = new List<ThreadedTimer>[_threadCount];
    
    for (int i = 0; i < _threadCount; i++)
    {
        _timerThreads[i] = new List<ThreadedTimer>();
        int threadIndex = i;
        
        // Запускаем поток для проверки таймеров
        Thread thread = new Thread(() => ProcessTimers(threadIndex));
        thread.Start();
    }
}
 
public void AddTimer(float duration, Action callback)
{
    // Находим поток с наименьшей нагрузкой
    int targetThread = 0;
    int minCount = int.MaxValue;
    
    for (int i = 0; i < _threadCount; i++)
    {
        int count = _timerThreads[i].Count;
        if (count < minCount)
        {
            minCount = count;
            targetThread = i;
        }
    }
    
    lock (_timerThreads[targetThread])
    {
        _timerThreads[targetThread].Add(new ThreadedTimer
        {
            duration = duration,
            startTime = Time.realtimeSinceStartup,
            callback = callback,
            threadId = targetThread,
            isCompleted = false
        });
    }
}
 
private void ProcessTimers(int threadIndex)
{
    while (_isRunning)
    {
        List<ThreadedTimer> completedTimers = new List<ThreadedTimer>();
        
        // Проверяем таймеры в потоке
        lock (_timerThreads[threadIndex])
        {
            float currentTime = Time.realtimeSinceStartup;
            
            foreach (var timer in _timerThreads[threadIndex])
            {
                if (!timer.isCompleted && currentTime - timer.startTime >= timer.duration)
                {
                    timer.isCompleted = true;
                    completedTimers.Add(timer);
                }
            }
        }
        
        // Запланируем вызовы коллбэков в основном потоке
        if (completedTimers.Count > 0)
        {
            foreach (var timer in completedTimers)
            {
                UnityMainThreadDispatcher.Instance().Enqueue(() => timer.callback?.Invoke());
            }
        }
        
        // Небольшая задержка чтобы не загружать CPU на 100%
        Thread.Sleep(16);
    }
}
 
private void OnDestroy()
{
    _isRunning = false;
}
}
Этот подход требует дополнительного компонента UnityMainThreadDispatcher для безопасного вызова Unity API из фоновых потоков, но значительно снижает нагрузку на основной поток при использовании тысяч таймеров.

Оптимизация под различные платформы



Разные платформы имеют свои особенности, которые стоит учитывать при оптимизации таймеров:
Мобильные устройства: Минимизация выделения памяти и пробуждений CPU
VR/AR: Высокая частота обновления и стабильность для предотвращения тошноты
WebGL: Учёт особенностей работы JavaScript и ограничений однопоточной модели

Для WebGL, например, часто эффективнее использовать нативные JavaScript таймеры:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#if UNITY_WEBGL && !UNITY_EDITOR
[System.Runtime.InteropServices.DllImport("__Internal")]
private static extern int SetJSTimeout(float milliseconds);
 
[System.Runtime.InteropServices.DllImport("__Internal")]
private static extern void ClearJSTimeout(int timeoutId);
#endif
 
public int SetTimeout(float seconds, Action callback)
{
#if UNITY_WEBGL && !UNITY_EDITOR
    return SetJSTimeout(seconds * 1000);
#else
    // Fallback для других платформ
    StartCoroutine(TimeoutCoroutine(seconds, callback));
    return 0;
#endif
}
 
private IEnumerator TimeoutCoroutine(float seconds, Action callback)
{
    yield return new WaitForSeconds(seconds);
    callback?.Invoke();
}
JavaScript-код для этих функций:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
mergeInto(LibraryManager.library, {
  SetJSTimeout: function(milliseconds) {
    var id = setTimeout(function() {
      unityInstance.SendMessage('TimerManager', 'OnJSTimeout', id.toString());
    }, milliseconds);
    return id;
  },
  
  ClearJSTimeout: function(timeoutId) {
    clearTimeout(timeoutId);
  }
});
При выборе оптимальной стратегии таймеров следует тестировать производительность на целевых устройствах. Часто то, что работает идеально на мощном ПК разработчика, может создавать серьёзные проблемы на мобильных устройствах или в WebGL-версиях игры.

Выбор оптимального таймера: рекомендации и перспективы



Выбор подходящего решения для управления временем в Unity — это баланс между удобством, производительностью и архитектурной согласованностью. Каждый проект имеет свои уникальные требования, которые определяют оптимальный подход.

Для небольших проектов со скромными требованиями к временным вычислениям, минималистичный таймер на основе структуры часто является наилучшим выбором. Он не генерирует мусор в памяти, имеет минимальные накладные расходы и достаточно прост для понимания. Базовая проверка Time.time >= triggerTime эффективно справляется с большинством повседневных задач. В средних проектах, где таймеры должны поддерживать функции паузы, повторения и интеграцию с пользовательским интерфейсом, класс UpdateTimer предлагает хороший компромисс между функциональностью и производительностью. Централизованная система управления таймерами через Timer Manager упростит отладку и поддержку кода. Для сложных проектов с асинхронными операциями, таймерам на основе Awaitable/async-await стоит отдать предпочтение, несмотря на незначительные затраты памяти. Удобство и читаемость кода, устранение вложенности коллбэков значительно упрощает поддержку проекта в долгосрочной перспективе.

При разработке мобильных игр особое внимание следует уделить минимизации выделения памяти. Пулинг таймеров, использование структур вместо классов и группировка по частоте обновления могут существенно улучшить производительность на устройствах с ограниченными ресурсами. Сетевые игры требуют особого подхода к синхронизации таймеров. Надёжное решение должно учитывать задержки, рассинхронизацию часов и возможные сбои соединения. Серверная проверка критических таймеров предотвращает читерство.

Вне зависимости от выбранного подхода, правильная архитектура таймеров значительно влияет на качество кода и удобство разработки. Стремитесь к единообразию — если в проекте используется несколько типов таймеров, они должны иметь схожие интерфейсы для упрощения переключения между ними.

С развитием Unity и C# появляются новые возможности для эффективной работы со временем. Система Job System и Entity Component System (ECS) позволяют обрабатывать тысячи таймеров одновременно с минимальными накладными расходами. Изучение этих подходов может дать значительные преимущества в масштабных проектах.

Unity сцены. Unity lifecycle
Всем привет. Не понимаю по каким словам искать ответ на этот вопрос. Не совсем понимаю жизненный...

Где можно почитать основы разработки в Unity/Unity 3D
До этого был небольшой опыт работы с Windows.Forms и WFP с C#. Где можно разобраться и научится...

Установка бесплатной Unity Personal с сайта Unity
Делаю так: Выбор Версии Personal здесь:...

Проблема в Unity all compiler errors have to be fixed unity
Всем доброго времени суток,столкнулся с такой проблемой в юнити Проект 2d Для кода использую...

Есть тут кто пишет на C# для Unity? Под игры созданные в Unity читы делаются?
Привет. Есть тут кто пишет на C# для Unity? Под игры созданные в Unity читы делаются? Такое...

Unity 2d unity.engine.ui не работает
using System.Collections; using System.Collections.Generic; using UnityEngine; using...

Разработка игр с Unity без Unity редактора
Здравствуйте. Хочу обрисовать ситуацию. Я слепой. Полностью слеп. Среди незрячих программистов...

Как реализовать свайп вправо-вверх или влево-вниз и т.д. в 3D проекте Unity (Двойной свайп в Unity)?
Здравствуйте. Я не могу решить одну большую проблему (3D проект на платформе Windows) Суть:...

Unity ECS ( нет блин CS:S) - Mr. Unity, мы ждём документацию
Всё до боли просто, да и дело не горит, но хочется знать всего одну вещь: как получить архитектуру...

Как работать с Unity Analytics в Unity 2020+?
Приветствую, делал по туториалу: 3jDD-E1OUkc. На 7:03 он выбирает Analytics Event Tracker. Хотя на...

Unity, Разработка игр на Unity за 24 часа
Читаю книгу &quot;Разработка игр на Unity за 24 часа&quot; остановился на часе 6, где надо сделать игру...

Unity Cloud(Unity сервисы)
Доброго времени суток, такая проблема: реализовал сохранение и загрузку данных через unity сервисы,...

Метки .net, c#, unity
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Запустил конкурс "тем и промптов для текстовых квестов созданных почти чисто ИИ"
Adler 06.10.2026
Всем привет! За последние три-четыре дня я создал более 16 текстовых квестовых игр используя преимущественно по одному запросу к ИИ на игру. Мне так понравилось смотреть все ветки/ сцены во всех. . .
ИИ не может найти нужный язык в списке
Supersumestria 05.10.2026
Я ему даю вот такое изображение и прошу найти и подчеркнуть немецкий язык. Возвращает он вот это: https:/ / i. **********/ vqBWLe2. png Нужную строчку в 3й колонке просто выдумал. . Это. . .
Новая последняя моя музыка в SUNO
zorxor 05.10.2026
Здравствуйте, дорогие мои друзья! С большой радостью я хотел бы представить вам свою новую последнею музыку, которую сгенерировала мне по моей просьбе нейросеть SUNO. С уважением, zorxor. Это. . .
Nekobox - outbounds[0].transport: unknown transport type: raw
damix 01.10.2026
Фикс ошибки Правым кликом по серверу -> отладочная информация -> edit Заменить "net": "raw", на "net": "tcp", Нажать кнопку reload.
Программный домашний кинотеатр
russiannick 27.09.2026
Сподобился на программный домашний кинотеатр. В качестве ЯВУ по традиции выбрал js. В помощники взял Яндекс-Алису. Было создано три зала на разные интересы. исторические и ретро сериал Хичкок. . .
Беседа с ИИ о программистах, недопускающих к созданию и правке кода генеративные ИИ и причины этого
zorxor 21.09.2026
Раньше я радовался или получал некоторые эмоции, пусть небольшие, но всё же, от самого процесса написания кода, рекомпиляции и запуска, видя постепенное развитие программы и прочее. А теперь лень. . .
Мобильное приложение ColorStep
pavlinmavlin 17.09.2026
Реализовал приложение Красный, Зеленый, Синий в Unity3d + c#. Название изменил на ColorStep. Приложение прошло модерацию и теперь доступно для скачивания. Делал его сам, шаг за шагом — и вот,. . .
Запрет дублирования строк в табличной части
Maks 13.09.2026
Реализация из решения ниже выполнена на нетиповом справочнике "Нормы ТО" с табличной часть "Виды ТО", разработанного в КА2, со следующими реквизитами: - ВидТО (СправочникСсылка. ВидыТО); - ВидГСМ. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru