Контекстные меню и интерфейс в Build 42
Контекстные меню и интерфейс B42
Цель урока: добавлять пункты в контекстное меню без конфликтов с другими модами и создавать собственные окна, понимая связь таблицы Lua с Java-интерфейсом. Источник относится к Project Zomboid Build 42; точная минорная версия не указана.
Подключайтесь к контекстному меню, не заменяйте его
Игра строит меню правого клика в больших скриптах client/ISUI/ISInventoryPaneContextMenu.lua и ISWorldObjectContextMenu.lua, а в конце вызывает события для модов.
| Объект правого клика | Событие | Аргументы | Место вызова в исходнике автора |
|---|---|---|---|
| Предметы инвентаря | OnFillInventoryObjectContextMenu | playerNum, context, items | ISInventoryPaneContextMenu.lua:935 |
| Объект мира | OnFillWorldObjectContextMenu | playerNum, context, worldobjects, test | ISWorldObjectContextMenu.lua:213 |
Комментарий игры возле второго события советует использовать событие, как OnTick, чтобы добавлять пункты меню без конфликтов модов. Для этого не оборачивайте createMenu — используйте событие.
Пример Pumpkin Z DIY
В Pumpkin Z DIY callback ищет тыкву среди выбранных объектов. Аргумент items может содержать как объект предмета Java, так и таблицу стопки с items = {...}; код извлекает первый предмет из стопки.
local function onFillInventoryMenu(playerNum, context, items)
local player = getSpecificPlayer(playerNum)
if not player or player:isDead() then return end
local pumpkin
for _, v in ipairs(items) do
local item = v
if type(v) == "table" and v.items then item = v.items[1] end -- ← ojo con esto
if item and item.getFullType and item:getFullType() == PUMPKIN then
pumpkin = item
break
end
end
if not pumpkin then return end
local knife = sharpKnife(player)
local option = context:addOption(getText("IGUI_PumpkinZ_CarveMenu"), player, PumpkinZ.openCarveUI, pumpkin, knife)
if not knife then
option.notAvailable = true -- se ve en gris
local tip = ISInventoryPaneContextMenu.addToolTip()
tip.description = getText("IGUI_PumpkinZ_NeedKnife") -- y explica por qué
option.toolTip = tip
end
end
Events.OnFillInventoryObjectContextMenu.Add(onFillInventoryMenu)
Пример сначала получает игрока через getSpecificPlayer(playerNum) (playerNum — номер, важен для split-screen), прекращает обработку при отсутствии игрока или его смерти, ищет тыкву и нож. Если ножа нет, пункт не скрывается: notAvailable = true отображает его серым, а tooltip объясняет причину. Так игрок понимает, чего не хватает.
Общий вид addOption и его аргументы:
context:addOption(texto, target, funcion, arg1, arg2, ...) -- hasta 10 argumentos
При выборе функция вызывается как funcion(target, arg1, arg2, ...); если передан метод MiClase.metodo, target становится self. Дополнительные методы ISContextMenu: addOptionOnTop, insertOptionAfter(nombreAnterior, ...), removeOptionByName, getNew(context) вместе с addSubMenu(opcion, submenu) и addDebugOption (только в debug).
Меню мира иногда строится только для проверки, появятся ли пункты (например, для контроллера), и получает test = true. Источник отмечает 35 игровых условий вида if test then return true end. Если callback открывает окно или выполняет затратную работу, учитывайте test.
Как устроено окно
У элемента интерфейса две половины: Lua-таблица (например, PumpkinZCarveUI, производная ISPanel) и Java-объект zombie.ui.UIElement.
Lua: таблица PumpkinZCarveUI (производная от ISPanel) Java: zombie.ui.UIElement
.x .y .width .height ◄──────► положение, размер, фокус, отрисовка
:prerender() :render() :onMouseDown() ... ◄──вызов── по имени, каждый кадр / событие
.javaObject ──────────────────────────────────────────────►
ISUIElement:instantiate() создаёт Java-часть через self.javaObject = UIElement.new(self) и передаёт ей таблицу Lua. После этого Java вызывает по имени функции таблицы — prerender, render, update, onMouseDown, onMouseUp, onMouseMove, onMouseWheel, onRightMouseDown, onKeyPress, onResize и другие. Если метода с таким именем нет, вызова нет. addToUIManager() вызывает instantiate() при необходимости и регистрирует элемент в UIManager; addChild(otro) делает это для дочернего элемента.
Жизненный цикл окна
Пример показывает создание таблицы и размещение методов по этапам:
MiVentana = ISPanel:derive("MiVentana")
function MiVentana:new(x, y, w, h) -- 1. crear la tabla: solo datos, nada de Java todavía
local o = ISPanel.new(self, x, y, w, h)
o.backgroundColor = { r = 0, g = 0, b = 0, a = 0.8 }
o.moveWithMouse = true
return o
end
function MiVentana:initialise() -- 2. crear hijos (botones, listas)
ISPanel.initialise(self)
self.boton = ISButton:new(10, 10, 100, 25, "OK", self, MiVentana.onOK)
self.boton:initialise()
self:addChild(self.boton)
end
function MiVentana:prerender() -- 3. cada frame: el fondo
ISPanel.prerender(self)
end
function MiVentana:render() -- 4. cada frame: lo que va encima
self:drawText("Hola", 10, 50, 1, 1, 1, 1, UIFont.Small)
end
function MiVentana:close() -- 5. salir
self:setVisible(false)
self:removeFromUIManager()
end
-- abrirla:
local v = MiVentana:new(100, 100, 300, 200)
v:initialise()
v:addToUIManager()
new() создаёт таблицу и задаёт данные, до создания Java-объекта. initialise() создаёт дочерние элементы. prerender() рисует фон каждый кадр, render() — содержимое поверх фона. close() скрывает окно и удаляет его из UIManager. Для открытия вызовите new, initialise и addToUIManager.
Pumpkin Z DIY хранит единственную открытую instance: если она уже существует, закрывает её перед открытием следующей. Иначе каждый клик создаёт ещё одно окно поверх прежнего. Некоторые элементы, например ISCollapsableWindow, создают дочерние элементы в createChildren(), которое игра вызывает при instantiate(); проверьте, какой метод использует родительский класс.
Полезные элементы из client/ISUI/: ISPanel (базовая панель), ISCollapsableWindow (окно с заголовком, перемещаемое и закрываемое), ISButton, ISLabel, ISTickBox, ISComboBox, ISTextEntryBox, ISScrollingListBox (длинные списки), ISRichTextPanel (форматированный текст) и ISModalDialog (да/нет). Перед созданием изучите похожее окно игры.
Размер шрифта и экрана
Игрок может изменить размер шрифта, поэтому интерфейс с фиксированными высотами в пикселях ломается при увеличенном тексте. Игра измеряет шрифт и строку:
local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small) -- al inicio del archivo
local ancho = getTextManager():MeasureStringX(UIFont.Small, texto) -- 733 usos en el juego
- Высоту кнопок и строк вычисляйте как высоту шрифта плюс поля, а не фиксированным числом.
- Ширину кнопок считайте по измеренной строке плюс поля: переводы, например на португальский, могут быть длиннее английских.
- Для центрирования используйте getCore():getScreenWidth() и getCore():getScreenHeight(), как в Pumpkin Z DIY.
Рисование
В prerender/render координаты задаются относительно элемента интерфейса.
| Функция | Назначение и порядок аргументов |
|---|---|
| drawRect(x, y, w, h, a, r, g, b) / drawRectBorder(...) | Прямоугольники; сначала alpha |
| drawText(txt, x, y, r, g, b, a, font), drawTextCentre, drawTextRight | Текст; alpha последним |
| drawTexture(tex, x, y, a, r, g, b), drawTextureScaled(tex, x, y, w, h, a, r, g, b) | Изображения; используйте getTexture(...) из модуля 04 |
Порядок цветовых параметров у drawRect и drawText различается. Если текст невидим, проверьте порядок аргументов.
Упражнения
10.1 Пункт с условиями
Добавьте в контекстное меню еды пункт «Ver datos (CursoLab)», который появляется только для еды. Для испорченного продукта пункт должен быть неактивным и объяснять причину в подсказке. При выборе выведите в консоль насыщение голода, дни свежести и состояние. Упражнение выполнено, если это работает и для отдельного предмета, и для стопки из пяти, но пункт не появляется у инструментов.
10.2 Подменю мира
Добавьте при правом клике по миру подменю «CursoLab» с тремя пунктами: координаты, тип поверхности и наличие зомби поблизости. Используйте worldobjects, чтобы получить клетку (у каждого объекта есть getSquare()). Убедитесь, что учитывается test.
10.3 Первое окно
Создайте CursoLab.VentanaBitacora на основе ISCollapsableWindow: покажите последние 20 записей журнала (упражнение 6.2) в ISScrollingListBox, добавьте кнопки «Записать в файл» и «Очистить». Открывайте горячей клавишей через Events.OnKeyPressed и Keyboard.KEY_.... Окно должно открываться, перемещаться и закрываться; не создавать дубликаты; оставаться читаемым при крупном шрифте.
10.4 Разобрать окно игры
Выберите окно в client/ISUI/, нарисуйте дерево его элементов: какие дочерние элементы и в какой функции создаются, какие render-функции используются. Найдите незнакомый приём.
10.5 Измерять до отрисовки
Переделайте окно из упражнения 10.3 так, чтобы его размеры не задавались фиксированными числами: вычисляйте их через getFontHeight и MeasureStringX. Проверьте испанский, английский и увеличенный шрифт.
Задача повышенной сложности
Создайте небольшую прозрачную мини-HUD в углу экрана с игровым временем, температурой и весом переносимого груза. Её можно перемещать мышью; позиция сохраняется между сессиями, например через getFileWriter (модуль 06) или ModData (модуль 14). Для производительности обновляйте текст раз в секунду, не каждый кадр.