SteamDB

» » Урок 4. Отложенные задачи во времени

Урок 4. Отложенные задачи во времени

Введение

Итак, что у нас есть из lua-инструментов для времени в Tabletop Simulator?

Во-первых, это функции времени:
os.time() - это локальное unix-время с точностью до секунды.
os.clock() - время в секундах с точностью до миллисекунд с начала работы программы.
os.date() - для форматирования даты и времени в виде строки или таблицы.
os.difftime() - просто для вычисления разницы во времени.

Во-вторых, это функции для создания сопрограмм. В TTS функционал ограничен:
startLuaCoroutine(fn_owner, fn_name) - создать сопрограмму.
coroutine.yield(0) - ждать один фрейм (тик, кадр).
Вот и всё, что здесь умеют сопрограммы, никаких супер-пупер возможностей, которые обычно есть в Луа.

Я расскажу тебе про оба подхода. Но, кроме того, я хочу провести тебя по пути создания собственного инструмента. Ведь если ты программист, то имея os.clock ты уже можешь идти и делать свой инструмент (изобретать велосипед или брать существующий). Но если нет, то тебе, должно быть, интересно, КАК этот инструмент создаётся с нуля. А создаётся он постепенно, не сразу, проходя через несколько этапов. Каждый этап - это законченный рабочий инструмент. Мы идём от простого к сложному. И на каждом следующем этапе этот инструмент становится более совершенным.

ОглавлениеУрок 1. Скприты Lua для TTS
Урок 2. Инструменты
Урок 3. Раскраска API для Notepad++
Урок 4. Отложенные задачи во времени
Урок 5. Колоды и карты

Сопрограммы

Сначала попробуем выполнить какой-либо код через 5 секунд на основе сопрограмм.
function onload() time_finish = os.clock() + 5 --Прибавляем к текущему времени 5 секунд. startLuaCoroutine(Global, "co") end function co() while os.clock() < time_finish do --Ждём, пока время придёт. coroutine.yield(0) --Пропустить фрейм. end print("Прошло 5 секунд.") return 1 end
Это минимальный код, который необходим, чтобы отложить какую-либо задачу во времени. При этом:
  • startLuaCoroutine может не сработать (например, за пределами onload) и вернуть false
  • coroutine.yield(0) пропускает ровно 1 тик, не больше и не меньше, других вариантов нет. Функция coroutine.yield должна вызываться с параметром 0 и только так, это пропуск одного фрейма.
  • Нельзя передавать параметры основной программе. Разве что через глобальные переменные.
  • Нельзя передавать параметры сопрограмме. Опять же - только через глобальные переменные.
  • Функция co обязана возвращать 1 (такое, вот, правило придумали).
  • В целом решение не элегантно.

Через update

Функция update тоже вызывается каждый фрейм и не имеет дополнительных ограничений (то есть ничем не хуже предыдущего варианта). Лично мне больше нравится update.
local time_finish = os.clock() + 5 local done --Функция вызывается каждый тик. function update() if os.clock() < time_finish do --Ждём, пока время придёт. return --Пропустить фрейм. end if not done then print("Прошло 5 секунд.") done = true end end Здесь мы точно также пропускаем по фрейму, пока время не окажется нужным. Дополнительно здесь приходится помечать, что задача выполнена, потому что мы не можем остановить функцию update, да и не нужно этого делать.

Теоретически можно попробовать написать _G.update = nil, но обычно функция update нужна также и для других целей. Кроме того, мы хотим отложить не одну задачу на старте игры, а иметь более гибкий инструмент, не так ли? Так что просто немного усовершенствуем этот способ. Напишем обёртку вокруг update и os.clock в виде небольшого API.

В качестве желаемых функций мы хотим пока что просто:
  • DoTaskInTime(time_in_seconds, fn) - выполнить функцию через некоторое время.
local time_now = os.clock() local TASKS = {} local function DoTaskInTime(seconds, fn) --seconds здесь - через какое время выполнить задачу. local task = { fn = fn, act_time = time_now + seconds, } table.insert(TASKS,task) end --Функция вызывается каждый тик. Потом оптимизируем, а сейчас лень. function update() time_now = os.clock() --в секундах с точностью до миллисекунд. for i=#TASKS,1,-1 do local task = TASKS[ i ] if time_now > task.act_time then --Если время подошло. task.fn() --Выполняем функцию. table.remove(TASKS,i) --Удаляем задачу. end end end Интересное и простое решение. Теперь у нас есть функция для откладывания задач. Можно легко проверить этот инструмент, просто добавив в конец такой код:
DoTaskInTime(5, function() print("Через 5 секунд") end) Такая строчка очень в духе программирования на Луа - короткая и понятная. Если у тебя есть хоть небольшой опыт, то ты понимаешь, насколько это удобно. В итоге мы можем откладывать несколько задач одновременно с разными интервалами, и у нас нет накладных расходов на передачу управления движку Tabletop Simulator и всяким там якобы сопрограммам. Обращение к os.clock (системная функция, однако) происходит лишь единожды каждый тик, а дальше полученное значение используется для проверки времени всех задач. И если мы захотим "буксовать" каждые 50 тиков, то эта оптимизация коснется также и отложенных задач.

Но нам и этого мало, не так ли? :)

Периодические задачи

До этого мы хотели выполнять лишь одноразовые задачи. А что если мы хотим, чтобы задача выполнялась каждые N секунд? Тогда нам нужно что-то вроде:
DoPeriodicTask(5, function() print("Каждые 5 секунд.") end) Предыдущий инструмент не годится. Придётся его улучшить.
local time_now = os.clock() local TASKS = {} local function DoPeriodicTask(period, fn) local task = { period = period, fn = fn, act_time = time_now + period, } TASKS[task] = task return task end local function DoTaskInTime(seconds, fn) --seconds здесь - через какое время выполнить задачу. local task = { --period = period, fn = fn, act_time = time_now + seconds, } --table.insert(TASKS,task) TASKS[task] = task return task end local function CancelTask(task) if task then TASKS[task] = nil end end function update() time_now = os.clock() --в секундах с точностью до миллисекунд. for k,task in pairs(TASKS) do if time_now > task.act_time then --Время пришло. if task.period then --Повторяющаяся... task.act_time = time_now + task.period --Снова заводим таймер на то же время. else --Одноразовая... TASKS[k] = nil end task.fn() --Саму функцию не забываем выполнять. end end end Что ж, это уже довольно элегантное решение, хоть и громоздкое. Но мы же не будем вспоминать о том, как всё сложно. Вместо этого мы скопируем в начало скрипта и забудем, а в самом скрипте будем использовать лишь сами функции:
  • DoTaskInTime(seconds, fn)
  • task = DoPeriodicTask(seconds, fn)
  • CancelTask(task)

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

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

Можно пойти дальше. Например, усовершенствовать функции так, чтобы они передавали аргументы в отложенную функцию. Например так:
DoTaskInTime(5, function(a,b) print(a+b) end, 6, 7)
Типа должно вывести число 13 через 5 секунд. Здесь я уже не буду рассказывать, как улучшить наш инструмент, но это очень просто. Так что если тебе это нужно позарез - дерзай (это уже будет третий этап, и это не предел).

Сохранение и загрузка задач

Помним о том, что сохранению важно уделять внимание. Ведь админ может сохранить игру, чтобы продолжить её завтра. Также админ может нажать Ctrl+Z. Как быть с отложенными задачами?

Здесь есть два варианта для каждой задачи:
  1. Задача не сохраняется вообще, потому что это не нужно.
  2. Задачу нужно сохранить в момент сохранения и снова запустить после загрузки.

Первый вариантНаиболее удобен и прост. Задача просто перестаёт быть актуальной после загрузки из сейва.

Например, нужно проверить какой-то параметр некоторого объекта через 5 секунд. А после загрузки мы на всякий случай проверяем все объекты в игре. Таким образом, задача перестаёт быть актуальной. (Во время игры постоянно проверять все объекты может быть накладно, так что на помощь приходят отложенные задачи для некоторых объектов, которые трогали или которые нужно мониторить).

Если надо, эту задачу можно снова создать в onload или позже. Просто нужно понимать, что в любой момент игра может быть прервана с тем, чтобы продолжить потом. А также понимать, что именно будет происходить с твоими данными и твоей логикой в скрипте в случае save/load.

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

Например, это может быть удаление объекта с тем, чтобы заспаунить (создать) его через 5 секунд, или чтобы создать другой объект с другими параметрами. Здесь уже важно запомнить факт того, что у нас должок - мы ДОЛЖНЫ заспаунить объект. Если в эти самые 5 секунд произойдет сохранение игры, то информация о "долге" должна попасть в сейв. (Вообще пример притянут за уши, и такие вещи лучше делать одновременно, чтобы не было "долгов").

В каком виде сохранять задачу, если очень нужно? Очень просто: вычисляем время, которое осталось, и сохраняем именно его. При загрузке восстанавливаем задачу и оставшееся время. Это простая математика, я даже не буду изобретать код. Если даже такой код действительно понадобится, то он будет довольно специфичным, и тебе вполне по силам его написать.

Главное, помнить о том, что целостность игры должна быть восстановлена при загрузке из сейва. Какую-то информацию можно восстановить по расположению объектов на столе, их количеству и свойствам. Но информацию, которая хранится исключительно в переменных Луа, мы вынуждены сами передавать в сейв в момент сохранения. скачать dle 10.6фильмы бесплатно