Resource Creating GUIs 'the right way'

Discussion in 'Spigot Plugin Development' started by Xilixir, Jun 16, 2016.

  1. I've seen an absolute shit ton of threads on here with people asking questions about making GUIs. This is my attempt to answer all the questions that need to be answered.

    Creation

    So for starters, I'd just like to point out that using lore or item-name based GUIs is very sloppy and leads to issues.

    Start off with your constructor. If you don't know what this is, basically when you create an instance of your class, you need the parameters in the constructor and the code runs.

    Code (Text):
    public abstract class YourGUI {
        public YourGUI() {
        }
    }
     
    Next you need to add the parameters to the constructor to define inv size and name on creation, create the inventory, and also add a variable/getter for the created inventory.

    Code (Text):
    public abstract class YourGUI {
        private Inventory yourInventory;

        public YourGUI(int invSize, String invName) {
            yourInventory = Bukkit.createInventory(null, invSize, invName);
        }

        public Inventory getYourInventory() {
            return yourInventory;
        }
    }
     
    For a GUI the best way to make actions on click is to use an interface, so we're going to add an inner class, and a HashMap to store the values using the slot as the key.

    Code (Text):
    public abstract class YourGUI {
        private Inventory yourInventory;
        private Map<Integer, YourGUIAction> actions;

        public YourGUI(int invSize, String invName) {
            yourInventory = Bukkit.createInventory(null, invSize, invName);
            actions = new HashMap<>();
        }

        public interface YourGUIAction {
            void click(Player player);
        }
    }
     
    Add methods for adding items and actions.

    Code (Text):
        public void setItem(int slot, ItemStack stack, YourGUIAction action){
            yourInventory.setItem(slot, stack);
            if (action != null){
                actions.put(slot, action);
            }
        }
     
        public void setItem(int slot, ItemStack stack){
            setItem(slot, stack, null);
        }
    A method to open the inventory for a player.

    Code (Text):
        public void open(Player p){
            p.openInventory(yourInventory);
        }
    To identify each inventory, we're going to give each created inventory its own UUID.

    Code (Text):
    public abstract class YourGUI {
        private UUID uuid;
        private Inventory yourInventory;
        private Map<Integer, YourGUIAction> actions;

        public YourGUI(int invSize, String invName) {
            uuid = UUID.randomUUID();
            yourInventory = Bukkit.createInventory(null, invSize, invName);
            actions = new HashMap<>();
        }

        public UUID getUuid() {
            return uuid;
        }
    }
    Add maps to store which inventory a player is using, storing the inventory's UUID as the value with the player's UUID as the key, and storing the inventory instance as the value by the inventory's UUID as the key.

    Code (Text):
    public abstract class YourGUI {
        public static Map<UUID, YourGUI> inventoriesByUUID = new HashMap<>();
        public static Map<UUID, UUID> openInventories = new HashMap<>();
     
        private UUID uuid;
        private Inventory yourInventory;
        private Map<Integer, YourGUIAction> actions;
    }
    Set map values in the methods we're using to open the inventory for the player, and also the constructor.

    Code (Text):
        public void open(Player p){
            p.openInventory(yourInventory);
            openInventories.put(p.getUniqueId(), getUuid());
        }
    Code (Text):
        public YourGUI(int invSize, String invName) {
            uuid = UUID.randomUUID();
            yourInventory = Bukkit.createInventory(null, invSize, invName);
            actions = new HashMap<>();
            inventoriesByUUID.put(getUuid(), this);
        }
    Add getters so we can access our private variables from the listener we're about to make

    Code (Text):
        public static Map<UUID, YourGUI> getInventoriesByUUID() {
            return inventoriesByUUID;
        }

        public static Map<UUID, UUID> getOpenInventories() {
            return openInventories;
        }

        public Map<Integer, YourGUIAction> getActions() {
            return actions;
        }
    Create the listener class, listen for when a player clicks and check for actions in the slot the player clicked in

    Code (Text):
    public class YourGUIListener implements Listener {
        @EventHandler
        public void onClick(InventoryClickEvent e){
            if (!(e.getWhoClicked() instanceof Player)){
                return;
            }
            Player player = (Player) e.getWhoClicked();
            UUID playerUUID = player.getUniqueId();

            UUID inventoryUUID = YourGUI.openInventories.get(playerUUID);
            if (inventoryUUID != null){
                e.setCancelled(true);
                YourGUI gui = YourGUI.getInventoriesByUUID().get(inventoryUUID);
                YourGUI.YourGUIAction action = gui.getActions().get(e.getSlot());
             
                if (action != null){
                    action.click(player);
                }
            }
        }
    }
    Add a listener for when the player closes the inventory

    Code (Text):
        @EventHandler
        public void onClose(InventoryCloseEvent e){
            Player player = (Player) e.getPlayer();
            UUID playerUUID = player.getUniqueId();
         
            YourGUI.openInventories.remove(playerUUID);
        }
    Apply the same code for the previous listener to the player quit event, just incase a player disconnects while inside an inventory.

    Code (Text):
        @EventHandler
        public void onQuit(PlayerQuitEvent e){
            Player player = e.getPlayer();
            UUID playerUUID = player.getUniqueId();

            YourGUI.openInventories.remove(playerUUID);
        }
    Add a delete method that will delete the inventory, use this if you are going to create inventories often.

    Code (Text):
        public void delete(){
            for (Player p : Bukkit.getOnlinePlayers()){
                UUID u = openInventories.get(p.getUniqueId());
                if (u.equals(getUuid())){
                    p.closeInventory();
                }
            }
            inventoriesByUUID.remove(getUuid());
        }
    Usage
    Create a new class, extend YourGUI. Do whatever you want in the constructor - add items, open it for a player, doesn't matter.
    Code (Text):
    public class RandomGUI extends YourGUI {
        public RandomGUI() {
            super(9, "Your Inventory Name");
         
            setItem(4, new ItemStack(Material.DIAMOND), player -> {
                player.sendMessage("Well this is pretty cool, I'm definitely going to give nice feedback!!!");
                player.setHealth(0);
            });
        }
    }
    Create an instance of this class somewhere - on an event, on player login, on enable. Call YourGUI#open(Player varPlayer) to open the inventory. Make sure to register the listener.

    Example of opening:

    Code (Text):
    public class TestPlugin extends SpigotPlugin implements Listener {
        private RandomGUI randomGUI;
     
        @Override
        public void enable() {
            randomGUI = new RandomGUI();
            registerListeners(this, new YourGUIListener());
        }

        @Override
        public void disable() {

        }

        @EventHandler
        public void onJoin(PlayerJoinEvent e){
            new BukkitRunnable() {
                @Override
                public void run() {
                    randomGUI.open(e.getPlayer());
                }
            }.runTaskLater(this, 1);
        }
    }
    Go make your GUIs in peace. Please leave feedback :^)
     
    • Informative x 13
    • Like x 5
    • Useful x 3
    • Winner x 2
    • Agree x 1
  2. Very informative, I like it. I think there is just one thing I would add; in the listener, I'd add something that checks if the player clicked their own inventory so they can move their own items around.
     
  3. I tend to avoid this, I've found it to be occasionally buggy.
     
  4. Very well made! This is going to help myself, and many others! Glad to see people like you helping the "newer" developers!
     
  5. It can be finicky. Other than that, if you wanted to add anything else, maybe expand on the custom inventory actions. Feels like you skimmed over their functionality or how to use them.
     
  6. Well there isn't much to them, you create the instance and it just runs the code.
     
  7. I know, but other people might not know exactly.
     
  8. There's no need for two Maps, you can just use a mapping player -> GUI. Moreover, it shouldn't be static (use a different class to track - as per Single Responsibility Principle, or use the Player's metadata). Or let menu's handle their own listeners, so you only need a Set<Player> to track whether a Player has your menu opened.
     
    • Agree Agree x 5
  9. I find it easier to work with if the inventories are in a seperate map, and there isn't any noticeable difference.
     
  10. Love this! Really good!
    I added itemmeta on my own for displayname and Lore on items and this works so good :)

    thank you for this tutorial
     
  11. I really like this! Thanks for the good Tutorial! :D
     
  12. Code (Java):
    public abtsract void onClick(InventoryClickEvent e);
    Pretty good system. I have a custom gui system kinda similar to this.
    Instead of using an interface, I declare the above method. in the equivalent of the 'YourGUI' class. This means you only need 1 Map, storing which gui a player has open, instead of having a second one storing actions.

    It is cool because whenever you extend the GUI class it reminds you to implement the click event which can be customised to your liking.

    I prefer my way, but I like the way yours is set out too. :D
     
  13. How do I add a lore to items in a separate method in the set item? I can't seem to figure it out.

    Code (Text):
    package com.visualsbyimpulse.core.inventory;

    import com.visualsbyimpulse.core.utilities.Chat;
    import com.visualsbyimpulse.core.utilities.Files;
    import com.visualsbyimpulse.core.utilities.ItemBuilder;
    import com.visualsbyimpulse.core.utilities.keymaster.Keys;
    import com.visualsbyimpulse.core.utilities.keymaster.RewardHandout;
    import org.bukkit.Bukkit;
    import org.bukkit.Material;
    import org.bukkit.entity.Player;
    import org.bukkit.inventory.ItemStack;

    /**
    * Created by Spencer on 2016-08-24.
    */
    public class KeyMasterOpen extends GUI {

        public KeyMasterOpen() {
            super(9, "Key Master > Open");

            ItemStack Common = new ItemBuilder(Material.NAME_TAG).setName(Chat.format("&aCommon Key")).setLore(Chat.format("&aAmount:&7 " + Keys.COMMON.getAmount(player) + " Keys")).toItemStack();
            ItemStack Rare = new ItemBuilder(Material.NAME_TAG).setName(Chat.format("&aRare Key")).setLore(Chat.format("&aAmount:&7 " + " Keys")).toItemStack();
            ItemStack Legendary = new ItemBuilder(Material.NAME_TAG).setName(Chat.format("&aLegendary Key")).setLore(Chat.format("&aAmount:&7 " + "Keys")).toItemStack();
            ItemStack Extinct = new ItemBuilder(Material.NAME_TAG).setName(Chat.format("&aExtinct Key")).setLore(Chat.format("&aAmount:&7 " + " Keys")).toItemStack();
            ItemStack Kit = new ItemBuilder(Material.NAME_TAG).setName(Chat.format("&aKit Key")).setLore(Chat.format("&aAmount:&7 " + " Keys")).toItemStack();

            setItem(2, Common, player -> {
                RewardHandout.giveCommon(player);
            });

            setItem(3, Rare, player -> {
                player.sendMessage("Add after");
            });

            setItem(4, Legendary, player -> {
                player.sendMessage("Add after");
            });

            setItem(5, Extinct, player -> {
                player.sendMessage("Add after");
            });

            setItem(6, Kit, player -> {
                player.sendMessage("Add after");
            });

        }

    }
     
     
  14. I like this, however I would prefer using a custom InventoryHolder to map the open inventory to the actions for each click etc... A hashmap works perfectly fine, but it feels (at least to me) like using an InventoryHolder would be elegant. :p
     
    • Agree Agree x 1
  15. That's irrelevant if you're whole tutorial is about doing things the "right" way. If you're going to preach that this is the best way then settle for second best that's just stupid. Listen to DarkSeraphim.
     
    • Like Like x 1
    • Agree Agree x 1
    • Friendly Friendly x 1
  16. "this is going to help myself"