Sunday, December 1, 2013

Retrieving data from a JSON webservice in Android - simple tutorial

In this guide I'll show how to write a basic Android app that retrieves data from a JSON webservice, and simply shows it in a list.

We'll be using the following stuff.

Tutorial

Create a new Android project, with a MainActivity class inside. Create a "libs" folder inside the project, and put the GSON jar in there. When you refresh the project, Eclipse should automatically add the jar to the project's build path.

Let's have our activity extend ListActivity instead of Activity.

 public class MainActivity extends ListActivity {  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
      }  

Now let's look at the "citiesJSON" webservice API.

Webservice Type : REST 
Url : api.geonames.org/citiesJSON?
Parameters : 
north,south,east,west : coordinates of bounding box 
callback : name of javascript function (optional parameter) 
lang : language of placenames and wikipedia urls (default = en)
maxRows : maximal number of rows returned (default = 10)

Result : returns a list of cities and placenames in the bounding box, ordered by relevancy (capital/population). Placenames close together are filterered out and only the larger name is included in the resulting list.

We'll call the webservice with the 4 coordinates parameters. The webservice also requires an "username" parameter. You can use the "pimentoso" user for this test, or you can register your own username here: http://www.geonames.org/login

Go to the JSONgen website, and paste the sample URL for the webservice.


When you hit "generate", the website will have you download a zip file containing the generated classes. Unzip the file and copy the classes in your project. The classes will look like this

 package com.example.placesjson;  
 import java.util.List;  
 public class GeonameList {  
      private List<Geonames> geonames;  
      public List<Geonames> getGeonames() {  
           return this.geonames;  
      }  
      public void setGeonames(List<Geonames> geonames) {  
           this.geonames = geonames;  
      }  
 }  

 package com.example.placesjson;  
 public class Geonames {  
      private String countrycode;  
      private String fcl;  
      private String fclName;  
      private String fcode;  
      private String fcodeName;  
      private Number geonameId;  
      private Number lat;  
      private Number lng;  
      private String name;  
      private Number population;  
      private String toponymName;  
      private String wikipedia;  
      [... getters and setters]  

Now we're gonna work on the activity. We're going to create the "callService()" method that does the main job: starting a thread that calls the webservice, and deserialize the data. The comments in the code should be self-explanatory.

 private void callService() {  
      // Show a loading dialog.  
      dialog = ProgressDialog.show(this, "Loading", "Calling GeoNames web service...", true, false);  
      // Create the thread that calls the webservice.  
      Thread loader = new Thread() {  
           public void run() {  
                // init stuff.  
                Looper.prepare();  
                cities = new GeonameList();  
                boolean error = false;  
                // build the webservice URL from parameters.  
                String wsUrl = "http://api.geonames.org/citiesJSON?lang=en&username=pimentoso";  
                wsUrl += "&north="+COORD_N;  
                wsUrl += "&south="+COORD_S;  
                wsUrl += "&east="+COORD_E;  
                wsUrl += "&west="+COORD_W;  
                String wsResponse = "";  
                try {  
                     // call the service via HTTP.  
                     wsResponse = readStringFromUrl(wsUrl);  
                     // deserialize the JSON response to the cities objects.  
                     cities = new Gson().fromJson(wsResponse, GeonameList.class);  
                }  
                catch (IOException e) {  
                     // IO exception  
                     Log.e(TAG, e.getMessage(), e);  
                     error = true;  
                }  
                catch (IllegalStateException ise) {  
                     // Illegal state: maybe the service returned an empty string.  
                     Log.e(TAG, ise.getMessage(), ise);  
                     error = true;  
                }  
                catch (JsonSyntaxException jse) {  
                     // JSON syntax is wrong. This could be quite bad.  
                     Log.e(TAG, jse.getMessage(), jse);  
                     error = true;  
                }  
                if (error) {  
                     // error: notify the error to the handler.  
                     handler.sendEmptyMessage(CODE_ERROR);  
                }  
                else {  
                     // everything ok: tell the handler to show cities list.  
                     handler.sendEmptyMessage(CODE_OK);  
                }  
           }  
      };  
      // start the thread.  
      loader.start();  
 }  

The code contains the "readStringFromUrl()" utility method which is not covered in this guide. Please download the project zip at the end of this post to grab the code.

When the thread has completed, the "cities" object shoud contain data returned from the webservice.


This is the simple handler that's called at the end of the method.

 // This handler will be notified when the service has responded.  
 final Handler handler = new Handler() {  
      public void handleMessage(Message msg) {  
           dialog.dismiss();  
           if (msg.what == CODE_ERROR) {  
                Toast.makeText(MainActivity.this, "Service error.", Toast.LENGTH_SHORT).show();  
           }  
           else if (cities != null && cities.getGeonames() != null) {  
                Log.i(TAG, "Cities found: " + cities.getGeonames().size());  
                buildList();  
           }  
      }  
 };  

The last thing that remains is to actually populate the list, so let's write the "buildList()" method.

 private void buildList() {  
      // init stuff.  
      List<Map<String, String>> data = new ArrayList<Map<String, String>>();  
      Map<String, String> currentChildMap = null;  
      String line1;  
      String line2;  
      // cycle on the cities and create list entries.  
      for (Geonames city : cities.getGeonames()) {  
           currentChildMap = new HashMap<String, String>();  
           data.add(currentChildMap);  
           line1 = city.getToponymName() + " (" + city.getCountrycode() + ")";  
           line2 = "Population: " + city.getPopulation();  
           currentChildMap.put("LABEL", line1);  
           currentChildMap.put("TEXT", line2);  
      }  
      // create the list adapter from the created map.  
      adapter = new SimpleAdapter(this, data, android.R.layout.simple_list_item_2,   
                new String[] { "LABEL", "TEXT" },  
                new int[] { android.R.id.text1, android.R.id.text2 });  
      setListAdapter(adapter);  
 }  

Let's wrap it all up, by calling "callService()" when the activity is started.

 @Override  
 protected void onCreate(Bundle savedInstanceState) {  
      super.onCreate(savedInstanceState);  
      callService();  
 }  

The final result should be something like this.



Error handling

You will notice that when the webservice returns an error, a JsonSyntaxException is not actually thrown. This is because GSON only throws the exception if your class has a field whose type didn't match what is in the JSON. So, in case of an error like this-

{"status":{"message":"user does not exist.","value":10}}

The exception is not actually thrown. So if you want to retrieve the error message, you could expand your GeonameList class to contain a Status object, so GSON can fill it when it is returned. 
You can read more about it here-

Other libraries

If GSON is a bit too heavy for you (with its almost 200KB) you can look into JSONbeans, a lighter library by EsotericSoftware.
https://github.com/EsotericSoftware/jsonbeans

Download

You can get the project on Github.
https://github.com/Pimentoso/AndroidPlacesJson

Alternatively, you can download the Eclipse project ZIP here.
http://www.pimentoso.com/uploads/PlacesFromJson.zip

Friday, November 8, 2013

New Facebook page

We created a new page for Pimentoso on Facebook!


On this page we will post updates, releases and graphics of all our apps.
Please follow us there too if you were a fan of the Candy Falls page!

Monday, September 16, 2013

Custom color chooser for Hexagons LWP

Our Evangelion-themed live wallpaper gets a new option: you can now create a custom color theme, choosing the colors for NORMAL, WARNING and EMERGENCY state of the hexagons. A color wheel lets you select hue and there's a slider to adjust lightness.


Thanks to Lars Werkman and Marie Schweiz for providing this awesome open-source holo color picker.

As usual, you can grab the app on Google Play at this link.

Thursday, July 25, 2013

MiSoundRecorder graphic fixes for Nexus 4

Maybe some of you will know MiSoundRecorder, a neat sound recorder app ported from MIUI rom by the XDA user HootanParsa. It records in MP3 and OGG formats.

The project is stalled and has gone open source, so I grabbed the sources and did a couple layout fixes so that everything is aligned correctly on Nexus 4 (and hopefully all other xhdpi phones).

Any credit for the app goes to their original creators.

Get the APK here. It's compiled for Android 2.3.3+
http://www.pimentoso.com/uploads/MiSoundRecorder.1.4.apk

Screenshot from Nexus 4:


Original XDA thread:
http://forum.xda-developers.com/showthread.php?t=1752011

Saturday, July 20, 2013

Mobile Data On/Off Widget

I recently got myself a Nexus 4, and I started looking for some simple on/off widgets to compensate the lack of features of the stock 4.2 Power Control widget (which is actually unchanged since 2.2). I came from Cyanogenmod so I was used to having all the toggles I could ever need.

I was surprised that on Google Play there were no widgets made to resemble the stock Power Control widget. In particular, I was looking for a mobile data switch on/off. Working in front of a pc all day, I usually turn off mobile data when I don't need it, to save battery.

So I tried myself with this simple widget and I styled it to look neat near the holo Power Control widget. Check the screenshot below for comparison.



The widget is only a few KB, with no ads, no settings app, no icons in app drawer, only pure and simple widget. You can grab it on Google Play at this link.

Tuesday, July 16, 2013

Hexagon Battery Indicator Live Wallpaper

New live wallpaper! Free to download.

Monitor your battery level with an hexagon grid. The number of filled hexagons reflects your battery charge, and their color shifts from green to yellow to red. Also reacts to SMS.
Heavily inspired by the NERV HQ emergency hexagons in Evangelion.

Video:


Download here!


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);  
     }  
 }  

Tuesday, April 2, 2013

Libgdx Scene2d Skin quick tutorial

This post shows a brief introduction to using Skins in your Scene2d project. It's very useful to use a JSON file that declares all the styles for your Scene2d elements, but it can be counter-intuitive at the beginning.

  • First, create a skin.json file in your asset folder.
  • Create a couple BitmapFonts using Hiero. Let's say you create "arial25" for text and "impact50" for titles. Put the .fnt and .png files inside your asset folder. 
  • At this point, you can already start writing stuff in your skin.json file. You got the BitmapFonts ready, so you can define styles for the Label elements. For the colors you can specify the single RGB values (as float in the 0-1 interval) or you can use the hexadecimal notation. If you use hexadecimal, make sure the value is wrapped in double quotes so the parser always reads it as string.
   {  
     com.badlogic.gdx.graphics.g2d.BitmapFont: {  
      normaltext: { file: arial25.fnt },  
      titletext: { file: impact50.fnt },  
     },  
     com.badlogic.gdx.graphics.Color: {  
      red: { a: 1, b: 0, g: 0, r: 1 },  
      green: { a: 1, b: 0, g: 1, r: 0 },  
      blue: { a: 1, b: 1, g: 0, r: 0 },  
      white: { hex: "FFFFFF" },  
      black: { hex: "000000" } 
     },  
     com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: {  
      default: { font: normaltext, fontColor: white },  
      normal-text: { font: normaltext, fontColor: white },  
      title-text: { font: titletext, fontColor: red },  
     }  
   }  
  • Fire up GIMP or Photoshop and draw a square button, let's say 50x50. Put a gradient and a stroke on it so it looks nice. Save it as PNG in some external folder with the name "button.9.png". The ".9" piece at the end will tell the game that the button is a 9patch. More on that later.
  • Make another button, identical to the first, but in another color, for the "pressed" state. If you made the button blue, make it red or something. Save with the name "button-down.9.png".
  • Make another button, identical to the first, but gray, for the "disabled" state. Save with the name "button-disabled.9.png". 
  • Open your Android SDK folder and fire up the 9patch tool. You can find it in \sdk\tools\draw9patch.bat if you're on Windows. Open your buttons, and draw some black pixels in the middle of the top and left edges, to make it stretch gracefully. Save. You can find additional info on 9patches here and here
  • Use libgdx TexturePacker to create an atlas of your 3 buttons. Obviously you can pack them together with your other game graphics. Hint: you can also pack together your BitmapFont .png files generated at step 2. This way you can have a single texture for all your UI elements, so the rendering thread has to bind less textures each frame. More info about TexturePacker here.
  • Copy the created pack.atlas and pack.png in your asset folder.
  • Now you can define Button and TextButton styles in your skin.json file. It becomes something like this.
   {  
     com.badlogic.gdx.graphics.g2d.BitmapFont: {  
      normaltext: { file: arial25.fnt },  
      titletext: { file: impact50.fnt },  
     },  
     com.badlogic.gdx.graphics.Color: {  
      red: { a: 1, b: 0, g: 0, r: 1 },  
      green: { a: 1, b: 0, g: 1, r: 0 },  
      blue: { a: 1, b: 1, g: 0, r: 0 },  
      white: { hex: "FFFFFF" },  
      black: { hex: "000000" }  
     },  
     com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: {  
      default: { font: normaltext, fontColor: white },  
      normal-text: { font: normaltext, fontColor: white },  
      title-text: { font: titletext, fontColor: red },  
     }  
     com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle: {  
      default: { up: button, down: button-down, checked: button-down, disabled: button-disabled },  
      transparent: { },  
     },  
     com.badlogic.gdx.scenes.scene2d.ui.TextButton$TextButtonStyle: {  
      default: { up: button, down: button-down, checked: button-down, disabled: button-disabled, font: normaltext, fontColor: white },  
     }  
   }  
  • Repeat the previous steps for creating more graphics for other Scene2d controls you want to style with your skin (like sliders, scrollbars, etc). But Labels and TextButtons are enough to get you started.
  • In your ApplicationListener.create() method, or whenever you load your assets, initialize the skin.
   AssetManager manager = new AssetManager();  
   ...  
   manager.load("pack.atlas", TextureAtlas.class);  
   ...  
   TextureAtlas atlas = manager.get("pack.atlas", TextureAtlas.class);  
   Skin skin = new Skin(Gdx.files.internal("skin.json"), atlas);  
  • That's it. Now you can create Labels, Buttons and TextButtons in your game, using your styles.
   Label label = new Label("Speed: 50m/s", Assets.skin); // Assets.skin is a static reference to the skin object 
   Label label = new Label("Speed: 50m/s", Assets.skin, "normal-text"); // equivalent to the previous label 
   Label label = new Label("Character selection", Assets.skin, "title-text");  

   Button buttonAttack = new Button(Assets.skin, "transparent"); // invisible button  
   TextButton buttonStart = new TextButton("Click to play!", Assets.skin);  

   Table table = new Table(Assets.skin); // this way you can enable table.add(String s)  
   table.add("First row", "title-text");  
   table.row();  
   table.add("Second row");  

More references about skins:

Tuesday, February 26, 2013

Back Issues for Android - updated

This post is shameless self-advertising for Back Issues, a VERY small android project I created some time ago, which only has ~300 downloads on Google Play. I released an update today to fix some silly problems, hence this post.

As I wrote in the release post, it's a simple app to keep track of missing issues of books, comic series, manga, dvds or whatever. It consists of 3 screens.

Main screen: series list. It shows the list of the series you have inserted, with a quickrundown of the missing issues.



In the option menu, you can do the following
  • review the tutorial dialog
  • send a plain text version of the list via email
  • NEW: backup your data to SD (backup will be saved to ".backissues" file in the SD root)
  • NEW: restore your backup from SD (overwrites all your current data)
Long-tapping on a series in the list will bring the context menu, from which you can
  • delete the series
  • send series name + missing issues via SMS
  • send series name + missing issues via email 
New series screen: you can add a new series to the list. You will be prompted for the series name, the number of total issues in the series, and the number of the last issue you own. You can also choose a color.



Saving the series will show it in the main list in "just inserted" state.
Tapping on it, you will be shown the missing issues screen.



Here you can review the total issues of this series, and check/uncheck issues you are missing. Remember to hit "save" when you're done.

The app is obviously free, and doesn't contain ads. Download link:
https://play.google.com/store/apps/details?id=com.pimentoso.android.backissues

It's currently localized to english and italian. If you want to contribute another language, leave a comment and I'll get back to you.

Wednesday, January 23, 2013

Meter and pixel units in a box2d game - LibGDX

I often see posts on the LibGDX forum from users confused about converting game units to pixel units, while coding box2d+scene2d games. I'm writing this short article to present my approach on this matter, which actually doesn't involve any unit conversion. It's all done by different cameras with different viewports.

As you know, box2d wants its bodies sized in meters. "But hey my character is 200 pixels big, I'll make it 200 meters and use a 1:1 conversion" you can't. The box2d manual says that the physics simulation starts to go freaky with dynamic bodies bigger than a truck. So stick with real-size dimensions.

The key is to have 2 different stages with different viewports. In the following example I'll use a stage to represent the box2d world, which will be the "game stage" and will have a viewport of about 3x6 meters; then I'll use another stage on top of the other one, aka "GUI stage", which will be used to render the GUI and will have a viewport of 1024x600 pixels. This way you get a meter-sized game with pixel-exact GUI.

Game Screen - this class is our main game screen. It contains the GUI stage.

 public class GameScreen implements Screen {  
   
     // this is actually my tablet resolution in landscape mode. I'm using it for making the GUI pixel-exact.  
     public static float SCREEN_WIDTH = 1024;  
     public static float SCREEN_HEIGHT = 600;  
   
     private GameWorld world; // contains the game world's bodies and actors.  
     private GameRenderer renderer; // our custom game renderer.  
     private Stage stage; // stage that holds the GUI. Pixel-exact size.  
     private OrthographicCamera guiCam; // camera for the GUI. It's the stage default camera.  
       
     @Override  
     public final void show() {  
           
         this.stage = new Stage(); // create the GUI stage
         this.stage.setViewport(SCREEN_WIDTH, SCREEN_HEIGHT, false); // set the GUI stage viewport to the pixel size
   
         world = new GameWorld();  
         renderer = new GameRenderer(world);  
           
         // add GUI actors to stage, labels, meters, buttons etc.  
         Label labelStatus = new Label("TOUCH TO START", Assets.skin);  
         labelStatus.setPosition(GenericScreen.SCREEN_WIDTH/2-500, GenericScreen.SCREEN_HEIGHT/2);  
         labelStatus.setWidth(1000);  
         labelStatus.setAlignment(Align.center);  
         labelStatus.setFontScale(2);  
         stage.addActor(labelStatus);  
         // add other GUI elements here  
     }  
       
     @Override  
     public void render(float delta) {  
           
         guiCam = (OrthographicCamera) stage.getCamera();  
         guiCam.position.set(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, 0);  
   
         Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);  
         Gdx.gl.glEnable(GL10.GL_TEXTURE_2D);  
         guiCam.update();  
   
         world.update(delta); // update the box2d world          
         stage.act(delta); // update GUI  
           
         renderer.render(); // draw the box2d world  
         stage.draw(); // draw the GUI  
     }  
 }  

Game World - our container for the box2d world and other game elements. It also contains the game stage.

 public class GameWorld {  
       
     // here we set up the actual viewport size of the game in meters.  
     public static float UNIT_WIDTH = GameScreen.SCREEN_WIDTH/160; // 6.4 meters width  
     public static float UNIT_HEIGHT = GameScreen.SCREEN_HEIGHT/160; // 3.75 meters height  
       
     public static final Vector2 GRAVITY = new Vector2(0, -9.8f);  
       
     public final Stage stage; // stage containing game actors (not GUI, but actual game elements)  
     public World box2dWorld; // box2d world  
     public Bob bob; // our playing character  
       
     public GameWorld() {      
           
         box2dWorld = new World(GRAVITY, true);          
         stage = new Stage(); // create the game stage  
         stage.setViewport(UNIT_WIDTH, UNIT_HEIGHT, false); // set the game stage viewport to the meters size   
           
         createWorld();  
     }  
       
     private void createWorld() {  
           
         // create box2d bodies and the respective actors here.  
         bob = new Bob(this);  
         stage.addActor(bob);  
         // add more game elements here  
     }  
       
     public void update(float delta) {  
       
         // perform game logic here  
         box2dWorld.step(delta, 3, 3); // update box2d world  
         stage.act(delta); // update game stage  
     }  
 }  

Bob - our character. It extends Image which is a subclass of Actor, and contains a reference to the box2d body (in this case a circle). Its texture is resized to the game meter units, but it will be rendered using the game stage camera, so even if the texture is scaled to 0.8x0.8, it will be quite big since the viewport is 6x3 meters.

 public class Bob extends Image {  
   
     public static final float RADIUS = 0.4f; // bob is a ball with 0.8m diameter      
     public final Body body; // bob's box2d body  
       
     public Bob(GameWorld world) {  
           
         // bob is an Image, so we load the graphics from the assetmanager  
         Texture tex = Assets.manager.get("characters.png", Texture.class);  
         this.setDrawable(new TextureRegionDrawable(new TextureRegion(tex, 0, 256, 128, 128)));  
           
         // generate bob's box2d body  
         CircleShape circle = new CircleShape();  
         circle.setRadius(RADIUS);  
           
         BodyDef bodyDef = new BodyDef();  
         bodyDef.type = BodyType.DynamicBody;  
         bodyDef.position.x = 2f;  
         bodyDef.position.y = 2f;  
         bodyDef.linearDamping = 0.1f;  
         bodyDef.angularDamping = 0.5f;  
           
         this.body = world.box2dWorld.createBody(bodyDef);  
         this.body.setUserData(ElementType.BOB);  
           
         Fixture fix = body.createFixture(circle, 50);  
         fix.setDensity(1);  
         fix.setFriction(1f);  
         fix.setRestitution(0.8f);  
         fix.setFilterData(filter);  
           
         circle.dispose();  
           
         // generate bob's actor  
         this.setPosition(body.getPosition().x-RADIUS, body.getPosition().y-RADIUS); // set the actor position at the box2d body position  
         this.setSize(RADIUS*2, RADIUS*2); // scale actor to body's size  
         this.setScaling(Scaling.stretch); // stretch the texture  
         this.setAlign(Align.center);  
     }  
       
     @Override  
     public void act(float delta) {  
         // here we override Actor's act() method to make the actor follow the box2d body  
         super.act(delta);  
         setOrigin(RADIUS, RADIUS);  
         setRotation(MathUtils.radiansToDegrees * body.getAngle());  
         setPosition(body.getPosition().x-RADIUS, body.getPosition().y-RADIUS);  
     }  
 }  

Game Renderer - a custom renderer used to draw the game world. It just positions the camera each frame and draws the game stage. Very simple.

 public class GameRenderer  
 {      
     GameWorld world;  
     OrthographicCamera camera;  
     Box2DDebugRenderer renderer;  
   
     public GameRenderer(GameWorld world)  
     {  
         this.world = world;  
         this.renderer = new Box2DDebugRenderer();  
       
         // we obtain a reference to the game stage camera. The camera is scaled to box2d meter units  
         this.camera = (OrthographicCamera) world.stage.getCamera();  
       
         // center the camera on bob (optional)  
         camera.position.x = world.bob.body.getPosition().x;  
         camera.position.y = world.bob.body.getPosition().y;  
     }  
   
     public void render()  
     {  
         // have the camera follow bob  
         camera.position.x = world.bob.body.getPosition().x;  
         camera.position.y = world.bob.body.getPosition().y;  
           
         Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);  
   
         // box2d debug renderering (optional)  
         camera.update();   
         renderer.render(world.box2dWorld, camera.combined);  
          
         // game stage rendering  
         world.stage.draw();  
     }  
 }  

Tuesday, January 22, 2013

Animated actor with scene2d - LibGDX

I am currently tampering with LibGDX scene2d API for the next game we're producing.

For Candy Falls! I used a personal framework for managing moving/transitioning/interactive game objects (named PimentosoLib) which is basically a watered-down version of scene2d. For the next game I'm trying scene2d, which is more complex, complete and already available inside the LibGDX framework.

The game will be kinda heavy animation-wise, and scene2d does not provide an Actor that manages an animated sprite. I quickly created one extending the Widget class, it's very similar to the Image actor but manages an Animation instance instead of a Drawable instance.

Here is the code, if someone's interested.

NOTE: the following code will work VERY wacky if you are using LibGDX TexturePacker to pack the animation assets, using "stripWhitespace" or "rotation" parameters set to true. This is because direct TextureRegion drawing to a SpriteBatch does not rotate/pad the regions. To solve this, you can change the "region" global field from TextureRegion to a Sprite, and cast it inside the draw() method. I might fix this myself in the future, but I need to test the memory usage.

 import com.badlogic.gdx.graphics.Color;  
 import com.badlogic.gdx.graphics.g2d.Animation;  
 import com.badlogic.gdx.graphics.g2d.SpriteBatch;  
 import com.badlogic.gdx.graphics.g2d.TextureRegion;  
 import com.badlogic.gdx.math.Vector2;  
 import com.badlogic.gdx.scenes.scene2d.ui.Image;  
 import com.badlogic.gdx.scenes.scene2d.ui.Widget;  
 import com.badlogic.gdx.scenes.scene2d.utils.Align;  
 import com.badlogic.gdx.scenes.scene2d.utils.Drawable;  
 import com.badlogic.gdx.utils.Scaling;  
   
 /**  
  * Animated scene2d actor. Similar to {@link Image} but uses {@link Animation} instead of a {@link Drawable}.  
  * @author Pimentoso   
  */  
 public class AnimatedImage extends Widget {  
   
     private Scaling scaling;  
     private int align = Align.center;  
     private float imageX, imageY, imageWidth, imageHeight;  
       
     private Animation animation;  
     private TextureRegion region;  
     public int state;  
     public float stateTime;  
       
     public AnimatedImage() {  
         this((Animation) null);  
     }  
       
     public AnimatedImage(Animation animation) {  
         this(animation, Scaling.stretch, Align.center);  
     }  
       
     public AnimatedImage(Animation animation, Scaling scaling, int align) {  
         setAnimation(animation);  
         this.scaling = scaling;  
         this.align = align;  
         setWidth(getPrefWidth());  
         setHeight(getPrefHeight());  
     }  
       
     @Override  
     public void draw(SpriteBatch batch, float parentAlpha) {  
         validate();  
   
         Color color = getColor();  
         batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);  
   
         float x = getX();  
         float y = getY();  
         float scaleX = getScaleX();  
         float scaleY = getScaleY();  
   
         if (animation != null) {  
             region = animation.getKeyFrame(stateTime);  
             float rotation = getRotation();  
             if (scaleX == 1 && scaleY == 1 && rotation == 0)  
                 batch.draw(region, x + imageX, y + imageY, imageWidth, imageHeight);  
             else {  
                 batch.draw(region, x + imageX, y + imageY, getOriginX() - imageX, getOriginY() - imageY, imageWidth, imageHeight,  
                     scaleX, scaleY, rotation);  
             }  
         }  
     }  
       
     @Override  
     public void layout() {  
         float regionWidth, regionHeight;  
         if (animation != null) {  
             regionWidth = animation.getKeyFrame(0).getRegionWidth();  
             regionHeight = animation.getKeyFrame(0).getRegionHeight();  
         } else  
             return;  
   
         float width = getWidth();  
         float height = getHeight();  
   
         Vector2 size = scaling.apply(regionWidth, regionHeight, width, height);  
         imageWidth = size.x;  
         imageHeight = size.y;  
   
         if ((align & Align.left) != 0)  
             imageX = 0;  
         else if ((align & Align.right) != 0)  
             imageX = width-imageWidth;  
         else  
             imageX = (width/2)-(imageWidth/2);  
   
         if ((align & Align.top) != 0)  
             imageY = height-imageHeight;  
         else if ((align & Align.bottom) != 0)  
             imageY = 0;  
         else  
             imageY = (height/2)-(imageHeight/2);  
     }  
   
     @Override  
     public void act(float delta) {  
         super.act(delta);  
         stateTime += delta;  
     }  
       
     public void setState(int state) {  
         this.state = state;  
         stateTime = 0.0f;  
     }  
       
     public void setPlayMode (int playMode) {  
         animation.setPlayMode(playMode);  
     }  
   
     public Animation getAnimation() {  
         return animation;  
     }  
       
     public void setAnimation(Animation animation) {  
         if (animation != null) {  
             if (this.animation == animation) return;  
             invalidateHierarchy();  
         } else {  
             if (getPrefWidth() != 0 || getPrefHeight() != 0) invalidateHierarchy();  
         }  
         this.animation = animation;  
     }  
   
     public void setScaling (Scaling scaling) {  
         if (scaling == null) throw new IllegalArgumentException("scaling cannot be null.");  
         this.scaling = scaling;  
     }  
   
     public void setAlign (int align) {  
         this.align = align;  
     }  
   
     public float getMinWidth () {  
         return 0;  
     }  
   
     public float getMinHeight () {  
         return 0;  
     }  
   
     public float getPrefWidth () {  
         if (animation != null) return animation.getKeyFrame(0).getRegionWidth();  
         return 0;  
     }  
   
     public float getPrefHeight () {  
         if (animation != null) return animation.getKeyFrame(0).getRegionHeight();  
         return 0;  
     }  
   
     public float getImageX () {  
         return imageX;  
     }  
   
     public float getImageY () {  
         return imageY;  
     }  
   
     public float getImageWidth () {  
         return imageWidth;  
     }  
   
     public float getImageHeight () {  
         return imageHeight;  
     }  
 }