Buttons for your gamesThe official Slick tutorial for
slickout provides an example to create a menu with buttons. Let's have a look at it:
The variable selection is set everytime the mouse is moved.
public void mouseMoved(int oldx, int oldy, int newX, int newY){
if(newX > 228 && newX < 702){
// start game
if ( newY > 308 && newY < 389){
selection = 1;
// exit game
}else if ( newY > 475 && newY < 544){
selection = 2;
}else {
selection = -1;
}
}
}
The numbers 308 and 475 seem to be the width and height of the images used for the buttons. What if you decide to use other images? You will have to change code in several places.
The code uses comments to state the meaning of the numbers 1 and 2 for the variable selection. You have to keep in mind the meaning of the selection numbers in three different places. First is above, second is here:
@Override
public void render(GameContainer container, StateBasedGame game, Graphics g)
throws SlickException {
background.draw();
if(selection == 1){
selector.draw(158, 310);
selector.draw(694, 310);
PlayerInfo.createNewCurrentPlayerInfo();
}else if(selection == 2){
selector.draw(158, 474);
selector.draw(694, 474);
}
// TODO: Log this
g.drawString("TOPSCORE : " + topScore, 10, 10) ;
}
And third is here:
public void update(GameContainer container, StateBasedGame game, int delta)
throws SlickException {
if(optionSelected == 1){
game.enterState(2);
}else if(optionSelected == 2){
System.exit(0);
}
}
This code is not very flexible, not readable and not good to maintain. If you add more buttons, you will loose track of what is going on and the class for the menu will bloat. That is the reason I made my own solution.
These are the features I wanted to realize:
1. Animated buttons for the build menu in the game- button has an activated and an inactivated state
- button icon plays an animation when activated
- only one button of that group is activated at once (if another button is pressed, the previously active button gets inactive)
- sounds for hovering and clicking a button
2. Font buttons for the main menu- shall work with fonts instead of images
- font gets bigger when hovered and changes its color
- button can be disabled for the user, which is shown by a grey color. a disabled button doesn't react to hovering or clicking
- sounds for hovering and clicking a button
Slick provides a
MouseOverArea. You can set the image when the area is hovered and when it is not hovered. You can also set a color overlay for hovering instead. I will use this as a basis for the two button types.
1. Animated buttonsI used the MouseOverArea as a basis to create an animated button class.
The animated button has two layers.
First layer:The plain button in active or inactive state
Second layer:The animated icon
Last but not least the MouseOverArea's ability to color the icon when hovering is used.
Here is the constructor of our AnimatedButton (which extends MouseOverArea):
public AnimatedButton(GUIContext guic, Animation animation, int x, int y,
StateBasedGame sbg, int stateID) throws SlickException {
super(guic, animation.getImage(0), x, y);
super.setMouseDownColor(Color.red);
super.setMouseOverColor(Color.blue);
this.animation = animation;
this.sbg = sbg;
this.stateID = stateID;
inactiveButton = new Image("sprites/menu/button.png");
activeButton = new Image("sprites/menu/button2.png");
}
Note, that the first image of the animation is used as icon for the second layer, when the animation is not played (button is inactive).
The mouseMoved() method checks for hovering:
@Override
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
if (sbg.getCurrentStateID() == stateID) {
if (isMouseOver() && !lastMouseOver && !isActivated()) {
SoundManager.getInstance().getButtonOver().play(1, (float) .2);
lastMouseOver = true;
} else if (!isMouseOver()) {
lastMouseOver = false;
}
}
super.mouseMoved(oldx, oldy, newx, newy);
}
Because the sound shall only be played once it is important if the mouse was over the area before or not (otherwise the sound would play again as long as we hover the button). We want it only playing for entering the area. This is why we need the boolean lastMouseOver.
The rendering is done like this:
@Override
public void render(GUIContext guic, Graphics g) {
if (activated) {
g.drawImage(activeButton, getX() - 7, getY() - 5);
g.drawAnimation(animation, getX() + 2, getY() + 2);
} else {
g.drawImage(inactiveButton, getX() - 7, getY() - 5);
super.render(guic, g);
}
}
If the button is activated we draw the active button image and the animation is played. Otherwise we only draw inactive button image. The call to super.render() will draw the inanimated icon (first image of the animation).
There are little adjustments to the position, so that the icon/animation is in the middle of the button. Once activated the second layer is moved a bit to the right and a bit down so that the impression is generated that the button was pressed.
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (isMouseOver() && sbg.getCurrentStateID() == stateID) {
activated = !activated;
SoundManager.getInstance().getButtonClick().play();
}
super.mouseClicked(button, x, y, clickCount);
}
The mouseClicked method has not much to tell about. A click sound is played and the button is changed from active to inactive or vice versa.
This is the code of the AnimatedButton class:
package menu.buttons;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.gui.GUIContext;
import org.newdawn.slick.gui.MouseOverArea;
import org.newdawn.slick.state.StateBasedGame;
import resourcemanager.SoundManager;
public class AnimatedButton extends MouseOverArea {
private boolean activated = false;
private boolean lastMouseOver = false;
private final Animation animation;
private final Image inactiveButton;
private final Image activeButton;
private final StateBasedGame sbg;
private final int stateID;
public AnimatedButton(GUIContext guic, Animation animation, int x, int y,
StateBasedGame sbg, int stateID) throws SlickException {
super(guic, animation.getImage(1), x, y);
super.setMouseDownColor(Color.red);
super.setMouseOverColor(Color.blue);
this.animation = animation;
this.sbg = sbg;
this.stateID = stateID;
inactiveButton = new Image("sprites/menu/button.png");
activeButton = new Image("sprites/menu/button2.png");
}
@Override
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
if (sbg.getCurrentStateID() == stateID) {
if (isMouseOver() && !lastMouseOver && !isActivated()) {
SoundManager.getInstance().getButtonOver().play(1, (float) .2);
lastMouseOver = true;
} else if (!isMouseOver()) {
lastMouseOver = false;
}
}
super.mouseMoved(oldx, oldy, newx, newy);
}
@Override
public void render(GUIContext guic, Graphics g) {
if (activated) {
g.drawImage(activeButton, getX() - 7, getY() - 5);
g.drawAnimation(animation, getX() + 2, getY() + 2);
} else {
g.drawImage(inactiveButton, getX() - 7, getY() - 5);
super.render(guic, g);
}
}
public boolean isActivated() {
return activated;
}
protected void setActivated(boolean b) {
activated = b;
}
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (isMouseOver() && sbg.getCurrentStateID() == stateID) {
activated = !activated;
SoundManager.getInstance().getButtonClick().play();
}
super.mouseClicked(button, x, y, clickCount);
}
}
Now we have a certain button group. The game I make has a menu with buttons that are used to create buildings. So only one button of that group can be active at a time. We extend the AnimatedButton to realize the BuildButton:
package menu.buttons;
import gamestates.BuildState;
import java.util.ArrayList;
import java.util.List;
import model.GridLocation;
import model.PixelPosition;
import model.Size;
import model.TileType;
import model.gameobjects.GameObject;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.gui.GUIContext;
import org.newdawn.slick.state.StateBasedGame;
public class BuildButton extends AnimatedButton {
private static List<BuildButton> buttons = new ArrayList<BuildButton>();
public BuildButton(GUIContext guic, Animation animation, int x, int y, StateBasedGame sbg, int stateID)
throws SlickException {
super(guic, animation, x, y, sbg, stateID);
buttons.add(this);
}
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (!isMouseOver()) {
// activate one button at a time
for (BuildButton b : buttons) {
if (b.isMouseOver()) {
setActivated(false);
break;
}
}
}
super.mouseClicked(button, x, y, clickCount);
}
}
The static list buttons holds all the BuildButtons ever created and thus makes it possible to deactivate every other button of the BuildButton group.
2. Font buttonsThe buttons for the main menu look like this:
The first button here is disabled, the others are enabled and the third button is hovered.
You could realize this with images of course. But everytime you like to change a color, adjust the size or change the font, you will have to create three images for every button again. So a button that takes a UnicodeFont is far more flexible for text menus than image buttons.
The FontButton is also a MouseOverArea, because I wanted to use some methods of it.
Here is the constructor:
public FontButton(GUIContext guic, UnicodeFont font, String text, int x,
int y, int width, int height, StateBasedGame sbg, int stateID)
throws SlickException {
super(guic, new Image(0, 0), x, y, width, height);
this.font = font;
this.text = text;
this.sbg = sbg;
this.stateID = stateID;
this.biggerFont = FontManager.getInstance().getSameFontWithSize(font,
text, font.getFont().getSize() + 4);
}
MouseOverArea needs an image, so we create an empty image in this case and set width and height manually.
biggerFont is created in another class. It is the same font that the constructor got, just with another size. Here is the code:
public UnicodeFont getSameFontWithSize(UnicodeFont font, String glyphs, int size)
throws SlickException {
UnicodeFont biggerFont = new UnicodeFont(font.getFont(), size, false, false);
biggerFont.addGlyphs(glyphs);
biggerFont.getEffects().addAll(font.getEffects());
biggerFont.loadGlyphs();
return biggerFont;
}
Only the necessary glyphs are added, the effects are copied and the font with the adjusted size is returned.
The methods mouseClicked() and mouseMoved() are pretty similar to the animated button. The only difference: This button checks whether it is enabled. A disabled button won't react with a sound.
So let's have a look into the render() method (I added comments to explain):
@Override
public void render(GUIContext guic, Graphics g) {
g.setFont(font); //set default font
if (isEnabled) {
g.setColor(Color.orange); //set color to orange if enabled
if (isMouseOver()) {
g.setFont(biggerFont); //draw bigger font if hovered
g.setColor(new Color(200, 50, 30)); //change color if hovered
}
} else {
g.setColor(Color.gray); //set gray color if disabled
}
g.drawString(text, getX(), getY()); //draw the text with that settings
super.render(guic, g);
}
Changing the font now only needs the change of one line of code instead of creating three different pictures for every button in the menu.
Here is the whole code for the FontButton:
package menu.buttons;
import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.gui.GUIContext;
import org.newdawn.slick.gui.MouseOverArea;
import org.newdawn.slick.state.StateBasedGame;
import resourcemanager.FontManager;
import resourcemanager.SoundManager;
public class FontButton extends MouseOverArea {
private final UnicodeFont font;
private final String text;
private boolean lastMouseOver = false;
private final StateBasedGame sbg;
private final int stateID;
private boolean isEnabled = true;
private final UnicodeFont biggerFont;
public FontButton(GUIContext guic, UnicodeFont font, String text, int x,
int y, int width, int height, StateBasedGame sbg, int stateID)
throws SlickException {
super(guic, new Image(0, 0), x, y, width, height);
this.font = font;
this.text = text;
this.sbg = sbg;
this.stateID = stateID;
this.biggerFont = FontManager.getInstance().getSameFontWithSize(font,
text, font.getFont().getSize() + 4);
}
public void setIsEnabled(boolean b) {
isEnabled = b;
}
public boolean isEnabled() {
return isEnabled;
}
@Override
public void render(GUIContext guic, Graphics g) {
g.setFont(font);
if (isEnabled) {
g.setColor(Color.orange);
if (isMouseOver()) {
g.setFont(biggerFont);
g.setColor(new Color(200, 50, 30));
}
} else {
g.setColor(Color.gray);
}
g.drawString(text, getX(), getY());
super.render(guic, g);
}
@Override
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
if (sbg.getCurrentStateID() == stateID && isEnabled) {
if (isMouseOver() && !lastMouseOver) {
SoundManager.getInstance().getButtonOver().play(1, (float) .2);
lastMouseOver = true;
} else if (!isMouseOver()) {
lastMouseOver = false;
}
}
super.mouseMoved(oldx, oldy, newx, newy);
}
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (isMouseOver() && sbg.getCurrentStateID() == stateID && isEnabled) {
SoundManager.getInstance().getButtonClick().play();
}
super.mouseClicked(button, x, y, clickCount);
}
}
You can make this even more flexible by adding fields and setters for the colors and sounds used here.
3. Add some ActionNow we have two types of buttons--the animated button and the font button. But how do we apply some actions that shall be done, when the buttons are clicked?
With the current classes you can determine the actions that shall be performed by overriding mouseClicked like this:
exitButton = new FontButton(gc, font, "Exit", x, y, width, height, sbg, MAIN_MENU_STATE) {
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (isMouseOver() && sbg.getCurrentStateID() == id && isEnabled()) {
gc.exit();
}
super.mouseClicked(button, x, y, clickCount);
}
};
But you have to remember to call super.mouseClicked, so the other actions (sound and so on) are played too. You also need to check by yourself again whether the mouse is over the button, whether the stateID is the current one and whether the button is enabled.
That is duplicate code and you have to remember a lot in order to make it work right. So there has to be a better solution.
Instead of overriding the button everytime, we will provide to add an instance of our own ButtonAction.
First we create an interface:
public interface ButtonAction {
public void perform();
}
Second we add a List that holds ButtonAction instances to the FontButton (or AnimatedButton) and provide a method to add them:
private final List<ButtonAction> actions = new ArrayList<ButtonAction>();
public void add(ButtonAction action) {
actions.add(action);
}
Third we modify the mouseClicked()-method so that it calls perform() for all ButtonActions:
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (isMouseOver() && sbg.getCurrentStateID() == stateID && isEnabled) {
SoundManager.getInstance().getButtonClick().play();
for(ButtonAction action : actions) {
action.perform();
}
}
super.mouseClicked(button, x, y, clickCount);
}
Eventually the code to creating our exitButton:
exitButton = new FontButton(gc, font, "Exit", x, y, width, height, sbg, MAIN_MENU_STATE);
exitButton.add(new ButtonAction() {
@Override
public void perform() {
gc.exit();
}
});
This is much easier to use and also more flexible:
If you have several buttons with the same purpose, you can create one ButtonAction and add it for all of them instead of writing the code several times.
If one of your buttons shall perform several actions at once, you can just add more than one ButtonAction instance to it.
That's it. If you have any questions or suggestions, just tell me.
Deque