Иконка ресурса

Руководство Правильно создаем менюшки на бакките

Поддерживаемые версии
  1. 1.16
  2. 1.17
  3. 1.18
  4. 1.19
  5. 1.20
  6. 1.21
Начнем с того что многие все еще не умеют создавать нормальные менюшки и делает те же самые проверки по title, не соблюдают структуру классов и позже данный код невозможно дополнять/читать. А также этот гайд будет очень полезен для новичков которые только начинают свое знакомство с Bukkit и его обитателями.

Шаг 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::eek: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;
    }
}
Не забудьте прописать команду в plugin.yml и зарегистрировать её в

Java:
onEnable:getCommand("menu").setExecutor(new MenuCommand());


Подведем итог руководства:​

  1. BaseMenu - абстрактный каркас с использованием InventoryHolder.
  2. ClickEvent - удобный контейнер (Record) для данных клика.
  3. InventoryListener - «движок», который блокирует воровство предметов и запускает действия.
  4. MenuFactory - инструмент для красивого создания меню (в том числе через рефлексию).
  5. TestMenu / ProfileMenu - готовые примеры реализации.
  6. Чистота и читаемость (Clean Code): Вам больше не нужно писать километровые цепочки if (event.getSlot() == 10). Вся логика кнопки находится в одной строке прямо при её создании. Это делает код понятным даже спустя месяц после написания.
  7. Масштабируемость: Благодаря BaseMenu и MenuFactory, создание нового меню - это вопрос пары минут. Система позволяет легко добавлять десятки новых окон без дублирования кода и регистрации новых слушателей.
  8. Безопасность «из коробки»: Централизованный InventoryListener гарантирует, что игроки не смогут дюпать предметы или забирать иконки меню себе в инвентарь. Вам не нужно прописывать setCancelled(true) в каждом новом меню - система делает это за вас автоматически.
  9. Универсальность и гибкость: Использование Reflection в фабрике и функциональных интерфейсов (Consumer) позволяет передавать в меню любые данные и объекты. Это дает возможность создавать динамические интерфейсы: личную статистику, рынки или системы прокачки.
  10. Современные стандарты: Использование Records, Adventure API (Components) и InventoryHolder показывает, что плагин написан по актуальным стандартам разработки 2026 года, а не по гайдам десятилетней давности.
Автор
Fallen
Просмотры
84
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок

Поделиться ресурсом

Назад
Сверху Снизу