- Поддерживаемые версии
- 1.16
- 1.17
- 1.18
- 1.19
- 1.20
- 1.21
Начнем с того что многие все еще не умеют создавать нормальные менюшки и делает те же самые проверки по title, не соблюдают структуру классов и позже данный код невозможно дополнять/читать. А также этот гайд будет очень полезен для новичков которые только начинают свое знакомство с Bukkit и его обитателями.
Почему это важно?
Что здесь происходит:
UPD:
Не забудьте прописать команду в plugin.yml и зарегистрировать её в
Шаг 1: Основа меню (BaseMenu)
Используем интерфейс InventoryHolder - это современный стандарт. Он позволяет «пометить» инвентарь как наш собственный.
Java:
// Создаем именно абстрактный класс т.к в дальнейшем от него и будем наследовать остальные меню
public abstract class BaseMenu implements InventoryHolder {
private final Inventory inventory;
public BaseMenu(int size, Component title) {
this.inventory = Bukkit.createInventory(this, size, title);
}
@Override
public @NotNull Inventory getInventory() {
return this.inventory;
}
}
Почему это важно?
- Забудьте про проверку по Title: Сравнивать меню по заголовку - плохая практика. Если у игрока откроется обычный сундук с таким же названием, ваш код сработает неверно.
- Безопасность: Проверка через holder (который мы передали в createInventory) гарантирует, что событие клика относится именно к вашему классу, а не к случайному объекту.
Шаг 2: Упрощение данных (ClickEvent)
Стандартные события Bukkit содержат много лишней информации. Чтобы код в самих меню выглядел аккуратно, создадим Record Class. Это компактный класс-контейнер для хранения только нужных нам данных о клике.
Java:
// Создадим кастомный мини ивент чтоб нам самим было легче работать с менюшками
public record ClickEvent(Player player,
int slot,
ItemStack clickedItem,
ItemStack cursorItem,
ClickType clickType,
InventoryClickEvent event) {
}
Шаг 3: Функционал и регистрация кликов
Добавляем в BaseMenu возможность не просто ставить предметы, а сразу привязывать к ним действия.
Java:
// Храним слот и действие, которое должно выполниться при клике
private final Map<Integer, Consumer<ClickEvent>> clickActions = new HashMap<>();
public void setItem(int slot, ItemStack item, Consumer<ClickEvent> action) {
this.clickActions.put(slot, action); // Запоминаем действие
this.inventory.setItem(slot, item); // Ставим предмет
}
public void handleItemClick(ClickEvent event) {
// Если на этот слот назначено действие - выполняем его
if (clickActions.containsKey(event.slot())) {
clickActions.get(event.slot()).accept(event);
}
}
Что здесь происходит:
- Map<Integer, Consumer<ClickEvent>>: Это словарь, где ключ - номер слота, а значение - кусок кода (действие), который мы хотим запустить.
- setItem с Consumer: Теперь при создании меню можно написать: setItem(10, item, e -> event.player().sendMessage("Ку!"));. Это избавляет от огромных конструкций if-else в обработчике событий.
- handleItemClick: Метод-посредник, который вызывается при клике и запускает нужное действие из нашей карты.
- onClose: Маленький лайфхак с updateInventory(), чтобы избежать визуальных багов синхронизации при закрытии меню.
Шаг 4: Оживление меню (InventoryListener)
Этот класс перехватывает стандартные действия игрока и перенаправляет их в наше BaseMenu. Без этого слушателя меню будет просто картинкой, из которой можно воровать предметы.
Java:
public class InventoryListener implements Listener {
@EventHandler
public void onInventoryClickEvent(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) {
return;
}
InventoryHolder inventoryHolder = player.getOpenInventory().getTopInventory().getHolder();
if (!isBaseMenu(inventoryHolder)) {
return;
}
ItemStack currentItem = event.getCurrentItem();
if (currentItem == null || currentItem.getType().isAir()) {
return;
}
BaseMenu baseMenu = getBaseMenu(inventoryHolder);
event.setCancelled(true);
baseMenu.onClick(event);
baseMenu.handleItemClick(new ClickEvent(
player,
event.getRawSlot(),
currentItem,
event.getCursor(),
event.getClick(),
event));
}
@EventHandler
public void onCloseMenu(InventoryCloseEvent event) {
InventoryHolder inventoryHolder = event.getInventory().getHolder();
if (isBaseMenu(inventoryHolder)) {
BaseMenu baseMenu = getBaseMenu(inventoryHolder);
baseMenu.onClose(event);
}
}
@EventHandler
public void onDragInventory(InventoryDragEvent event) {
if (isBaseMenu(event.getInventory().getHolder())) {
event.setCancelled(true);
}
}
private BaseMenu getBaseMenu(InventoryHolder inventoryHolder) {
return (BaseMenu) inventoryHolder.getInventory().getHolder();
}
private boolean isBaseMenu(InventoryHolder inventoryHolder) {
return inventoryHolder instanceof BaseMenu;
}
}
Основные задачи слушателя:
- Фильтрация событий: Методы isBaseMenu и getBaseMenu проверяют, является ли открытый инвентарь нашим кастомным классом. Это критично: мы не должны отменять клики в обычных сундуках или верстаках.
- Безопасность (event.setCancelled(true)): Мы запрещаем игроку забирать предметы из меню. Это стандарт для любых GUI, чтобы иконки магазина не оказались в инвентаре игрока.
- Обработка клика (onInventoryClickEvent):
- Проверяем, что кликнул именно игрок.
- Убеждаемся, что слот не пустой.
- Создаем наш ClickEvent (тот самый Record из Шага 2) и передаем его в handleItemClick. Теперь код в самом меню «узнает», что на кнопку нажали.
- Защита от хитростей (onDragInventory): Новички часто забывают про этот ивент. Он срабатывает, когда игрок пытается «растянуть» стак предметов по слотам. Мы просто блокируем это для наших меню.
- Завершение работы (onCloseMenu): Вызывает метод onClose из твоего BaseMenu. Это полезно, если при закрытии меню нужно что-то сохранить или выдать игроку сообщение.
Шаг 5: Регистрация плагина (Menu)
Чтобы Bukkit начал отслеживать клики и открывать наши меню, нам нужно «включить» созданный InventoryListener при запуске плагина.
Java:
public final class Menu extends JavaPlugin {
@Override
public void onEnable() {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new InventoryListener(), this);
}
@Override
public void onDisable() {
}
}
Шаг 6: Пример использования (TestMenu)
Теперь создание любого меню занимает пару минут. Мы просто наследуем наш BaseMenu и описываем предметы.
Java:
public class TestMenu extends BaseMenu {
public TestMenu() {
// Указываем размер (27 слотов) и заголовок
super(27, Component.text("Тестовое меню"));
// Вариант 1: Ссылка на метод (чистый код)
setItem(1, new ItemStack(Material.RED_STAINED_GLASS), this::onClickRedPane);
// Вариант 2: Лямбда-выражение (быстро и удобно)
setItem(2, new ItemStack(Material.BARRIER), event -> {
event.player().sendMessage(Component.text("Вы нажали на барьер!"));
});
}
// Логика для красного стекла
private void onClickRedPane(ClickEvent event) {
event.player().sendMessage(Component.text("Привет! Это кнопка из метода."));
}
@Override
public void onClose(InventoryCloseEvent event) {
// Дополнительная логика при закрытии
event.getPlayer().sendMessage(Component.text("Меню закрыто!"));
}
}
- Никаких if-else: Вам не нужно проверять if (slot == 1). Логика привязана прямо к предмету.
- Гибкость: Вы можете использовать либо отдельный метод (this:
nClickRedPane), если логики много, либо короткую лямбду для простых действий.
Шаг 7: Упрощаем создание (MenuFactory)
Если твои меню требуют каких-то зависимостей (например, доступ к конфигу или базе данных), создавать их через new в каждом классе неудобно. Factory позволяет централизовать этот процесс. Иногда меню должно быть "умным" - например, показывать баланс конкретного игрока. Для этого нам нужно передать данные прямо в момент создания..
Java:
public class MenuFactory {
/**
* Создает меню с параметрами в конструкторе.
* @param menuClass Класс меню
* @param player Игрок (которому открываем)
* @param args Аргументы для конструктора меню
*/
public static <T extends BaseMenu> boolean open(Class<T> menuClass, Player player, Object... args) {
try {
// Получаем типы классов переданных аргументов
Class<?>[] argTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = args[i].getClass();
}
// Ищем подходящий конструктор и создаем объект
T menu = menuClass.getDeclaredConstructor(argTypes).newInstance(args);
player.openInventory(menu.getInventory());
return true;
} catch (Exception exception) {
exception.printStackTrace();
return false;
}
}
}
UPD:
Вариант 2: Фабрика без рефлексии (через Supplier)
Если вы не хотите использовать рефлексию, можно использовать Supplier. Это более «явный» способ: мы заранее передаем функции, которые знают, как создавать наши меню.
Java:
public class MenuFactory {
/**
* Открывает меню, созданное через Supplier.
* @param menuSupplier Функция создания меню (например, TestMenu::new)
* @param player Игрок
*/
public static void open(Player player, Supplier<? extends BaseMenu> menuSupplier) {
BaseMenu menu = menuSupplier.get();
player.openInventory(menu.getInventory());
}
}
Шаг 8: Меню с параметрами и вызов через команду
Иногда в меню нужно передать данные - например, профиль игрока. Благодаря нашей Factory, это делается элементарно.
Java:
public class ProfileMenu extends BaseMenu {
public ProfileMenu(Player target) {
super(9, Component.text("Профиль: " + target.getName()));
setItem(4, new ItemStack(Material.PLAYER_HEAD), event -> {
event.player().sendMessage(Component.text("Это профиль игрока " + target.getName()));
});
}
}
Регистрация команды для открытия меню
Создадим простую команду, чтобы игрок мог открыть это меню в игре.
Java:
public class MenuCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) return true;
// Пример 1: Открытие меню через Factory рефлексий
if (args.length == 0) {
MenuFactory.open(TestMenu.class, player);
} else {
MenuFactory.open(ProfileMenu.class, player, player);
}
// Пример 2: Открытие меню через Factory supplier
if (args.length == 0) {
MenuFactory.open(player, TestMenu::new);
} else {
MenuFactory.open(player, () -> new ProfileMenu(player));
}
return true;
}
}
Java:
onEnable:getCommand("menu").setExecutor(new MenuCommand());
Подведем итог руководства:
- BaseMenu - абстрактный каркас с использованием InventoryHolder.
- ClickEvent - удобный контейнер (Record) для данных клика.
- InventoryListener - «движок», который блокирует воровство предметов и запускает действия.
- MenuFactory - инструмент для красивого создания меню (в том числе через рефлексию).
- TestMenu / ProfileMenu - готовые примеры реализации.
- Чистота и читаемость (Clean Code): Вам больше не нужно писать километровые цепочки if (event.getSlot() == 10). Вся логика кнопки находится в одной строке прямо при её создании. Это делает код понятным даже спустя месяц после написания.
- Масштабируемость: Благодаря BaseMenu и MenuFactory, создание нового меню - это вопрос пары минут. Система позволяет легко добавлять десятки новых окон без дублирования кода и регистрации новых слушателей.
- Безопасность «из коробки»: Централизованный InventoryListener гарантирует, что игроки не смогут дюпать предметы или забирать иконки меню себе в инвентарь. Вам не нужно прописывать setCancelled(true) в каждом новом меню - система делает это за вас автоматически.
- Универсальность и гибкость: Использование Reflection в фабрике и функциональных интерфейсов (Consumer) позволяет передавать в меню любые данные и объекты. Это дает возможность создавать динамические интерфейсы: личную статистику, рынки или системы прокачки.
- Современные стандарты: Использование Records, Adventure API (Components) и InventoryHolder показывает, что плагин написан по актуальным стандартам разработки 2026 года, а не по гайдам десятилетней давности.