Постоянные данные и производительность Lua в Build 42
Постоянные данные и производительность 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 · Измерить частое событие
- Зарегистрируйте в OnZombieUpdate умеренно затратную функцию, например чтение расстояния до игрока и сохранение в таблицу.
- Измерьте getTimestampMs() общее время работы в секунду при 10, 100 и 500 зомби; статья предлагает создать их через меню отладки.
- Оптимизируйте ранним выходом, меньшим числом вызовов 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 показывает таблицу, отсортированную по времени и обновляемую каждую секунду. Используйте её для поиска самой затратной функции большого мода.