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);  
   Label label = new Label("Speed: 50m/s", Assets.skin, "normal-text"); // equivalent  
   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:

Friday, March 1, 2013

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

Monday, November 12, 2012

Candy Falls! game guide

Now that the free version of Candy Falls! has access to all 3 game modes, it's time for a complete tutorial of the game.Any comment/question is welcome!


Time attack mode 
In this game mode, you have 60 seconds to get as many falling sweets as you can. Every sweet will give you score. The goal is to get the highest score.

Combos 
Falling cakes, highlighted with a pink glow, will give you a 3 second combo time if you catch them. A red bar on top of the screen will be displayed indicating this. If you get another cake during combo time, the combo multiplier will be incremented (going from 1x, 2x, until 10x). Combo multiplier doubles the score you get from every sweet you catch, and if you get to 10x combo, you will get a STOP TIME effect for 8 seconds. During this effect, the game timer will be stopped, and the 10x combo bonus will not expire. So you get extra play time, AND sweets give you ten times the normal score.

Special effects 
Falling macarons, highlighted with a blue glow, will trigger a random special effect for 8 seconds. Special effects can be positive or negative (negative effects actually appear only 25% of the times).

Hints 
For a high score, try to get as many time stops as you can. Focus on getting only cakes, and when you get to combo max, try to get as many sweets as you can until time stop expires. Then go back to getting cakes.

Endurance mode 
In this game mode, sweets fall in preordered sequences, and you have to catch them all. The game gradually grows faster, and you will need good reflexes when you get to 3-4 minutes of playing. You start with 10 lives, and you will lose one every time you miss a sweet. If you get 50 sweets in a row (or multiples of 50) you get an extra life. There's also a chance you get a 8 seconds LARGE BASKET effect.

Hints 
Try to stay focused and hold the phone with both hands for more sensibility. This game mode is not reccomended while moving (walking, car, train, etc)

Sequence mode 
In this game mode, a sequence of 4 sweets is displayed on the lower right corner. You need to get the current highlighted sweet, while avoiding the others. When you get all 4 sweets, you will complete the sequence and get the score. Then a new sequence will be generated. You start the game with 10 lives, and you will lose a life everytime you get a wrong sweet. When you lose a life, the current sequence will be reset and you will have to start it again.

Hints
For an high score, try to complete sequences as fast as possible. A yellow bar is displayed on top when you start a sequence; if you complete it before the yellow bar disappears (less than 12 seconds) you will get a GREAT bonus, which is 500 points. If you complete it in less than 18 seconds, you will get a NICE bonus, which is 250 points.

Powerups
Before any game, you will be given the option to buy a powerup. It will be active during the next game. There are 3 types of powerup.

Coins powerup
During the next game, coins will fall among the other sweets. They will be highlighted by a yellow glow. Every coin you catch will reward you 10 coins at the end of the game.

Initial powerup
The game will be longer. If you buy this powerup, you will get more play time in time attack mode, and more lives in endurance/sequence mode. Also, this powerup can be upgraded in the "powerup upgrades" shop. Upgrades will be notified by the number of stars.

Passive powerup
There will be a bonus effect during the game. The effect is different in each of the three game modes.
  • Time attack mode: falling sweets that would bounce on the edges of your basket, will now have a 50% chance of being caught instead.
  • Endurance mode: everytime you lose a life, you will be "invulnerable" for 3 seconds. No more lives will be lost during this 3 seconds.
  • Sequence mode: everytime you catch a wrong sweet, a 1 second "safe zone" effect will be started. If you catch the right sweet during the "safe zone", you will not lose a life. If the "safe zone" ends, you will lose a life as usual.

Item shop 
When you complete a game in each mode, you will be rewarded some coins. In this item shop you can spend those coins.

Costume shop
You can buy different costumes for your character. Each costume comes with a personalized sweet, that will replace cakes in time attack mode, and will be randomly dropped in endurance and sequence modes.

Basket shop
You can buy different baskets to play with.

Powerup upgrades
Here you can pay coins to upgrade the initial powerup for the three game modes. You can buy 5 stars of each powerup, which will grant you more time in time attack mode (up to 90 seconds) and more lives in sequence/endurance mode (up to 16 lives).

High scores
The game uses the Scoreloop platform to save online scores. The first time you play the game, a popup will appear, asking if you want to enable Scoreloop capabilities. It is highly reccomended that you enable it. If you deny it, the online score system will not be activated.