Найти в Дзене
Сергей Эланд

Основы языка С# для создания игр #5: Время, кадры и готовые решения

При создании игр частенько могут потребоваться интервалы времени, чтобы нужные нам команды выполнялись не мгновенно, а через какой-то промежуток времени. Например, как в предыдущей статье, можно сделать цикл, который выполняется бесконечно, но каждое его выполнение происходит через определённое количество миллисекунд. Если же мы работаем с игровым движком типа Unity, то у нас появляется еще один вариант отсчёта времени. Движок добавляет такое понятие, как кадры. Каждый раз, когда изображение формируется и отрисовывается на экране, отсчитывается время одного кадра. Как только кадр полностью отрисовался, то начинает отрисовываться следующий кадр, в котором что-то на экране может поменяться, а может и не поменяться ничего. Время отрисовки кадра тоже измеряется в миллисекундах, но эта величина непостоянная. Чёрный экран с курсором отрисуется например за 1 миллисекунду, а сложное трехмерное изображение с эффектами отрисуется за 20 миллисекунд. При этом, чтобы получить комфортные глазу 60 к
Оглавление

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

Даже если условие никогда не станет false, то метод будет работать параллельно с остальной логикой программы, проверяя каждые 100 миллисекунд условие и выполняя код из тела цикла
Даже если условие никогда не станет false, то метод будет работать параллельно с остальной логикой программы, проверяя каждые 100 миллисекунд условие и выполняя код из тела цикла

Если же мы работаем с игровым движком типа Unity, то у нас появляется еще один вариант отсчёта времени. Движок добавляет такое понятие, как кадры. Каждый раз, когда изображение формируется и отрисовывается на экране, отсчитывается время одного кадра. Как только кадр полностью отрисовался, то начинает отрисовываться следующий кадр, в котором что-то на экране может поменяться, а может и не поменяться ничего.

Время отрисовки кадра тоже измеряется в миллисекундах, но эта величина непостоянная. Чёрный экран с курсором отрисуется например за 1 миллисекунду, а сложное трехмерное изображение с эффектами отрисуется за 20 миллисекунд. При этом, чтобы получить комфортные глазу 60 кадров в секунду, нужно уложиться в 16 миллисекунд для отрисовки каждого кадра.

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

Update() - метод, который выполняется каждый кадр.

Также игровой движок Unity предоставляет нам метод Start(), который выполняется в момент запуска игры, либо в момент активации самого скрипта. Метод Start() выполняется раньше самого первого выполнения метода Update() и позволяет прописать какую-то исходную логику, с которой потом можно работать каждый кадр. Например, исходные параметры или ссылки.

Когда мы только создаём наш первый скрипт в Unity, то эти два метода прописаны по умолчанию.

По умолчанию также подключаются необходимые библиотеки (UnityEngine и другие)
По умолчанию также подключаются необходимые библиотеки (UnityEngine и другие)

Готовые решения в C#

Методы Start() и Update() по умолчанию внедряются в наш код путём добавления библиотеки UnityEngine с помощью оператора using в самом начале нашего скрипта. Иными словами мы используем пространство имён UnityEngine.

Подключение библиотек с помощью using позволяет использовать классы, методы и поля из другого скрипта (из другой программы). Можно сказать, что мы таким образом подключаем к нашему коду некий уже заготовленный функционал.

VisualStudio помечает серым цветом библиотеки (пространства имён), которые мы никак не использовали в нашем коде. UnityEngine выделен белым, потому что в коде уже прописан метод Update(), являющейся частью этой библиотеки (пространства имён)
VisualStudio помечает серым цветом библиотеки (пространства имён), которые мы никак не использовали в нашем коде. UnityEngine выделен белым, потому что в коде уже прописан метод Update(), являющейся частью этой библиотеки (пространства имён)

Это отличный способ не писать свою программу с абсолютного нуля и распространенная практика в программировании.

Использование Unity - это использование готового решения, чтобы не прописывать графический движок с нуля, например, на языке C#.

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

Языки низкого уровня и компьютер - это тоже готовое решение, позволяющее нам не изобретать "велосипед". :) И даже они пользуются готовыми решения предоставленными природой и физикой.

При создании игры на Unity мы будем использовать многие готовые решения, предоставляемые нам игровым движком. Например, класс Time.

Время в игре и Time.deltaTime

Unity предоставляет нам полезную функцию Time.deltaTime, которая даёт нам значение времени в секундах между предыдущим кадром и текущим. Т.е. фактически время, за которое последний кадр был отрисован.

Поскольку время отрисовки кадров величина непостоянная, то и каждое выполнение метода Update() будет происходить через разные интервалы времени. Это оптимально для того, чтобы например к каждому следующем кадру успеть посчитать количество здоровья главного героя и вывести его на экран. Но вот при работе с движением будут проблемы.

Если кто помнит, то легендарная игра Doom 1993 года работала на разных процессорах с разной скоростью. Например, на моём 286-ом персонаж и враги передвигались довольно медленно, а у моего двоюродного брата на 386-ом, в том же Думе, все перемещалось чуть ли не в 2 раза быстрее. Такое наблюдалось во многих играх. В некоторые старые игры невозможно играть на новых компьютерах из-за того, что они работают нереально быстро и нужны специальные эмуляторы.

Разные процессоры и разные видеокарты будут выдавать различное количество кадров в секунду (FPS - Frame Per Second).

Чтобы движение в игре происходило с постоянной скоростью вне зависимости от мощности компьютера и количества кадров в секунду, мы должны использовать Time.deltaTime при расчётах времени, а соответственно и скорости.

Простейший секундомер
Простейший секундомер

Таким образом мы легко можем сделать таймер или секундомер, которые будут изменяться в соответствии с реальными секундами.

Чтобы посчитать расстояние, зная скорость, нам нужно умножить скорость на время. Время в нашем случае - это время отрисовки кадра. Пройденное расстояние тоже будет рассчитываться каждый кадр. Вне зависимости от количества кадров в секунду за 1 секунду расстояние будет 1 метр. Таким образом, скорость движения на любом устройстве будет одинакова. Справедливость! :)

Скорость умножаем на время между кадрами и решаем проблему старых игр.
Скорость умножаем на время между кадрами и решаем проблему старых игр.

К реальному времени мы можем в своей игре привязать изменение параметров персонажа, время перезарядки оружия или способности, время какого-нибудь события в игре, внутриигровое время, скорость движения монстров и много всего другого.

Главное не перегрузить метод Update() большим количеством затратных в плане ресурсов компьютера операций. Иначе каждый кадр в игре будет требовать много вычислительной мощности и это повлияет на производительность игры. Количество кадров (fps) в игре снизится. В основном fps в игре зависит от графики, но неоптимизированный код способен усугубить ситуацию. Особенно когда дело касается мобильных игр.