Scaling Util & A Cool Text Menu for Desktop/Android Dev

First, I wanted to share a cool Text-based Menu you can use for you Android/Desktop Games (since I haven’t done anything in the way of documentation for the 2D framework, I thought more examples might help).

The menu just displays a list of text links, however… they ripple outward from the point of contact when clicked or touched.

Here is a vid showing how they work… then the classes needed and a usage sample (In the usage sample, you’ll see how to also use ExecuteAction to call a method after a dureation of time has passed):

[video]http://youtu.be/V8nYDnoNyU4[/video]

Here is the scale utility (used to determine basic screen scaling and also has a method for adjust fonts appropriately:

ScaleUtil.java
[java]
import com.jme3.font.BitmapFont;
import com.jme3.font.LineWrapMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import tonegod.gui.controls.text.TextElement;
import tonegod.gui.core.Screen;
import tonegod.gui.core.utils.UIDUtil;

/**
*

  • @author t0neg0d
    */
    public class ScaleUtil {
    private static float REF_WIDTH = 480;
    private static float REF_HEIGHT = 720;

    private Main main;
    private Screen screen;

    private float baseFontSize = 30;
    private float gameScale = 1;
    private float fontScale = 1;

    public ScaleUtil(Main main, Screen screen) {
    this.main = main;
    this.screen = screen;

     gameScale = screen.getWidth()/REF_WIDTH;
     fontScale = getFontScale(baseFontSize)/baseFontSize;
    

    }

    public float getGameScale() { return this.gameScale; }
    public float getFontScale() { return this.fontScale; }

    private float getFontScale(float startSize) {
    Vector2f dim1 = new Vector2f(REF_WIDTH, REF_HEIGHT);
    TextElement refString = getTestLabel(UIDUtil.getUID(),“Testing”, dim1);
    refString.setFontSize(startSize);
    float refScale = refString.getAnimText().getLineWidth()/dim1.x;

     Vector2f dim2 = new Vector2f(screen.getWidth(), REF_HEIGHT);
     TextElement testString = getTestLabel(UIDUtil.getUID(),"Testing", dim2);
     startSize = 5;
     testString.setFontSize(startSize);
     float testScale = testString.getAnimText().getLineWidth()/dim2.x;
     
     while (testScale < refScale) {
     	startSize++;
     	testString.setFontSize(startSize);
     	testScale = testString.getAnimText().getLineWidth()/dim2.x;
     }
     
     return startSize;
    

    }

    private TextElement getTestLabel(String UID, String text, Vector2f dim) {
    TextElement el = new TextElement(screen, UID, Vector2f.ZERO, new Vector2f(dim), null) {
    @Override
    public void onUpdate(float tpf) { }
    @Override
    public void onEffectStart() { }
    @Override
    public void onEffectStop() { }
    };
    el.setIsResizable(false);
    el.setIsMovable(false);
    el.setUseTextClipping(false);
    el.setTextWrap(LineWrapMode.NoWrap);
    el.setTextVAlign(BitmapFont.VAlign.Center);
    el.setTextAlign(BitmapFont.Align.Center);
    el.setFont(main.getDefaultFont());
    el.setFontColor(ColorRGBA.White);
    el.setFontSize(baseFontSize);//*main.getGameScale());
    el.setText(text);
    el.setIgnoreMouse(true);
    el.getAnimText().setIgnoreMouse(true);
    return el;
    }
    }
    [/java]

Here are the 3 classes needed:

TextButton.java
[java]
import com.jme3.font.BitmapFont;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.math.Vector2f;
import tonegod.gui.core.Screen;
import tonegod.gui.framework.core.AnimText;
import tonegod.gui.listeners.MouseButtonListener;

/**
*

  • @author t0neg0d
    */
    public class TextButton extends AnimText implements MouseButtonListener {
    Screen screen;

    public TextButton(Screen screen, Vector2f position, BitmapFont font) {
    super(screen.getApplication().getAssetManager(), font);
    this.screen = screen;
    this.setIsMovable(false);
    this.setZOrderEffect(ZOrderEffect.None);
    }

    public void onMouseLeftPressed(MouseButtonEvent evt) { }

    public void onMouseLeftReleased(MouseButtonEvent evt) { }

    public void onMouseRightPressed(MouseButtonEvent evt) { }

    public void onMouseRightReleased(MouseButtonEvent evt) { }
    }
    [/java]

TextMenuItem.java
[java]
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.math.Vector2f;
import tonegod.gui.core.Screen;
import tonegod.gui.core.utils.UIDUtil;
import tonegod.gui.framework.animation.ScaleByAction;
import tonegod.gui.framework.core.QuadData;

/**
*

  • @author t0neg0d
    */
    public class TextMenuItem {
    private Main main;
    private Screen screen;
    private TextMenu menu;
    private String label;
    private TextButton button;
    private int index;
    private String UID;
    private int sQuadIndex = 0;
    private int lQuadIndex = 0, rQuadIndex = 0;
    private float rippleTime = .05f, rippleCounter = 0;
    private float scaleTime = 0.25f;
    private boolean ripple = false;
    private float executeTime = 0;

    public TextMenuItem(Main main, Screen screen, TextMenu menu, String label) {
    this.main = main;
    this.screen = screen;
    this.menu = menu;
    this.label = label;
    this.index = menu.getNextIndex();
    this.UID = UIDUtil.getUID();

     initMenuItem();
    

    }

    private void initMenuItem() {
    button = new TextButton(screen, Vector2f.ZERO, main.getDefaultFont()) {
    @Override
    public void animElementUpdate(float tpf) {
    super.animElementUpdate(tpf);
    rippleText(tpf);
    }

     	@Override
     	public void onMouseLeftReleased(MouseButtonEvent evt) {
     		startRippleEffect(screen.getEventQuad().userIndex);
     		menu.handleMenuItemClick(index);
     	}
     };
     button.setFontSize(menu.getDefaultFontSize());
     button.setText(label);
     
     executeTime = (button.getQuads().size()-1)*rippleTime+scaleTime;
    

    }

    public TextButton getButton() { return this.button; }

    public String getUID() { return this.UID; }

    public float getExecuteTime() {
    return this.executeTime;
    }

    private void startRippleEffect(int quadIndex) {
    sQuadIndex = quadIndex;
    lQuadIndex = quadIndex;
    rQuadIndex = quadIndex;

     ripple = true;
    

    }

    public void resetRipple() {
    ripple = false;
    rippleCounter = 0;
    for (QuadData qd : button.getQuads().values()) {
    qd.actions.clear();
    qd.setScale(1, 1);
    qd.setRotation(0);
    }
    }

    private void rippleText(float tpf) {
    if (ripple) {
    rippleCounter += tpf;
    if (rippleCounter >= rippleTime) {
    if (lQuadIndex == rQuadIndex) {
    if (button.getQuadDataAt(sQuadIndex).actions.isEmpty()) {
    ScaleByAction lScale = new ScaleByAction();
    lScale.setAmount(.6f, .85f);
    lScale.setDuration(.25f);
    lScale.setAutoReverse(true);
    button.getQuadDataAt(sQuadIndex).addAction(lScale);
    }
    lQuadIndex–;
    rQuadIndex++;
    } else {
    if (lQuadIndex > -1) {
    if (button.getQuadDataAt(lQuadIndex).actions.isEmpty()) {
    ScaleByAction lScale = new ScaleByAction();
    lScale.setAmount(.6f, .85f);
    lScale.setDuration(.25f);
    lScale.setAutoReverse(true);
    button.getQuadDataAt(lQuadIndex).addAction(lScale);
    }
    lQuadIndex–;
    }
    if (rQuadIndex < button.getQuads().size()) {
    if (button.getQuadDataAt(rQuadIndex).actions.isEmpty()) {
    ScaleByAction rScale = new ScaleByAction();
    rScale.setAmount(.6f, .85f);
    rScale.setDuration(.25f);
    rScale.setAutoReverse(true);
    button.getQuadDataAt(rQuadIndex).addAction(rScale);
    }
    rQuadIndex++;
    }
    if (lQuadIndex == -1 && rQuadIndex == button.getQuads().size()) {
    boolean active = false;
    for (QuadData qd : button.getQuads().values()) {
    if (!qd.actions.isEmpty()) {
    active = true;
    break;
    }
    }
    if (!active)
    resetRipple();
    }
    }
    rippleCounter = 0;
    }
    }
    }
    }
    [/java]

TextMenu.java
[java]
import com.jme3.math.Vector2f;
import java.util.LinkedList;
import java.util.List;
import tonegod.gui.core.Screen;

/**
*

  • @author t0neg0d
    */
    public abstract class TextMenu {
    private Main main;
    private Screen screen;
    private List<TextMenuItem> menuItems = new LinkedList();
    private int index = -1;
    private float fontSize = 50;
    private float spacer = 10;
    private Vector2f pos = new Vector2f();

    public TextMenu(Main main, Screen screen) {
    this.main = main;
    this.screen = screen;
    fontSize *= main.getScaleManager().getFontScale();
    spacer *= main.getScaleManager().getGameScale();
    }

    public void addMenuItem(String label) {
    index++;
    TextMenuItem mi = new TextMenuItem(main, screen, this, label);
    menuItems.add(mi);
    }

    public void pack() {
    float lineHeight = menuItems.get(0).getButton().getLineHeight();
    float totalSize = lineHeightmenuItems.size()+(spacer(menuItems.size()-1));
    float halfSize = totalSize/2;

     pos.set(screen.getWidth()/2,screen.getHeight()/2+(halfSize));
     
     for (TextMenuItem mi : menuItems) {
     	pos.setX(screen.getWidth()/2-(mi.getButton().getLineWidth()/2));
     	mi.getButton().setPosition(pos);
     	pos.setY(pos.getY()-(lineHeight+spacer));
     }
    

    }

    public TextMenuItem getMenuItem(int index) {
    return menuItems.get(index);
    }

    public int getNextIndex() { return this.index; }
    public float getDefaultFontSize() { return this.fontSize; }

    public void displayMenu() {
    for (TextMenuItem mi : menuItems) {
    main.getMenuLayer().addAnimElement(mi.getUID(),mi.getButton());
    }
    }

    public void hideMenu() {
    for (TextMenuItem mi : menuItems) {
    main.getMenuLayer().removeAnimElement(mi.getUID());
    }
    }

    public abstract void handleMenuItemClick(int index);
    }
    [/java]

Create an AnimLayer for your Menus to display in in your main class (or alter the source above to reflect where ever you want to put this:

[java]
AnimLayer menuLayer;
ScaleUtil scaleManager;
// in init
scaleMAnager = new ScaleUtil(this, screen);
menuLayer = screen.addAnimLayer(“menuLayer”);
// Somewhere in main
public ScaleUtil getScaleManager() { return this.scaleManager; }
public AnimLayer getMenuLayer() { return this.menuLayer; }
[/java]

And lastly, Usage Sample:
[java]
menu = new TextMenu(main, screen) {
@Override
public void handleMenuItemClick(int index) {
ExecuteAction ea;
switch (index) {
case 0:
System.out.println("0: " + menu.getMenuItem(index).getButton().getText());
break;
case 1:
System.out.println("1: " + menu.getMenuItem(index).getButton().getText());
break;
case 2:
System.out.println("2: " + menu.getMenuItem(index).getButton().getText());
break;
case 3:
ea = new ExecuteAction() {
@Override
public void execute() { System.exit(0); }
};
screen.getAnimManager().addQueuedAction(ea, menu.getMenuItem(index).getButton(), menu.getMenuItem(index).getExecuteTime());
break;
}
}
};
menu.addMenuItem(“New Game”);
menu.addMenuItem(“Options”);
menu.addMenuItem(“Controls”);
menu.addMenuItem(“Quit”);
menu.pack();

// To show the menu
menu.displayMenu();

// To remove the menu:
menu.hideMenu();
[/java]

Anyways, hope someone finds this useful.

4 Likes

Minor update to the ripple effect in TextMenuItem.java

1 Like

That’s nice!

1 Like