000154. Вся правда о wait 0

Вся правда о wait 0|Многие из скриптеров, прочитав название статьи, подумают - "А чего там знать? Просто нужно не забывать ставить wait 0 после каждой метки и всё!". Но, к сожалению, они окажутся неправы.|BoPoH|BoPoH||||Подавляющая часть скриптеров училась либо по справке к SB, либо у тех, кто учился по этой справке. В конечном итоге, они используют wait 0 после каждой метки или в каждом цикле, ведь так делалось в примерах в справке, а справка "никогда не врёт". Но тут стоит обратить внимание на то, что автор справки ( Alexander, насколько я знаю написал большую её часть ) сам мало знал о том, для чего на самом деле предназначен опкод задержки.

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

На самом деле, такого понятия как параллельность не может существовать в рамках одного процессора ( процессорного ядра ). Все "движки" игры выполняются последовательно. Если использовать синтаксис SB, то это будет выглядеть примерно так ( на номера опкодов внимания не обращаем ):

while true 
 0001: process_animations
 0002: process_physics
 0003: process_AI
 0004: process_scripting
 0005: process_render
end

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

Итак, вернёмся к нашему "wait 0". Предположим, выполнение кода игры дошло до процесса скриптинга. Наш скрипт начал выполняться. Возьмём такой пример, пока что на основе меток:

{$CLEO}
0000: NOP // Чтобы SB не ругался на переход на нулевой оффсет

:label_1
if
0AB0: key_pressed 48
then
 actor.Health($PLAYER_ACTOR) = 100
end
wait 0
jump @label_1

Как только наш скрипт начал выполняться, мы попадаем на проверку нажатия клавиши 0`. Если клавиша нажата - даём игроку полное здоровье и идём дальше. Если не нажата - просто идём дальше. Дальше у нас стоит "wait 0". Обычно его пишут в начале, сразу после метки или перед проверкой, но я специально поставил его в конец. Когда скриптовый процесс достигает опкода "wait", он получает задачу переключиться на выполнение следующего скрипта, или, если это был последний скрипт в очереди - закончить скриптовый процесс и приступить к следующему игровому процессу.

Как мы все прекрасно знаем, если убрать "wait 0" в такой конструкции, то игра зависнет. Вообще, любое зависание любой программы с большой нагрузкой на ЦП - это, чаще всего, попадание в бесконечный цикл. Так и здесь. Так как опкод задержки является своеобразным "сигналом" для скриптового процесса для переключения на следующие процессы игры, то его отсутствие будет равносильно бесконечному выполнению только одного скриптового движка. Проще говоря, скриптовый процесс будет бесконечно проверять, нажата ли наша кнопка, а другие игровые процессы выполняться не будут ( такие как рендер новых кадров, например ), поэтому нам кажется, что игра зависла, но на самом деле она работает, но расчёты физиких моделей, их анимации, а также вывод новых "картинок" на экран не производятся.

"Разве плохо ставить wait 0 после каждой метки?" - спросите вы. Не всегда это плохо. Но давайте разберём случай, когда "wait 0" после каждой метки будет лишним. К примеру, нам нужно проверить несколько клавиш, на каждую из которых предусмотрено определённое действие.

{$CLEO}
0000: NOP

:label_1
wait 0
if
0AB0: key_pressed 48
then
 actor.Health($PLAYER_ACTOR) = 10
else
 jump @label_2
end
jump @label_1

:label_2
wait 0
if
0AB0: key_pressed 49
then
 actor.Health($PLAYER_ACTOR) = 20
else
 jump @label_3
end
jump @label_1

:label_3
wait 0
if
0AB0: key_pressed 50
then
 actor.Health($PLAYER_ACTOR) = 30
else
 jump @label_1
end
jump @label_1

Итак, давайте посмотрим, как будет выполняться наш код. Сперва метка label_1 и "wait 0". Пропустим их. Дальше проверка на нажатие клавиши 0` и соответствующее ей действие - установка игроку здоровья равному 10. Если клавиша не нажата - переходим на label_2. Тут у нас снова "wait 0", который передаст управление игрой другим процессам, и только после их выполнения вернётся к скрипту, к месту после "wait 0". Тут снова проверка на нажатие клавиши и соответствующее действие. Если не нажата - идём к третьей метке. Здесь опять же "wait 0", после которого выполнение скрипта прервётся, чтобы выполнить другие процессы и, после этого - вернётся обратно в это же место. Таким образом, прежде чем мы дойдём до проверки третьей клавиши, у нас образуется задержка в 3 общих цикла игры ( будем называть их кадрами ).

Вы скажете - "Но ведь мы написали wait 0, а раз там ноль, значит задержки никакой не будет!". Но даже если там указан ноль, процессор не успеет настолько быстро обработать все остальные процессы игры, чтобы за 0 миллисекунд вернуться обратно к выполнению скриптового процесса. Таким образом, создаётся задержка в несколько миллисекунд - примерно 10-20 мс при 60 фпс. Три таких задержки равны уже 60 мс. А что, если таких проверок много? Придётся зажимать клавишу на полсекунды-секунду, чтобы её нажатие обработалось.

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

{$CLEO}
0000: NOP

while true
 33@ = 0
 wait 0
 0AD1: show_formatted_text_highpriority "%d" time 2000 33@
end

Как мы знаем из той же справки, переменные 32@ и 33@ являются таймерами и изменяют своё значение после каждого цикла ( "wait 0" ) на такое кол-во миллисекунд, которое прошло с момента начала прошлого цикла. Перед "wait 0" мы обнуляем значение переменной, а после - выводим её значение на экран, получая ту самую задержку, создаваемую опкодом "wait 0".

В результате, в скрипте с тремя проверками, выше, можно убрать "wait 0" после второй и третьей метки - тогда все проверки выполнятся подряд, без задержек, а после проверок код всё равно вернётся к метке label_1 где стоит "wait 0". Таким образом мы убъём двух зайцев сразу - избежим зависания и уберём задержку между проверками.

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

for 0@ = 0 to 20 step 1
 if
 actor.Defined($ACTORS[0@])
 then
 1@ = actor.Health($ACTORS[0@])
 if
 1@ < 100
 then
 actor.DestroyInstantly($ACTORS[0@])
 actor.RemoveReferences($ACTORS[0@])
 end
 end
end

Я не вставил в эту конструкцию ни одного "wait", так как мы знаем, что цикл НЕ будет выполняться бесконечно, а лишь 20 раз, поэтому шансов на зависание в данном участке кода нет. Если мы вставим в конструкцию "wait 0", то "поиск раненого" будет длиться дольше из-за задержек, создаваемых "wait 0".

Если взять бесконечные циклы, такие как "while true", к примеру, то они должны в себе содержать "wait 0" - иначе мы снова попадём в зависание.

Сами создатели игры - Rockstar конечно тоже использовали "wait 0" по назначению, поэтому можно у них поучиться, посмотрев строение скриптов-потоков в оригинальном "main.scm".

Таким образом мы должны наблюдать за тем, как выполняется код и следить за тем, чтобы не создавалось бесконечных циклов, в которых скрипт не будет давать возможности выполняться другим процессам игры. В то же время, нужно исключить все лишние задержки, которые могут навредить работе вашего скрипта.|1777|315|0||vsja_pravda_o_wait_0|1504596516

Last updated