Welcome to the 11th part in this game from scratch series, in this tutorial we look at changing the projection of our sprite batch to allow us to render a heads up display / menu and also create the classes which will allow us to generate interactive menus. The full source code is available on Github.
New Images
- core/assets/gui/main_background.png
- core/assets/gui/pink_button.png
- core/assets/gui/square_menu.png
- core/assets/gui/icons/build.png
- core/assets/gui/icons/close_menu.png
- core/assets/gui/icons/resources.png
- core/assets/gui/icons/settings.png
- core/assets/gui/selector.png
New classes
- core/src/uk/co/carelesslabs/ui/BuildMenu.java
- core/src/uk/co/carelesslabs/ui/Button.java
- core/src/uk/co/carelesslabs/ui/Menu.java
- core/src/uk/co/carelesslabs/ui/OnClickListener.java
- core/src/uk/co/carelesslabs/ui/SquareMenu.java
Updated
- core/src/uk/co/carelesslabs/Media.java
- core/src/uk/co/carelesslabs/gameclass.java
- core/src/uk/co/carelesslabs/Enums.java
- core/src/uk/co/carelesslabs/Control.java
Custom menu
In this tutorial we will look at creating a custom menu for our game, this eventually will
allow the player to access/view build, settings and inventory. There are libraries available to handle all sorts of inputs (buttons, text boxes etc) such as Scene2D, but when the menu is basic and for learning LibGDX its interesting and sometimes simpler to create your own.
Rendering the menu
Whereas the main game camera moves around the map and can zoom / shake etc the menu view will need to be fixed.
The projection set for game screen is not fit for displaying the menu. Initially I created a new spriteBatch for the HUD (Heads Up Display) which was created using the screen size, this seemed less efficient than having just one spriteBatch, to use only one we will need to update the Matrix of the spriteBatch before rendering the HUD.
We can achieve this by updating the projection of the spriteBatch prior to rendering.
gameclass.java
// new class variable Matrix4 screenMatrix; @Override public void create() { ... // Setup Matrix4 for HUD screenMatrix = new Matrix4(batch.getProjectionMatrix().setToOrtho2D(0, 0, control.screenWidth, control.screenHeight)); ... @Override public void render () { ... // GUI batch.setProjectionMatrix(screenMatrix);
The screenMatrix will allow us to change to spriteBatch back to a view the size of the screen, co-ordinates 0,0 will be the bottom left of the screen. With the view being the same size as the screen it will be easy to position menu items as we can access the screen width and height.
Menu
Menu.java
The new menu class has a texture which is the background for the menu, a postiion, width and height used when rendering, a Rectanlge (hitbox)
and an array of buttons.
package uk.co.carelesslabs.ui; import java.util.ArrayList; import uk.co.carelesslabs.Enums; import uk.co.carelesslabs.Enums.MenuState; import uk.co.carelesslabs.entity.Entity; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Menu { public String name; public Vector2 pos; public Texture texture; public float width; public float height; public float scale; public MenuState state; public float time; public float coolDown; public Rectangle hitbox; public ArrayList<Button> buttons; public Menu(float x, float y, float scale, Texture texture){ pos = new Vector2(x,y); this.texture = texture; width = texture.getWidth() * scale; height = texture.getHeight() * scale; buttons = new ArrayList<Button>(); hitbox = new Rectangle(x,y,width,height); setActive(); } // Render the texture and all of the button textures public void draw(SpriteBatch batch){ if(texture != null) batch.draw(texture, pos.x, pos.y, width, height); for(Button b : buttons){ b.draw(batch); } } // If the player has clicked the mouse then processedClick will be true // We check if the mouse position is contained within any of the button Rectangles public boolean checkClick(Vector2 pos, boolean processedClick){ boolean processed = false; if(!processedClick){ if(hitbox.contains(pos)){ System.out.println("Hit: " + name); } // Check if a button has been clicked for(Button b : buttons){ if(b.hitbox.contains(pos)){ if (b.listener != null) b.listener.onClick(b); processed = true; break; } } } else { return processedClick; } return processed; } // If the mouse is inside of the menu then check if its also inside of a button // When the mouse is inside a button then set its state to hovering // Else set all buttons to idle public void checkHover(Vector2 pos){ if(hitbox.contains(pos)){ // Check if a button is being hovered over for(Button b : buttons){ if(b.hitbox.contains(pos)){ b.state = Enums.EnityState.HOVERING; } else { b.state = Enums.EnityState.IDLE; } } } else { for(Button b : buttons){ b.state = Enums.EnityState.IDLE; } } } // A function to add multiply buttons to our menu // It is possible to add any size grid of buttons with a certain sized padding public void addButtons(float offset, int columns, int rows, Texture texture, Texture select, int scale) { for(int i = 0; i < columns; i++){ for(int j = 0; j < rows; j++){ float bx = pos.x + (offset + ((i+1)*offset) + (i * texture.getWidth())) * 2; float by = pos.y + (offset + ((j+1)*offset) + (j * texture.getHeight())) * 2; float width = texture.getWidth() * 2; float height = texture.getHeight() * 2; Entity selector = new Entity(); selector.texture = select; selector.width = selector.texture.getWidth() * scale; selector.height = selector.texture.getHeight() * scale; selector.pos.x = bx - ((selector.width - width) / 2); selector.pos.y = by - ((selector.height - height) / 2); buttons.add(new Button(bx, by, width, height, texture, selector)); } } } // Check if the menu is active public boolean isActive(){ return state == Enums.MenuState.ACTIVE; } // Set meny to active public void setActive(){ state = Enums.MenuState.ACTIVE; } // Set menu to inactive public void setInactive(){ state = Enums.MenuState.DISABLED; } // Toggle active state public void toggleActive() { if(isActive()){ setInactive(); } else { setActive(); } } }
Button.java
The Button class extends Entity, it adds an OnClickListener, a hitbox, an icon texture and a selector entity. The OnClickListener defines
what to do when the button is clicked, the hitbox allows us to test if the mouse position when a click occurs should interact with
button, the selector is a texture that is drawn when the button is being hovered over and the icon is a texture to help identify what
the button does.
package uk.co.carelesslabs.ui; import uk.co.carelesslabs.Enums; import uk.co.carelesslabs.entity.Entity; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Rectangle; public class Button extends Entity { public OnClickListener listener; public Rectangle hitbox; public Texture icon; public Entity selector; public Button(float x, float y, float width, float height, Texture texture, Entity selector) { super(); this.texture = texture; this.selector = selector; this.pos.x = x; this.pos.y = y; this.width = width; this.height = height; hitbox = new Rectangle(pos.x, pos.y, width, height); } public void setOnClickListener(OnClickListener listener){ this.listener = listener; } @Override public void draw(SpriteBatch batch){ if(texture != null) batch.draw(texture, pos.x, pos.y, width, height); if(icon != null) batch.draw(icon, pos.x, pos.y, width, height); if(isHovered() && selector != null){ selector.draw(batch); } } // Is button currently being hovered over by the mouse private boolean isHovered(){ return state == Enums.EnityState.HOVERING; } // Updates the position and size of the hitbox (Rectangle) public void updateHitbox() { hitbox.set(pos.x, pos.y, width, height); } }
OnClickListener.java
We can create an instance of this interface for each button and use it to create an over ride of the onClick function, within this function
we can write custom code that will run when onClick is called.
package uk.co.carelesslabs.ui; public interface OnClickListener { public void onClick(Button b); }
An example of setting the onClick function for a button
// Setting the onClick function for a button called button // We can put any code here btn.setOnClickListener( new OnClickListener(){ @Override public void onClick(Button b) { // Declare code to run here System.out.println("This button was clicked."); } });
SquareMenu.java
The SquareMenu is the bottom left square shapped main menu, it has 3 icons at the moment, one for inventory/resources, a build menu and also an icon for settings.
This class extends menu, we set the position and background texture using super (calls the constructor for the extended class ‘Meny’), 4 buttons are added in a 2×2 grid.
The icons and onClick for each button is set, currently we set up only 3 of the buttons.
A new type of menu ‘Build’ is also added, this menu will be hidden by default and only shown when the build icon/button is clicked.
package uk.co.carelesslabs.ui; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Vector2; import uk.co.carelesslabs.Media; import uk.co.carelesslabs.gameclass; public class SquareMenu extends Menu { public BuildMenu build; public SquareMenu(final gameclass game){ super(0, 0, 2, Media.squareMenu); int scale = 2; addButtons(3, 2, 2, Media.pinkButton, Media.selector, scale); Button btn = buttons.get(0); btn.setOnClickListener( new OnClickListener(){ @Override public void onClick(Button b) { } }); btn = buttons.get(1); btn.icon = Media.iconSettings; btn.setOnClickListener( new OnClickListener(){ @Override public void onClick(Button b) { System.out.println("Settings."); } }); btn = buttons.get(2); btn.icon = Media.iconResources; btn.setOnClickListener( new OnClickListener(){ @Override public void onClick(Button b) { game.control.inventory = true; } }); btn = buttons.get(3); btn.icon = Media.iconBuild; buttons.get(3).setOnClickListener( new OnClickListener(){ @Override public void onClick(Button b) { build.toggleActive(); } }); // BUILDING build = new BuildMenu(pos.x + width, 0, 2, Media.mainBack); } // Draw the extended menu and also the build menu. @Override public void draw(SpriteBatch batch){ super.draw(batch); build.draw(batch); } // Check if the menu / build menu buttons are being hovered over. @Override public void checkHover(Vector2 pos) { super.checkHover(pos); build.checkHover(pos); } }
BuildMenu.java
This class is another extended Menu class which is shown when the build menu button has been pressed. It has 14 columns and 2 rows which will be used
to add building options in another tutorial. Clicking these will allow items to be built/placed onto the map.
This class also adds another button which will call toggleActive(), basically this is a close menu button. The main build menu button will also close
menu when it is clicked and the build menu is currently active.
package uk.co.carelesslabs.ui; import uk.co.carelesslabs.Media; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; public class BuildMenu extends Menu { public BuildMenu(float x, int y, int scale, Texture mainBack){ super(x, y, 2, Media.mainBack); addButtons(3, 14, 2, Media.pinkButton, Media.selector, 2); setInactive(); // Add a close button Button close = new Button(0, 0, Media.close_menu.getWidth() * scale, Media.close_menu.getHeight() * scale, Media.close_menu, null); close.pos.x = x + width - (Media.close_menu.getWidth() * scale) - (6 * scale); close.pos.y = height - (Media.close_menu.getHeight() * scale) - (6 * scale); close.updateHitbox(); close.setOnClickListener( new OnClickListener(){ @Override public void onClick(Button b) { toggleActive(); } }); buttons.add(close); } // Only draw when the menu is active. public void draw(SpriteBatch batch){ if(isActive()){ super.draw(batch); } } }
Enum.java
New ENUM list for menu/button states.
... public enum MenuState { ACTIVE, DISABLED, HOVEROVER, CLICKED }
Control.java
New Vector for the mouse position which is updated on mouseMoved.
... public Vector2 mousePos = new Vector2(); ... @Override public boolean mouseMoved(int screenX, int screenY) { mousePos.set(screenX, screenHeight - screenY); return false; }
The menus are not yet complete but this gives us a decent foundation for completing / progressing our HUD.