Перейти к содержанию

Постоянные данные и производительность Lua в Build 42

Сравните способы хранения данных модов, разберитесь с частыми событиями Lua и измеряйте производительность до и после оптимизации.

Постоянные данные и производительность Lua в Build 42

Руководство сравнивает способы хранения данных объекта, мода, установки и текущего сеанса, описывает стоимость частых операций в Kahlua и предлагает измерять результат до и после оптимизации. Страница заявляет Build 42 без указания точной минорной версии.

Где хранить данные

Что нужно хранитьГдеС чем сохраняется
Данные отдельного объекта (предмета, мебели, зомби или игрока)obj:getModData()С объектом в сохранении игры.
Данные мода в целом (счётчики, журналы, состояние мира)ModData.getOrCreate("CursoLab")В global_mod_data.bin в каталоге сохранения.
Данные этой установки, а не сохранения (настройки, положение окна)getFileWriter / getFileReaderВ файле под ~/Zomboid/Lua/.
Данные только на время текущего сеансаОбычная таблица LuaНе сохраняются и пропадают при выходе.

По объяснению автора, getModData() и ModData возвращают таблицы Lua, которые Java сериализует через KahluaTableImpl.save. Сохраняются текстовые или числовые ключи и текстовые, числовые, логические значения и вложенные таблицы.

Другие значения, например IsoPlayer, предмет или функция, при сохранении молча пропускаются методом canSave. Чтобы «запомнить» объект, сохраняйте его идентификатор: например getID() предмета, имя пользователя или координаты объекта мира.

getModData() создаёт и сохраняет таблицу в объекте, если её ещё нет. В источнике приведён такой байткод:

1: getfield table 4: ifnonnull 17 // si ya hay tabla, la devuelve 11: J2SEPlatform.newTable() 14: putfield table // si no, CREA una y la guarda en el objeto 18: getfield table 21: areturn

Поэтому вызов getModData() только ради проверки наличия данных может создавать пустую таблицу у каждого проверяемого объекта. Автор предлагает hasModData(), который возвращает true, только если таблица существует и не пуста. Следующий пример взят из неопубликованного мода автора WarFactions, файл WowFactions.lua; это атрибутированный пример, а не независимо проверенная рекомендация:

-- Camino caliente: OnZombieUpdate llama esto para CADA zombi del mapa en CADA -- frame. hasModData() es una consulta barata y descarta de inmediato a los zombis -- de vanilla (no tienen modData) sin crear la tabla con getModData(). if actor.hasModData ~= nil then local okH, has = pcall(actor.hasModData, actor) if okH and has == false then return nil end end

Добавляйте к ключам префикс, например md.CursoLab_nivel, или объединяйте данные в подтаблицу md.CursoLab = { nivel = 2 }; ModData объекта используют разные моды. Сохраняйте и версию формата, например md.CursoLab.v = 2, чтобы мигрировать старые сохранения после изменения структуры.

Автор указывает, что OnInitGlobalModData(isNewGame) вызывается при загрузке глобальных данных, и предлагает создавать или мигрировать таблицы в этом обработчике. Пример приведён без изменений:

Events.OnInitGlobalModData.Add(function(isNewGame) local d = ModData.getOrCreate("CursoLab") d.v = d.v or 1 d.sopaipillasComidas = d.sopaipillasComidas or 0 end)

Стоимость операций в Kahlua

Kahlua — интерпретатор на JVM. В статье перечислены наиболее затратные операции: часто обходить множество объектов на каждом tick; создавать внутри циклов таблицы, объединять строки или создавать closures, увеличивая работу сборщика мусора Java; вызывать методы Java в больших циклах; обращаться к глобальным переменным, которые автор считает медленнее локальных.

Выбрать подходящее событие

СобытиеЧастота по источникуЧисло использований в базовой игре по источнику
OnZombieUpdateКаждый загруженный зомби, каждый кадр0
OnTick, OnPlayerUpdate, OnRenderTickКаждый кадр (или кадр для каждого игрока)22, 3, 3
EveryOneMinuteКаждая игровая минута2
EveryTenMinutes, EveryHours, EveryDaysКаждые 10 игровых минут, час или день7, 4, 4
События действий, например OnFillContainer, OnClientCommand, OnEat…Только при соответствующем событииБольшинство

Автор утверждает, что базовая игра не использует OnZombieUpdate и обращается к OnTick только 22 раза среди тысяч файлов. Если нужен OnTick, он предлагает сначала найти более узкое событие. Если проверять состояние всё же приходится часто, пример ограничивает частоту максимум одним вызовом в секунду:

local last = 0 Events.OnTick.Add(function() local now = getTimestampMs() if now - last < 1000 then return end -- una vez por segundo como mucho last = now ... end)

Для проверки 300 объектов можно обрабатывать по 20 за tick, а не все 300 каждые 15 ticks: общий объём работы похож, но пиков меньше. В часто вызываемых событиях автор советует как можно раньше отсеивать ненужные случаи. По его словам, WarFactions проверяет зомби каждые 15 кадров через Fence.observeEvery = 15 и отбрасывает обычных зомби вызовом hasModData().

В статье говорится, что таблица, где ключом служит Java-объект (например cache[zombi] = ...), может удерживать объект после удаления из мира и расти без ограничения. Автор утверждает, что слабые ключи Lua в Kahlua это не исправляют: таблицы реализованы как Java LinkedHashMap, а описанный .jar не обрабатывает __mode. Эти сведения относятся к реализации, исследованной автором.

Предложенная ручная очистка: удалить запись при смерти зомби и периодически проверять объекты, исчезнувшие из мира. Пример автора:

local cache = {} Events.OnZombieDead.Add(function(zombi) cache[zombi] = nil -- el caso común: se borra al morir end) local tick = 0 Events.OnTick.Add(function() tick = tick + 1 if tick % 600 ~= 0 then return end -- cada ~10 segundos for z in pairs(cache) do -- los que salieron del mundo sin morir if not z:isExistInTheWorld() then cache[z] = nil end end end)

По утверждению источника, удалять элементы во время обхода pairs безопасно в Kahlua, поскольку итератор KahluaTableImpl использует копию ключей.

Измерять до оптимизации

  • Ручной таймер: вызвать getTimestampMs() до и после, накапливать результаты и выводить их раз в заданное число вызовов.
  • Медленные события: в параметрах отладки Checks → SlowLuaEvents игра может выводить предупреждение, если обработчик события занимает больше 250 мс. В статье это предлагается для поиска серьёзных задержек, а не точной настройки.
  • Профайлер игры: по источнику Event.trigger оборачивает событие в GameProfiler.profile("Lua - <evento>"). Ищите в меню отладки способ включить GameProfiler.Enabled.

Упражнения

Упражнение 14.1 · Запоминающий холодильник

Сделайте так, чтобы каждый холодильник запоминал, сколько раз игрок его открывал, и показывал число в контекстном меню холодильника. Используйте ModData объекта, синхронизируйте его (модуль 12) и проверьте сохранение после выхода и повторного входа. Упражнение выполнено, если число сохраняется, а проверка никогда не открывавшихся холодильников не создаёт им ModData; проверьте это с hasModData().

Упражнение 14.2 · Что не сохраняется

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

Упражнение 14.3 · Измерить частое событие

  1. Зарегистрируйте в OnZombieUpdate умеренно затратную функцию, например чтение расстояния до игрока и сохранение в таблицу.
  2. Измерьте getTimestampMs() общее время работы в секунду при 10, 100 и 500 зомби; статья предлагает создать их через меню отладки.
  3. Оптимизируйте ранним выходом, меньшим числом вызовов Java и проверкой раз в несколько кадров; измерьте повторно.

По критерию из источника, нужно получить таблицу до и после и определить, сколько зомби выдерживает каждая версия без падения ниже 60 FPS.

Упражнение 14.4 · Утечка

Намеренно создайте cache[zombi] = true, дайте зомби появляться и исчезать и выведите число записей. Повторите с setmetatable(cache, { __mode = "k" }) и проверьте, меняется ли результат. Затем добавьте ручную очистку из раздела 3 и сравните.

Упражнение 14.5 · Миграция формата

В версии 1 глобальная ModData CursoLab имела вид { comidas = 5 }. Теперь нужна структура { v = 2, comidas = { total = 5, porJugador = {} } }. Напишите миграцию в OnInitGlobalModData и проверьте её на сохранении версии 1.

Задача со звёздочкой 14

Создайте профайлер для собственных модов: CursoLab.prof(nombre, fn) оборачивает функцию, накапливает число вызовов и общее время, а окно из модуля 10 показывает таблицу, отсортированную по времени и обновляемую каждую секунду. Используйте её для поиска самой затратной функции большого мода.

Информация
Технические утверждения о сериализации, стоимости вызовов, событиях, Java-классах, итераторах и профайлерах относятся к контексту B42 в источнике; они не проверялись в игре и не сверялись с актуальным кодом движка. Дата страницы — 12 октября 2026 года — указана в её метаданных. Пять исходных фрагментов кода сохранены буквально; игра и упражнения не запускались.