Monday, May 13, 2013

Libgdx Scene2d complex UI elements

Working on my game UI using scene2d, I found myself needing custom elements composed by multiple actors, like icon + label + button.

A common example is an item listing for a shop screen, in which you need to have a list of buyable items, each with an icon, a description, price, and a "buy" button. A good approach is to create an class that extends Group and contains various other actors.
In the following example, ShopItem is just a POJO that contains basic info about the item (actually deserialized from a JSON file).

 public class ItemListing extends Group {  
   
     public ItemListing(final ShopItem item) {  
   
         // background image  
         Image background = new Image(Assets.white);  
         background.setBounds(getX(), getY(), 800, 86);  
         addActor(background);  
   
         // item icon, 80x80  
         Image icon = new Image(item.icon);  
         icon.setBounds(getX()+3, getY()+3, 80, 80);  
         addActor(icon);  
   
         // item name  
         Label labelName = new Label(item.name, Assets.skin, "item-title-style");  
         labelName.setPosition(getX()+88, getY()+54);  
         addActor(labelName);  
   
         // item description  
         Label labelDesc = new Label(item.description, Assets.skin, "item-desc-style");  
         labelDesc.setPosition(getX()+88, getY()+1);  
         labelDesc.setWidth(500);  
         addActor(labelDesc);  
   
         // item price  
         Label labelPrice = new Label(Utils.sb.append("price:\n").append(Integer.toString(item.price)), Assets.skin, "medium-red");  
         labelPrice.setPosition(getX()+650, getY()+15);  
         labelPrice.setWidth(100);  
         labelPrice.setAlignment(Align.center);  
         addActor(labelPrice);  
   
         // buy button  
         TextButton buttonBuy = new TextButton("BUY", Assets.skin);  
         buttonBuy.setBounds(getX()+870, getY()+13, 100, 60);  
         buttonBuy.align(Align.center);  
         addActor(buttonBuy);  
   
         // buy button listener  
         buttonBuy.addListener(new InputListener() {  
             @Override  
             public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {  
                 // item buying logic  
                 return true;  
             }  
         });  
   
         // set the group size to background size  
         setBounds(getX(), getY(), background.getWidth(), background.getHeight());  
     }  
 }  

In your shop screen you can then create an ItemListing for each item, and add them to a Table.

 public class ShopScreen implements Screen {  
   
     @Override  
     public final void show() {  
   
         // add title and stuff to your screen  
         Label labelTitle = new Label("ITEM SHOP", Assets.skin, "screen-title-style");  
         labelTitle.setBounds(0, SCREEN_HEIGHT-50, 1024, 50);  
         stage.addActor(labelTitle);  
   
         // buyable item list table  
         Table table = new Table();  
         table.setFillParent(true);  
         table.align(Align.bottom).padBottom(20).padTop(20);  
         table.defaults().space(10);  
   
         table.add(new Label("BUY ITEMS", Assets.skin, "section-title-style"));  
   
         ItemListing listing;  
         for (int i = 0, len = Assets.items.size; i < len; i++) {  
             listing = new ItemListing(Assets.activeItems.get(i));  
             table.row();  
             table.add(listing);  
         }  
   
         // put the table inside a scrollpane  
         ScrollPane scrollPane = new ScrollPane(table);  
         scrollPane.setBounds(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT-80);  
         scrollPane.setScrollingDisabled(true, false);  
         scrollPane.setOverscroll(false, false);  
         scrollPane.invalidate();  
         stage.addActor(scrollPane);  
     }  
   
     @Override  
     public void render(float delta) {  
         stage.act(delta);  
         stage.draw();  
     }  
 }  

Remember that a Group is still an Actor, so you can add actions to it. The following example is a small box showing an achievement, that appears and disappears sliding in and out from the bottom of the screen, when the start() method is called.

 public class AchievementBox extends Group {  
   
     private final Label labelDesc;  
     private float initialX = SCREEN_WIDTH/2-256;  
     private float initialY = -46;  
   
     public AchievementBox() {  
   
         Image background = new Image(Assets.white);  
         background.setBounds(getX(), getY(), 512, 46);  
         addActor(background);  
   
         Image icon = new Image(Assets.achievementIcon);  
         icon.setBounds(getX()+3, getY()+3, 40, 40);  
         addActor(icon);  
   
         labelDesc = new Label("", Assets.skin, "normal-text-style");  
         labelDesc.setPosition(getX()+48, getY()+20);  
         labelDesc.setWidth(450);  
         addActor(labelDesc);  
   
         setBounds(getX(), getY(), background.getWidth(), background.getHeight());  
     }  
   
     public void start() {  
         if (getActions().size > 0) clearActions();  
         float movement = getHeight()+10;  
         setPosition(initialX, initialY);  
         addAction(Actions.sequence(Actions.moveBy(0, movement, 1f, Interpolation.sine),  
                 Actions.delay(3f),  
                 Actions.moveBy(0, -movement, 1f, Interpolation.sine),  
                 Actions.hide()));  
         setVisible(true);  
     }
   
     public void setText(String text) {  
         labelDesc.setText(text);  
     }  
 }