Сделай это ещё раз: циклы¶
Что такое «цикл»?¶
Во всех предыдущих скриптах поток выполнения шёл строго сверху вниз. Команды перечислялись в определённом порядке и выполнялись одна за другой, иногда пропуская часть, если встречалась команда if.
А что если вы захотите выполнить только что написанные команды ещё раз? У вас есть несколько вариантов. Самый прямолинейный — просто написать эти же команды снова. Это сработает, если нужно повторить всего дважды, или если повторить нужно всего одну строку, — но в целом такой подход очень быстро становится неуправляемым.
К счастью, в Denizen есть три разные команды циклов, которые повторяют блок команд:
repeat— повторяет блок заданное число раз;foreach— повторяет блок по списку на входе, по одному разу на каждый элемент списка;while— повторяет блок, пока выполняется условие, — как командаif, которая крутится снова и снова, пока условие истинно.
Команда Repeat¶
Самое базовое применение цикла — просто запустить блок команд ещё раз, и для этого лучше всего подходит команда repeat.
Команда repeat, как и следует из её названия, повторяет блок команд несколько раз.
Базовый формат команды repeat:
- repeat (number of times):
- (commands here)
По структуре это похоже на команду if: repeat тоже завершается символом :. Команды, которые должны попасть в цикл, нужно сдвинуть отступом внутрь, чтобы Denizen понимал, что именно их нужно выполнять в цикле.
Вот как это может выглядеть в настоящем скрипте:
my_lightning_task:
type: task
script:
- repeat 5:
- strike <player.location>
- narrate "you have been struck by lightning!"
- wait 1s
- narrate "no more lightning"
Попробуйте выполнить его в игре командой /ex run my_lightning_task.
Цикл выполнится 5 раз: каждый раз по игроку будет бить молния, и каждый раз он будет получать сообщение «you have been struck by lightning!».
В конце блока стоит команда wait, которая приостанавливает скрипт на заданное время (в данном случае на 1 секунду). Во многих случаях это важно, чтобы команды не выполнялись друг за другом мгновенно. В этом примере без wait все удары молнии выглядели бы так, как будто они произошли одновременно.
После того как цикл заканчивается, скрипт продолжается — в данном случае выводится сообщение «no more lightning».
А что если вы хотите сообщить игроку, сколько раз его уже ударило? Можно было бы добавить определение, которое считает итерации, но repeat уже делает это за вас. Достаточно добавить к команде repeat аргумент as::
my_lightning_task:
type: task
script:
- repeat 5 as:count:
- strike <player.location>
- narrate "you have been struck by lightning <[count]> times!"
- wait 1s
- narrate "no more lightning"
Теперь narrate будет писать «you have been struck by lightning 1 times!», «you have been struck by lightning 2 times!» и так далее, до 5.
Команда Foreach¶
Команда repeat удобна, но очень часто вам нужно перебрать содержимое списка и что-то сделать с каждым его элементом.
Допустим, мы хотим сообщить игроку, где находятся все коровы вокруг него.
С помощью repeat можно было бы сделать так:
my_cow_task:
type: task
script:
- define cows <player.location.find_entities[cow].within[30]>
- repeat <[cows].size> as:index:
- define cow <[cows].get[<[index]>]>
- narrate "There's a cow just <[cow].location.distance[<player.location>].round> blocks away!"
- playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3
Однако есть способ куда лучше — с помощью команды foreach.
Название «foreach» буквально означает «для каждого элемента списка …» — и команда работает ровно так, как подсказывает её имя: повторяет блок команд по одному разу на каждый элемент списка.
У foreach та же структура, что и у repeat:
- foreach (some list) as:(definition to store each entry):
- (commands here)
Вот как можно упростить предыдущий скрипт с помощью foreach:
my_cow_task:
type: task
script:
- foreach <player.location.find_entities[cow].within[30]> as:cow:
- narrate "There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
- playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3
Попробуйте в игре, постояв рядом с парой коров, выполнить /ex run my_cow_task.
Куда лучше. Главное — нам больше не нужно вручную доставать элемент cow из списка: он уже лежит в определении.
Кроме того, у foreach есть встроенное определение <[loop_index]>, которое считает, сколько итераций уже прошло. Вот пример его использования:
my_cow_task:
type: task
script:
- foreach <player.location.find_entities[cow].within[30]> as:cow:
- narrate "<[loop_index]> - There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
- playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3
Теперь в начале каждого narrate будет добавляться «1-», «2-» и так далее по мере прохождения цикла.
Команда While¶
Бывают ситуации, когда вы заранее не знаете, сколько раз нужно пройти по циклу, но точно знаете, когда пора остановиться. Здесь поможет цикл while. Он работает как команда if, которая снова и снова выполняет блок ниже, пока условие истинно.
- while (condition):
- (commands here)
Формат условия — такой же, как у команды if; подробности о её структуре можно посмотреть на соответствующей странице руководства.
А теперь — пример цикла while:
my_move_task:
type: task
script:
- define location <player.location>
- while <player.is_online> && <[location].distance[<player.location>]> < 3:
- narrate "You're too close, move away!"
- wait 2s
Попробуйте выполнить в игре /ex run my_move_task. Этот task при запуске будет раз за разом напоминать вам отойти, пока вы не отойдёте хотя бы на 3 блока от точки, в которой находились в начале.
Как и в команде repeat, в конце цикла стоит wait. В repeat это нужно не всегда, но в while использовать wait почти всегда обязательно. Без него условие будет проверяться снова и снова без перерыва до тех пор, пока цикл не закончится, — и сервер может зависнуть на всё это время или даже упасть, если цикл так и не остановится.
Предупреждение: циклов while по возможности стоит избегать. Очень легко случайно получить так называемый «бесконечный цикл» — то есть цикл, у которого нет шанса завершиться. Когда он запущен, остановить его можно, по сути, только выключив сервер.
Кроме того, некоторые пользователи пишут while true ради скриптов, которые намеренно работают бесконечно. Это не лучший подход: обычно для этого стоит использовать событие delta time.
Остановка цикла¶
Иногда в цикле нужно продолжать итерации только до определённого момента, но при этом не хочется останавливать весь скрипт целиком. Команда stop останавливает скрипт, а repeat/foreach/while stop — соответствующий цикл.
Например, в первом примере мы хотели бы прекратить бить молнией, если у игрока осталось меньше 5 единиц здоровья. Это делается так:
my_lightning_task:
type: task
script:
- repeat 5 as:count:
- strike <player.location>
- narrate "you have been struck by lightning <[count]> times!"
- if <player.health> < 5:
- repeat stop
- wait 1s
- narrate "no more lightning"
Поскольку мы используем repeat stop, завершается только цикл; завершающая строка narrate всё равно выполнится.
То же самое работает и в циклах foreach. Например, если мы хотим прервать перебор коров, как только нашли телёнка:
my_cow_task:
type: task
script:
- foreach <player.location.find_entities[cow].within[30]> as:cow:
- narrate "There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
- playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3
- if <[cow].is_baby>:
- narrate "omg its a baby!!"
- foreach stop
Следующий, пожалуйста¶
Бывают случаи, когда вы хотите перейти сразу к следующей итерации цикла, не дорабатывая текущую. Для этого существует команда repeat/foreach/while next.
Возьмём тот же пример с foreach и предположим, что мы хотим пропускать всех телят. Можно обернуть весь блок в if…
my_cow_task:
type: task
script:
- foreach <player.location.find_entities[cow].within[30]> as:cow:
- if !<[cow].is_baby>:
- narrate "<[loop_index]> - There's a cow just <[cow].location.distance[<player.location>].round> blocks away!"
- playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3
…а можно вместо этого использовать next:
my_cow_task:
type: task
script:
- foreach <player.location.find_entities[cow].within[30]> as:cow:
- if <[cow].is_baby>:
- foreach next
- narrate "There's a cow <[cow].location.distance[<player.location>].round> blocks away!"
- playeffect effect:fireworks_spark at:<[cow].location> visibility:50 quantity:100 data:0 offset:3