Найти тему
ZDG

Как я учился программировать, или что делать, когда говорят DELAY

Предыдущие части: Моя первая игра, Трудности и озарения, Маленькие лайфхаки, Гордость и унижение

В 90-х годах были очень распространены самопальные игры, то есть сделанные не какими-то компаниями, а обычными людьми. Они писались для таких компьютеров, как БК-0010, ДВК-2, ДВК-3, МС-0511.

Одна из таких игр (для ДВК-3) меня одновременно насмешила, поставила в тупик и кое-чему научила. Сразу после запуска она требовала:

DELAY:

Это выглядело очень забавно. В то время русские кодировки не всегда поддерживались, так что вполне можно было подумать, что игра говорит "ДЕЛАЙ". А что делать?

На самом деле игра просила ввести задержку (по-английски DELAY). Нужно было ввести число от 0 до 9, и после этого запускалась игра "Змейка". Змейка двигалась тем быстрее, чем меньше была задержка.

Задержка между чем и чем?

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

Скорость работы компьютера такова, что если бы состояния игры менялись непрерывно друг за другом, то игрок просто не успел бы ничего сделать.

Поэтому, показав игроку текущее состояние, игра должна создать задержку, перед тем как показать следующее. Именно эту задержку и требовала игра "Змейка". То есть игрок мог сам выбрать, насколько быстро будут сменяться состояния игры.

Однако реализация такой задержки в программе является отдельной задачей. Компьютеры БК, ДВК и МС-0511 обладали фиксированной тактовой частотой процессора и поэтому, если занять их процессор на какое-то количество тактов, то получится соответствующая по времени задержка. Эта задержка будет одинаковой для всех компьютеров одного семейства, потому что в них стоят одинаковые процессоры с одинаковой тактовой частотой.

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

for (i = 0; i < 10000; i++);

Этого хватало для небольших задержек. В случаях, когда нужно было сделать большую задержку, нужно было увеличивать количество повторений цикла, но оно было ограничено максимальным размером целого двухбайтного числа (unsigned int), то есть 65535. Если его было недостаточно, то спасал цикл внутри цикла:

for (i = 0; i < 10000; i++) for (j = 0; j < 10000; j++);

Вышеуказанные циклы создали бы уже 100 миллионов повторений.

Однако у данного подхода были две проблемы.

1. Разная тактовая частота

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

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

2. Отсутствие отклика программы

Когда в программе выполняется цикл, она фактически зависает и ни на что не реагирует. Это было не сильно заметно, так как длительность зависания была мала, и кроме того, программа всегда выполнялась в режиме полного владения компьютером, то есть в этот момент другие программы не работали, и поэтому не имело никакого значения, чем занят процессор.

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

Современное решение

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

Стало быть, вы просто задаёте нужную задержку в нормальных единицах времени, например 10 миллисекунд. Затем получаете текущее значение миллисекунд и прибавляете к нему задержку. Вы получили момент в будущем, когда задержка должна истечь. Затем в цикле вы получаете новое значение миллисекунд и проверяете, достигнут ли момент в будущем. Если достигнут, значит можно заканчивать цикл:

var delay = 10;
var future = get_milliseconds() + delay;
while(get_milliseconds() < future);

Здесь для получения миллисекунд я написал условную функцию get_milliseconds(), так как это зависит от языка.

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

Решить эту проблему можно следующими способами:

1. Полезная работа в цикле

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

while(get_millisecons() < future) do_something();

В том числе программа может опрашивать пользовательский ввод, что сделает её отзывчивой во время ожидания.

2. sleep()

В некоторых языках вы можете использовать функцию sleep() или usleep(). Она "усыпляет" программу на заданное количество милли- или микросекунд. Процессорное время отдаётся другим программам, и это самый лучший вариант для мультизадачной системы.

3. События

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

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

Дальше программа может ничего не делать или заниматься какими-то другими делами.

Объект-таймер обслуживается не программой, а вышележащей средой (скажем, для JavaScript это браузер). Поэтому отсчёт задержки в этом объекте происходит автономно, без участия программы, и эффективным способом, не вредящим остальным программам.

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

Дополнительно о событийной модели можно почитать в материале про игру Robots.