Minecraft Modding: Block With Simple GUI

Introduction


This tutorial is for making a custom block with a simple, fixed message GUI.

For blocks with more complex GUIs, check out Jabelar's Blocks With GUIs Tutorial.

In this example, we're essentially just making a sort of book that will display some information about the story for the mod.  The GUI will be simple consisting of just a background graphic, a few pages of text, buttons for flipping pages and a button for done.

The steps are quite simple:
  1. Create a custom Block class along with models and textures, register the block and block item renderer. (See Jabelar's Blocks In 1.8 Tutorial).
  2. Create PNG Image Assets For GUI Background And Buttons
  3. Create a custom class that extends GuiScreen.
  4. Create any custom buttons needed that extend GuiButton.
  5. In your custom block class, in the onBlockActivated() method make it open (only on client side!) your custom GUI.
Okay, let's look at the details.

Create A Custom Block Class


I'll assume you already know how to make a basic custom block.  If not, check out Jabelar's Blocks In 1.8 Tutorial.

Don't forget to register the block, create the JSON assets for blockstates, block models and item block models, and put the texture assets into properly assets location.  Get the block working before worrying about the GUI.

Create PNG Image Assets


It is totally up to you what you want your GUI to look like so have fun creating graphics for it -- I recommend a free program called GIMP for image editing (just make sure to export to PNG).

In this tutorial example I'm pretty much copying the vanilla book but slightly modified.  Here are the images I used (for book cover and for rest of pages):

Tip: You can see I actually also put textures for the page turning buttons in the same image. When you bind the textures later you can choose portions of the image asset -- I could have even combined both these images into a single one.  But it is also okay to make separate image assets for every element of your GUI.

Warning: Make sure your assets are filed in a place that matches the resource locations in your code. I recommend creating a textures/gui/ folder and putting them there.

Create A Class That Extends GuiScreen


GuiScreen is the main parent class for most GUIs in the game.  In this tutorial I'm going to make a screen that displays sort of like a book -- it will have two pages and will have buttons to flip pages.  A "done" button will appear on the last page of the book.

Here is the example code for this class:


public class GuiMysteriousStranger extends GuiScreen
{
    private final int bookImageHeight = 192;
    private final int bookImageWidth = 192;
    private int currPage = 0;
    private static final int bookTotalPages = 4;
    private static ResourceLocation[] bookPageTextures = 
          new ResourceLocation[bookTotalPages];
    private static String[] stringPageText = new String[bookTotalPages];
    private GuiButton buttonDone;
    private NextPageButton buttonNextPage;
    private NextPageButton buttonPreviousPage;
    
    public GuiMysteriousStranger()
    {
        bookPageTextures[0] = new ResourceLocation(
              MagicBeans.MODID+":textures/gui/book_cover.png");
        bookPageTextures[1] = new ResourceLocation(
              MagicBeans.MODID+":textures/gui/book.png");
        bookPageTextures[2] = new ResourceLocation(
              MagicBeans.MODID+":textures/gui/book.png");
        stringPageText[0] = "";
        stringPageText[1] = "The Mysterious Stranger admired your family cow and asked if it was for sale.\n\nWhen you nodded, he offered to trade some Magic Beans, that (if planted in tilled dirt) would lead to more wealth than you could imagine.";
    stringPageText[2]="So you handed him your cow, and grabbed the Magic Beans.\n\nPleased with yourself, you hurried away, looking for tilled dirt in which to plant the Magic Beans.\n\nYou couldn't wait to see how proud your mother would be for";
     stringPageText[3]="being so shrewd!  Untold wealth in return for an old, milkless cow; what a good deal you made!\n\nSo off you went, looking for a place to plant the Magic Beans with room to grow...";
 }

    /**
     * Adds the buttons (and other controls) to the screen in question.
     */
    @Override
    public void initGui() 
    {
     // DEBUG
     System.out.println("GuiMysteriousStranger initGUI()");
        buttonList.clear();
        Keyboard.enableRepeatEvents(true);

        buttonDone = new GuiButton(0, width / 2 + 2, 4 + bookImageHeight, 
              98, 20, I18n.format("gui.done", new Object[0]));
  
        buttonList.add(buttonDone);
        int offsetFromScreenLeft = (width - bookImageWidth) / 2;
        buttonList.add(buttonNextPage = new NextPageButton(1, 
              offsetFromScreenLeft + 120, 156, true));
        buttonList.add(buttonPreviousPage = new NextPageButton(2, 
              offsetFromScreenLeft + 38, 156, false));
    }

    /**
     * Called from the main game loop to update the screen.
     */
    @Override
    public void updateScreen() 
    {
        buttonDone.visible = (currPage == bookTotalPages - 1);
        buttonNextPage.visible = (currPage < bookTotalPages - 1);
        buttonPreviousPage.visible = currPage > 0;
    }
 
    /**
     * Draws the screen and all the components in it.
     */
    @Override
    public void drawScreen(int parWidth, int parHeight, float p_73863_3_)
    {
        GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
        if (currPage == 0)
     {
         mc.getTextureManager().bindTexture(bookPageTextures[0]);
     }
        else
        {
         mc.getTextureManager().bindTexture(bookPageTextures[1]);
        }
        int offsetFromScreenLeft = (width - bookImageWidth ) / 2;
        drawTexturedModalRect(offsetFromScreenLeft, 2, 0, 0, bookImageWidth, 
              bookImageHeight);
        int widthOfString;
        String stringPageIndicator = I18n.format("book.pageIndicator", 
              new Object[] {Integer.valueOf(currPage + 1), bookTotalPages});
        widthOfString = fontRendererObj.getStringWidth(stringPageIndicator);
        fontRendererObj.drawString(stringPageIndicator, 
              offsetFromScreenLeft - widthOfString + bookImageWidth - 44, 
              18, 0);
        fontRendererObj.drawSplitString(stringPageText[currPage], 
              offsetFromScreenLeft + 36, 34, 116, 0);
        super.drawScreen(parWidth, parHeight, p_73863_3_);

    }

    /**
     * Called when a mouse button is pressed and the mouse is moved around. 
     * Parameters are : mouseX, mouseY, lastButtonClicked & 
     * timeSinceMouseClick.
     */
    @Override
    protected void mouseClickMove(int parMouseX, int parMouseY, 
          int parLastButtonClicked, long parTimeSinceMouseClick) 
    {
     
    }

    @Override
    protected void actionPerformed(GuiButton parButton) 
    {
     if (parButton == buttonDone)
     {
         // You can send a packet to server here if you need server to do 
         // something
         mc.displayGuiScreen((GuiScreen)null);
     }
        else if (parButton == buttonNextPage)
        {
            if (currPage < bookTotalPages - 1)
            {
                ++currPage;
            }
        }
        else if (parButton == buttonPreviousPage)
        {
            if (currPage > 0)
            {
                --currPage;
            }
        }
   }

    /**
     * Called when the screen is unloaded. Used to disable keyboard repeat 
     * events
     */
    @Override
    public void onGuiClosed() 
    {
     
    }

    /**
     * Returns true if this GUI should pause the game when it is displayed in 
     * single-player
     */
    @Override
    public boolean doesGuiPauseGame()
    {
        return true;
    }
    
    @SideOnly(Side.CLIENT)
    static class NextPageButton extends GuiButton
    {
        private final boolean isNextButton;

        public NextPageButton(int parButtonId, int parPosX, int parPosY, 
              boolean parIsNextButton)
        {
            super(parButtonId, parPosX, parPosY, 23, 13, "");
            isNextButton = parIsNextButton;
        }

        /**
         * Draws this button to the screen.
         */
        @Override
        public void drawButton(Minecraft mc, int parX, int parY)
        {
            if (visible)
            {
                boolean isButtonPressed = (parX >= xPosition 
                      && parY >= yPosition 
                      && parX < xPosition + width 
                      && parY < yPosition + height);
                GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
                mc.getTextureManager().bindTexture(bookPageTextures[1]);
                int textureX = 0;
                int textureY = 192;

                if (isButtonPressed)
                {
                    textureX += 23;
                }

                if (!isNextButton)
                {
                    textureY += 13;
                }

                drawTexturedModalRect(xPosition, yPosition, 
                      textureX, textureY, 
                      23, 13);
            }
        }
    }
}

Hopefully most of the code is self explanatory.

In the field declarations you can see that it is mostly fields related to how the GUI will display: size, texture, etc.  I also am using a custom button called NextPageButton (explained below).

In the constructor we assign the text for each page.  This could also have been done as field initialization or in the initGui() method.

In the initGui() method, it mostly just creates a list of the buttons that will be on the GUI.  Note that this should have all the buttons that might be in GUI -- some buttons may not actually display depending on what is going on in the GUI (for example I hide the previous page button when on the first page).

Tip: For the buttonDone you can see that I pass the text as gui.done. This is the unlocalized name. In this case I'm using a standard Minecraft button name, but if you wanted custom text label on the button you'd need to give it a mod-specific unlocalized name and also update your .lang files to translate it.

The updateGUI() method can be used to change the GUI based on what the user is doing or the state of the GUI.  In this case I'm hiding buttons depending on what page of the GUI is being displayed. Hiding a button is as simple as modifying the public visible field for that button.

The drawScreen() method actually draws the GUI and you can pretty much do anything that GL11 allows.  You could do animations, draw images, text, etc.  In this example, firstly I'm changing the texture depending on whether it is the cover or later pages of the book.  I then draw that texture, then draw the text for that page, and some page numbering.

Tip: The drawTexturedModalRect() method allows you to select just a portion from the image asset.

Key Point: You do not need to draw the buttons in the drawScreen() method, because each visible button in the buttons list will automatically get its drawButton() method called.

The onActionPerformed() method is an important method.  Any time any button is clicked this will be called.  In this case I check which button is clicked.  If it is the done method then it closes the GUI, and if it is the next or previous page buttons it adjusts the page number index.

Key Point: The onActionPerformed() method is where you should send any custom packets to server if needed (i.e. because the GUI is supposed to change something in the game).

Warning: You should ensure the doesGuiPauseGame() method returns what you want. Returning true will pause the game (in single player games).

Create Custom GuiButton If Needed


If you want your button to look different than standard (grey box with text) then you'll want a custom button by creating a class that extends GuiButton. In this case I want to use arrow icons on the buttons so I created a custom button called NextPageButton.  Note that I could have created a different custom button for both previous and next, but to be fancy I combined them into one and it displays different texture depending on which it is.

The key method is of course the drawButton() method and hopefully that is self explanatory. 

Key Point: If you want your button to change looks based on what player is doing in the GUI, you should put that in the drawButton() method of a custom button.

Open The GUI From The Block


There are two main methods for opening GUIs.  You'll often see the EntityPlayer#openGui() method used; however that method only works for GUIs that have containers and an IGuiHandler.

In our case, we should use the Minecraft#displayGuiScreen() method.

Warning: A GUI should only be opened on the client side.

Since you want the GUI to open when the player right-clicks on a block, you need to @Override the onBlockActivated() method in your custom block.  However, the problem is that that method runs on both client and server so in multi-player the Minecraft class won't be present (and will cause crash). So you need to process the call through your proxy, as follows.

In your common proxy, make an empty method something like this:


void openMyGui()
{
}

Then in your client proxy, override the method with this:


@Override
void openMyGui()
{
     Minecraft.getMinecraft().displayGuiScreen(new GuiMysteriousStranger());
}

Now you can safely call this proxy method from code that runs on both sides. Something like this (replace MyMod with the name of your mod's main class:

@Override
public boolean onBlockActivated(World parWorld, BlockPos parBlockPos, 
      IBlockState parIBlockState, EntityPlayer parPlayer, EnumFacing parSide, 
      float hitX, float hitY, float hitZ)
{
    if (!parWorld.isRemote)
    {
        MyMod.proxy.openMyGui()); 
    }
        
    return true;
}

Conclusion


That's it.  Hopefully it gives you some ideas for interactive blocks.  You can get as creative as you want with the drawing of the GUI.

As always please feel free to comment if you need clarifications or see things needing correction. Happy modding!


23 comments:

  1. Sometimes the game crashes with various Exceptions and Ticking Screen. Could is be because I'm modding in 1.7.10? (Most of the time everything works perfectly)
    Also could you make a tutorial about adding a custom GUI to a custom NPC?
    Otherwise great work,probably the best on the net! :)

    ReplyDelete
    Replies
    1. I seem to be getting that error as well, and I am on 1.8

      Delete
  2. Hi, I'm getting an error while trying to run it on a server. I have narrowed the error down to the custom block class. I removed "Minecraft.getMinecraft().displayGuiScreen( new GuiMysteriousStranger());" and the crash no longer happens. Possibly any ideas?

    I have to say, thanks a lot! You have helped me through some hoops on many occasions:)

    ReplyDelete
    Replies
    1. I'm getting this in 1.7.10 too. I found putting @sideonly client.side allows the server to load, but then my @override of onItemUse never fires.

      Delete
    2. Okay, I fixed the tutorial. The command needs to be processed through the proxy because the method runs on both sides but the Minecraft class won't be loaded on the server side.

      Delete
  3. Thanks for the tutorial! Its great! However I have a problem and would appreciate any help:
    When I try running you exact code, the first one - two runs are ok, then on the third, with no changes to the code what so ever, I get a Null Pointer Exception on the mc.getTextureManager().bindTexture(bookPageTextures[0]); line when I try opening the GUI. If i then change some parameters, say, the texture offset, it works another 1 or 2 compilations, then NPE s again. I have been investigating it for over a day and am quite stumped. Could you help me please?
    Ps. I am using minecraft 1.7.10 if that may influence the problem.

    ReplyDelete
    Replies
    1. For anyone out there with a similar problem, I seem to have solved it: in the gui class, in the start of the drawScreen method you should add:
      Minecraft mc = Minecraft.getMinecraft();

      Delete
    2. Thanks for the feedback. That is weird though, because the way mc is supposed to get set is when the Minecraft#displayGuiScreen() method is called, it calls the GuiScreen#setWorldAndResolution() method which passes along the Minecraft instance. So the mc should be filled in automatically.

      Delete
  4. Would you be willing to go into a bit of animated texture rendering in the GUI? For example, a flashing texture to draw attention to that spot, or a crafting recipe shown in parts to build a multi-block structure for example (GUI would be describing how to build it in tiers and show a repeating GIF basically of each layer).

    ReplyDelete
    Replies
    1. You just draw what you want in the drawScreen() method. So to create an animation you can bind different textures that require different steps in the animation and pick which one should actually be drawn. Basically in drawScreen() you can draw whatever you want.

      Delete
    2. I guess I'm just confused on how that would work in the sense of like, a timer then to say when or how fast to draw the next one? is it just a loop of cycling which texture is being bound?

      Delete
    3. You can have a int field in the GUI that you increment every time in the drawScreen() method. Then you can divide that down (there are 20 ticks per second, so divide by 20 to figure out seconds or whatever) into whatever your animation interval is. Then each time you count up to that amount, you can change what you're displaying. That's all that animation really is.

      Delete
  5. This comment has been removed by the author.

    ReplyDelete
  6. How do you remove the page number in the top right hand corner?

    ReplyDelete
    Replies
    1. Get rid of the line that says:

      String stringPageIndicator = I18n.format("book.pageIndicator", new Object[] {Integer.valueOf(currPage + 1), bookTotalPages});

      Delete
  7. The gui don't open in multiplayer even though I did the proxy thing. What is the problem?
    Thank you! :)

    ReplyDelete
  8. Please help... i have 6 pages with pictures but pages 1 and 2 have the correct textures and the rest have texture 2. please help thank you

    ReplyDelete
  9. Why would the mouse cursor not show after opening the GUI?

    ReplyDelete
    Replies
    1. Minecraft#displayGuiScreen() is not thread safe, it must be runned on the main thread or strange behaviour like this happens.

      To solve it just override Minecraft.getMinecraft().displayGuiScreen() with Minecraft.getMinecraft().addScheduledTask(()->Minecraft.getMinecraft().displayGuiScreen()

      This will require the minecraft client to open the gui on the main thread and solve the problem.

      Delete
  10. If the proxy only opens the gui on the client side, wouldn't checking !parWorld.isRemote before running the code via proxy make it so that the gui never will open as this would only run the code in the common proxy?

    ReplyDelete
  11. does this work on a item? I'm just wondering?

    ReplyDelete
  12. Why do you need to override the onBlockActivated method?

    ReplyDelete