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

Контекстные меню и интерфейс в Build 42

Добавляйте пункты в контекстные меню через события, создавайте окна ISUI и рассчитывайте их размеры для разных шрифтов и экранов.

Контекстные меню и интерфейс B42

Цель урока: добавлять пункты в контекстное меню без конфликтов с другими модами и создавать собственные окна, понимая связь таблицы Lua с Java-интерфейсом. Источник относится к Project Zomboid Build 42; точная минорная версия не указана.

Подключайтесь к контекстному меню, не заменяйте его

Игра строит меню правого клика в больших скриптах client/ISUI/ISInventoryPaneContextMenu.lua и ISWorldObjectContextMenu.lua, а в конце вызывает события для модов.

Объект правого кликаСобытиеАргументыМесто вызова в исходнике автора
Предметы инвентаряOnFillInventoryObjectContextMenuplayerNum, context, itemsISInventoryPaneContextMenu.lua:935
Объект мираOnFillWorldObjectContextMenuplayerNum, context, worldobjects, testISWorldObjectContextMenu.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). Для производительности обновляйте текст раз в секунду, не каждый кадр.