Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon
14. January 2014

 

EDIT: You can download the precompiled binaries here.  The Mac binaries are in fbx-conv.zip.  The reason for the failed build is LibGDX used a previous build of Autodesk’s library.

 

If you want to convert 3D models to LibGDX’s native file format G3DB you need to use fbx-conv.  The general instructions for the entire process is available here.  If you are working on Windows, you are in luck, as there are pre-compiled binaries.  If you are however working on MacOS, like I currently am, there is a bit more of an adventure ahead.

 

Simply put, you need to compile from scratch and that requires a few hoops to be jumped through, especially if you are used to working only in Java, as the source is actually C++.  I will run through the process I went through.

 

First and foremost, you need to install Xcode if you haven’t already.  You can get Xcode in the App Store, or here.

Make sure command line tools are installed.  Simply open a terminal and type “gcc”.  If a prompt appears asking you to install the tools, do it.

NewImage

If it doesn’t, yay, you have them installed. 

Run gcc from the terminal one more time after install is completed and agree to the EULA.

 

Ok, Xcode is configured.  Now you need to set up the Autodesk FBX sdk.  From this link locate and download FBX SDK 2014.2.1 GCC.  Once downloaded, in Finder double click to extract, then double click the extracted file to install.

By default it will install to Application/Autodesk/FBX SDK/2014.2.1.  Obviously if you do this at a later date, the version number will be different.  You need to add FBX_SDK_ROOT to your environment.  To do so open a terminal window and cd to your home ( ~ ) directory.  Type:

pico .profile

Add the line:

export FBX_SDK_ROOT='/Applications/Autodesk/FBX SDK/2014.2.1'

 

to the end of the script.  Then hit CTRL+X to exit, and chose Y when prompted.  Now you need to create a new terminal window for the updated settings to be valid.

 

Next you need to install Premake.  Simply download the file from the link, extract it and copy the binary ‘pre make’ somewhere it can be seen ( I in a fit of poor judgement simply used /usr/bin ).

 

OK, ready to go.  First we need to download the fbx-conv code.  Open a terminal and cd to a directory you want to build in.

type:

git clone https://github.com/libgdx/fbx-conv.git

./generate_xcode

 

This will create an Xcode project in the directory build/xcode3.  cd into that directory then type:

xcodebuild

If all goes well, you will now have fbx-conv in the root directory you cloned the git to.  fox-conv depends on the dylib file libfbxsdk.dylib being in the path.  I simply copied it from Applications/Autodesk/FBX SDK/2014.2.1/lib/gcc4/ub/release/ to the same folder as fbx-conv.  That said, I really don’t understand dylib pathing in OSX since they changed it in OSX 10.8 so there is probably a much better solution.

 

 

There is one gotcha I ran into.  The code actually failed to compile on this line:

animStack->GetScene()->GetEvaluator()->SetContext(animStack);

In FbxConverter.h. It seems GetEvaluator() is no longer a member of  FBXScene.  Perhaps fbx-conv was built on a previous release?  Anyways, I simply commented the line out and fbx-conv seems to still work.  YMMV. 

Programming


10. January 2014

As I mentioned yesterday when talking about how to generate character sprites supporting dynamic inventory, the very first step would be to actually figure out how to do 3D in LibGDX.

 

Well, I did, and thankfully it’s pretty simple.  This isn’t a tutorial by any stretch of the means, but what follows is basically the “minimum” 3D application you can create in LibGDX.  It simple creates a blue cube and rotates the camera around it.  While it isn’t a tutorial, it is heavily documented, so you should be able to figure things out with ease, especially if you’ve already done my LibGDX tutorial series.

 

package com.gamefromscratch;

 

import com.badlogic.gdx.ApplicationListener;

import com.badlogic.gdx.Gdx;

import com.badlogic.gdx.graphics.Color;

import com.badlogic.gdx.graphics.GL10;

import com.badlogic.gdx.graphics.PerspectiveCamera;

import com.badlogic.gdx.graphics.VertexAttributes.Usage;

import com.badlogic.gdx.graphics.g3d.Environment;

import com.badlogic.gdx.graphics.g3d.Material;

import com.badlogic.gdx.graphics.g3d.Model;

import com.badlogic.gdx.graphics.g3d.ModelBatch;

import com.badlogic.gdx.graphics.g3d.ModelInstance;

import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;

import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;

import com.badlogic.gdx.math.Vector3;

 

public class TestApp implements ApplicationListener {

   private PerspectiveCamera camera;

   private ModelBatch modelBatch;

   private Model box;

   private ModelInstance boxInstance;

   private Environment environment;

 

   @Override

   public void create() {

   // Create camera sized to screens width/height with Field of View of 75 degrees

      camera = new PerspectiveCamera(

      75,

      Gdx.graphics.getWidth(),

      Gdx.graphics.getHeight());

 

      // Move the camera 3 units back along the z-axis and look at the origin

      camera.position.set(0f,0f,3f);

      camera.lookAt(0f,0f,0f);

 

      // Near and Far (plane) repesent the minimum and maximum ranges of the camera in, um, units

      camera.near = 0.1f; 

      camera.far = 300.0f;

 

      // A ModelBatch is like a SpriteBatch, just for models.  Use it to batch up geometry for OpenGL

      modelBatch = new ModelBatch();

 

      // A ModelBuilder can be used to build meshes by hand

      ModelBuilder modelBuilder = new ModelBuilder();

 

      // It also has the handy ability to make certain premade shapes, like a Cube

      // We pass in a ColorAttribute, making our cubes diffuse ( aka, color ) red.

      // And let openGL know we are interested in the Position and Normal channels

      box = modelBuilder.createBox(2f, 2f, 2f, 

      new Material(ColorAttribute.createDiffuse(Color.BLUE)),

      Usage.Position | Usage.Normal

      );

 

      // A model holds all of the information about an, um, model, such as vertex data and texture info

      // However, you need an instance to actually render it.  The instance contains all the 

      // positioning information ( and more ).  Remember Model==heavy ModelInstance==Light

      boxInstance = new ModelInstance(box,0,0,0);

 

      // Finally we want some light, or we wont see our color.  The environment gets passed in during

      // the rendering process.  Create one, then create an Ambient ( non-positioned, non-directional ) light.

      environment = new Environment();

      environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.8f, 0.8f, 0.8f, 1.0f));

   }

 

   @Override

   public void dispose() {

      modelBatch.dispose();

      box.dispose();

   }

 

   @Override

   public void render() {

      // You've seen all this before, just be sure to clear the GL_DEPTH_BUFFER_BIT when working in 3D

      Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());

      Gdx.gl.glClearColor(1, 1, 1, 1);

      Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

 

      // For some flavor, lets spin our camera around the Y axis by 1 degree each time render is called

      camera.rotateAround(Vector3.Zero, new Vector3(0,1,0),1f);

      // When you change the camera details, you need to call update();

      // Also note, you need to call update() at least once.

      camera.update();

 

      // Like spriteBatch, just with models!  pass in the box Instance and the environment

      modelBatch.begin(camera);

      modelBatch.render(boxInstance, environment);

      modelBatch.end();

   }

 

   @Override

   public void resize(int width, int height) {

   }

 

   @Override

   public void pause() {

   }

 

   @Override

   public void resume() {

   }

}

 

And when you run it you should see:

RotatingCube

 

Next up, loading a 3D model from Blender and playing the animations.

 

By the way, although this isn’t a tutorial, if you have a question about something I did feel free to use the comments and I will do my best to answer!

Programming


18. December 2013

 

One of the nicest features of Scene2D is the UI layer built over top of it.  Scene2D.ui provides a series of widgets that make creating a UI an breeze, something often lacking in game development libraries.  That said, there is one very confusing stumbling block before you can get to work with UI… Skins.

 

A skin is a collection of files that go together to make up your user interface.

First is a JSON file ( JavaScript Object Notation ), which is a popular JavaScript based data storage format, like XML light.  In the JSON file you describe the various properties of your skin such as how your widgets should look. 

Next is a texture atlas.  We looked at using TexturePacker to make a texture atlas back in the graphics tutorial.  The texture atlas describes the layout of all the images that make up your UI.  This is a .atlas extension file.

Along with the texture atlas, you’ve also got the actual image that TexturePacker generated.

Finally you have the fnt and the associated image.  We covered fonts back in the Hello World tutorial.

 

This can all seem pretty daunting, especially starting from scratch.  Fortunately there is a skin included in the LibGDX tests.  The files you are interested in are uiskin.atlas, uiskin.json, uiskin.png, default.png and default.fnt.  Simply download and place each of these files in the Android assets/data folder of your project.  If you are downloading from the Github link I provided, be sure to download the RAW version of each file!

 

Let’s take a quick look at some of the above files for a better idea of what goes into them.  I will ignore the font files, we already covered that.  The atlas and png file we’ve already covered as well, but here is uiskin.png, so you’ve got some idea what is included:

uiskin

 

These are the various graphics that go in to creating the UI.  Let’s take a look at uiskin.json:

 

 

{
com.badlogic.gdx.graphics.g2d.BitmapFont: { default-font: { file: default.fnt } },
com.badlogic.gdx.graphics.Color: {
    green: { a: 1, b: 0, g: 1, r: 0 },
    white: { a: 1, b: 1, g: 1, r: 1 },
    red: { a: 1, b: 0, g: 0, r: 1 },
    black: { a: 1, b: 0, g: 0, r: 0 }
},
com.badlogic.gdx.scenes.scene2d.ui.Skin$TintedDrawable: {
    dialogDim: { name: white, color: { r: 0, g: 0, b: 0, a: 0.45 } }
},
com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle: {
    default: { down: default-round-down, up: default-round },
    toggle: { down: default-round-down, checked: default-round-down, up: default-round }
},
com.badlogic.gdx.scenes.scene2d.ui.TextButton$TextButtonStyle: {
    default: { down: default-round-down, up: default-round, font: default-font, fontColor: white },
    toggle: { down: default-round-down, up: default-round, checked: default-round-down, font: default-font, fontColor: white, 
    downFontColor: red }
},
com.badlogic.gdx.scenes.scene2d.ui.ScrollPane$ScrollPaneStyle: {
    default: { vScroll: default-scroll, hScrollKnob: default-round-large, background: default-rect, hScroll: default-scroll, 
    vScrollKnob: default-round-large }
},
com.badlogic.gdx.scenes.scene2d.ui.SelectBox$SelectBoxStyle: {
    default: {
        font: default-font, fontColor: white, background: default-select,
        scrollStyle: default,
        listStyle: { font: default-font, selection: default-select-selection }
    }
},
com.badlogic.gdx.scenes.scene2d.ui.SplitPane$SplitPaneStyle: {
    default-vertical: { handle: default-splitpane-vertical },
    default-horizontal: { handle: default-splitpane }
},
com.badlogic.gdx.scenes.scene2d.ui.Window$WindowStyle: {
    default: { titleFont: default-font, background: default-window, titleFontColor: white },
    dialog: { titleFont: default-font, background: default-window, titleFontColor: white, stageBackground: dialogDim }
},
com.badlogic.gdx.scenes.scene2d.ui.Slider$SliderStyle: {
    default-horizontal: { background: default-slider, knob: default-slider-knob }
},
com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: {
    default: { font: default-font, fontColor: white }
},
com.badlogic.gdx.scenes.scene2d.ui.TextField$TextFieldStyle: {
    default: { selection: selection, background: textfield, font: default-font, fontColor: white, cursor: cursor }
},
com.badlogic.gdx.scenes.scene2d.ui.CheckBox$CheckBoxStyle: {
    default: { checkboxOn: check-on, checkboxOff: check-off, font: default-font, fontColor: white }
},
com.badlogic.gdx.scenes.scene2d.ui.List$ListStyle: {
    default: { fontColorUnselected: white, selection: default-rect-pad, fontColorSelected: white, font: default-font }
},
com.badlogic.gdx.scenes.scene2d.ui.Touchpad$TouchpadStyle: {
    default: { background: default-pane, knob: default-round-large }
},
com.badlogic.gdx.scenes.scene2d.ui.Tree$TreeStyle: {
    default: { minus: tree-minus, plus: tree-plus, selection: default-select-selection }
}
}

 

If you’ve ever used CSS, this should look immediately familiar.  You are essentially telling LibGDX how to style each class by using the fully qualified Java class name.  Let’s take a look at a particular example we are going to be using shortly, TextButton.

 

com.badlogic.gdx.scenes.scene2d.ui.TextButton$TextButtonStyle: {
    default: { down: default-round-down, up: default-round, font: default-font, fontColor: white },
    toggle: { down: default-round-down, up: default-round, checked: default-round-down, font: default-font, 
fontColor: white, 
    downFontColor: red }
}

 

You are setting the values for the class TextButtonStyle or on of the classes it is derived from.  You may notice for fontColor, “white” is passed.  If you look up in the skin, you can see how its defined:

com.badlogic.gdx.graphics.Color: {
    green: { a: 1, b: 0, g: 1, r: 0 },
    white: { a: 1, b: 1, g: 1, r: 1 },
    red: { a: 1, b: 0, g: 0, r: 1 },
    black: { a: 1, b: 0, g: 0, r: 0 }
}

 

You can see “white” is a Color with the values 1/1/1 for rgb and 1 for alpha… in other words, white Smile

The other key thing is the value for down.  As you can see from the TextButtonStyle, down is a Drawable.  The value specified, default-round-down is an entry in the atlas file:

default-round-down
  rotate: false
  xy: 99, 29
  size: 12, 20
  split: 5, 5, 5, 4
  orig: 12, 20
  offset: 0, 0
  index: -1

This is specifying the size and location within the uiskin.png file.  More details about skins can be found on the wiki.

 

Now that you have your skin files, let’s take a look at a sample using Scene2d.ui:

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;

public class UIDemo implements ApplicationListener {
    private SpriteBatch batch;
    private Skin skin;
    private Stage stage;

    @Override
    public void create() {        
        batch = new SpriteBatch();
        skin = new Skin(Gdx.files.internal("data/uiskin.json"));
        stage = new Stage();

        final TextButton button = new TextButton("Click me", skin, "default");
        
        button.setWidth(200f);
        button.setHeight(20f);
        button.setPosition(Gdx.graphics.getWidth() /2 - 100f, Gdx.graphics.getHeight()/2 - 10f);
        
        button.addListener(new ClickListener(){
            @Override 
            public void clicked(InputEvent event, float x, float y){
                button.setText("You clicked the button");
            }
        });
        
        stage.addActor(button);
        
        Gdx.input.setInputProcessor(stage);
    }

    @Override
    public void dispose() {
        batch.dispose();
    }

    @Override
    public void render() {        
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        
        batch.begin();
        stage.draw();
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

 

You run this app and you should see:

image

 

Most of this is pretty familiar at this point.  We create a Skin object and load it using the typical file IO calls.  The file you want to load is the json config, uiskin.json.  This file points to all all of the other files, they will be loaded automatically by the Skin.  Next we create a TextButton, the values passed in to the constructor are the text to display on the button, the skin and the value in the skin to use.  If you look at the entry for the TextButton in the skin JSON file see:

com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle: {
    default: { down: default-round-down, up: default-round },
    toggle: { down: default-round-down, checked: default-round-down, up: default-round }
},

You are saying you want to use the “default” entry.  Coincidentally, if you dont specify anything, default will be used.  We position and size the button and set up a click listener that simply changes the displayed text when the button is clicked.  Now the cool part… UI widgets like TextButton are simply Actors, so they work just like other Scene2D actors, add it to the stage and it’s ready to go.

 

Scene2D ui is a fairly involved topic so I will follow up with a more advanced example next tutorial.

Programming


11. December 2013

 

So far we’ve look at what Scene2D provides in terms of Actors, Actions as well as handling input, now we will look at some of the scene management functionality it provides.  One of the very powerful capabilities of Scene2D is grouping.  Let’s jump right in with an example.  In this example I used these two graphics:

jet(look, there is a space here!)flame

 

By the way, those are two different images.  One is a jet and the second is the engine exhaust.  Let,s take a look at grouping them together in a scene as a single transformable entity, a pretty common game development task.

 

 

package com.me.mygdxgame;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.Stage;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;


public class SceneManagementDemo implements ApplicationListener {
    
    private Stage stage;
    private Group group;
    
    @Override
    public void create() {        
        stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true);
        final TextureRegion jetTexture = new TextureRegion(new Texture("data/jet.png"));
        final TextureRegion flameTexture = new TextureRegion(new Texture("data/flame.png"));
        
        final Actor jet = new Actor(){
            public void draw(Batch batch, float alpha){
                batch.draw(jetTexture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), 
                        getScaleX(), getScaleY(), getRotation());
            }
        };
        jet.setBounds(jet.getX(), jet.getY(), jetTexture.getRegionWidth(), jetTexture.getRegionHeight());
    
        final Actor flame = new Actor(){
            public void draw(Batch batch, float alpha){
                batch.draw(flameTexture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(),
                        getScaleX(), getScaleY(), getRotation());
            }
        };
        flame.setBounds(0, 0, flameTexture.getRegionWidth(), flameTexture.getRegionHeight());
        flame.setPosition(jet.getWidth()-25, 25);
        
        group = new Group();
        group.addActor(jet);
        group.addActor(flame);
        
        group.addAction(parallel(moveTo(200,0,5),rotateBy(90,5)));
        
        stage.addActor(group);
        
        
    }

    @Override
    public void dispose() {
        stage.dispose();
    }

    @Override
    public void render() {    
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        stage.act(Gdx.graphics.getDeltaTime());
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

 

When you run it, you will see:

SceneManagementCompressed

 

As you can see, once grouped, the attached actors inherit any transformations applied to the group.  The code in this example should be pretty straight forward at this point, not much new here.  First we load our two images as TextureRegions.  We then create an actor for each, in both cases setting it’s boundaries with setBounds() or it wont render correctly.  For each Actor we implement the full batch.draw() function to make sure rotation and scaling are properly rendered.  Finally for the flame texture, we set it’s position relative to the jet Actor.

 

Then we create a new Group object, this is the secret sauce behind Scene2D grouping.  Then instead of adding our two Actors to the Scene, we instead add them to the Group, which is then added to the Scene.  So that we can actually see something happening in this example, we apply a moveTo and rotateBy Action to our group.  We covered Actions in the last tutorial post if you want more details.  One important thing I didn’t show here is, it is possible to translate the individual Actors within the Group.

 

One other aspect of Scene2D is determining if a hit occurred.  Back in Scene2D Tutorial Part 1 we saw the touchDown function, which is called when an Actor within a Scene is touched.  Now we will briefly look at the logic driving this process.  The Stage’s hit() method is called, which in turn calls the hit() method of every Actor within the stage.  hit() passes in un-translated coordinates, making the process easier on the developer.  The default hit() method simply checks the bounding box of the Actor…  the following example instead checks the bounding circle… in case you had a say… circular object! Smile

 

package com.me.mygdxgame;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Touchable;

import java.util.Random;

public class SceneManagementDemo implements ApplicationListener {
    
    // Create an Actor "Jet" that displays the TextureRegion passed in
    class Jet extends Actor {
        private TextureRegion _texture;
        
        public Jet(TextureRegion texture){
            _texture = texture;
            setBounds(getX(),getY(),_texture.getRegionWidth(), _texture.getRegionHeight());
            
            this.addListener(new InputListener(){
                public boolean touchDown(InputEvent event, float x, float y, int pointer, int buttons){
                    System.out.println("Touched" + getName());
                    setVisible(false);
                    return true;
                }
            });
        }

        // Implement the full form of draw() so we can handle rotation and scaling.
        public void draw(Batch batch, float alpha){
            batch.draw(_texture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(),
                    getScaleX(), getScaleY(), getRotation());
        }
        
        // This hit() instead of checking against a bounding box, checks a bounding circle.
        public Actor hit(float x, float y, boolean touchable){
            // If this Actor is hidden or untouchable, it cant be hit
            if(!this.isVisible() || this.getTouchable() == Touchable.disabled)
                return null;
            
            // Get centerpoint of bounding circle, also known as the center of the rect
            float centerX = getWidth()/2;
            float centerY = getHeight()/2;
            
            // Square roots are bad m'kay. In "real" code, simply square both sides for much speedy fastness
            // This however is the proper, unoptimized and easiest to grok equation for a hit within a circle
            // You could of course use LibGDX's Circle class instead.
            
            // Calculate radius of circle
            float radius = (float) Math.sqrt(centerX * centerX +
                    centerY * centerY);

            // And distance of point from the center of the circle
            float distance = (float) Math.sqrt(((centerX - x) * (centerX - x)) 
                    + ((centerY - y) * (centerY - y)));
            
            // If the distance is less than the circle radius, it's a hit
            if(distance <= radius) return this;
            
            // Otherwise, it isnt
            return null;
        }
    }
    
    private Jet[] jets;
    private Stage stage;
    
    @Override
    public void create() {        
        stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true);
        final TextureRegion jetTexture = new TextureRegion(new Texture("data/jet.png"));
        
        jets = new Jet[10];
        
        // Create/seed our random number for positioning jets randomly
        Random random = new Random();
        
        // Create 10 Jet objects at random on screen locations
        for(int i = 0; i < 10; i++){
            jets[i] = new Jet(jetTexture);
            
            //Assign the position of the jet to a random value within the screen boundaries
            jets[i].setPosition(random.nextInt(Gdx.graphics.getWidth() - (int)jets[i].getWidth())
                    , random.nextInt(Gdx.graphics.getHeight() - (int)jets[i].getHeight()));
            
            // Set the name of the Jet to it's index within the loop
            jets[i].setName(Integer.toString(i));
            
            // Add them to the stage
            stage.addActor(jets[i]);
        }
        
        Gdx.input.setInputProcessor(stage);
    }

    @Override
    public void dispose() {
        stage.dispose();
    }

    @Override
    public void render() {    
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        stage.act(Gdx.graphics.getDeltaTime());
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

 

 

When you run this app, 10 randomly located jet images will be drawn on screen. 

 

SceneManagement2Compressed

 

As you click each, it will disappear.  One important thing to realize is hit() is evaluated in reverse order objects are created.  So, the last object you add to the stage will be the first one hit() if more than one object occupy the same space.  The determining function whether a Jet has been touched or not is hit().  hit() works by creating a bounding circle to fit the image within, then checking to see if the mouse pointer is within the bounds of that circle.  When a hit occurs, we return the object hit, otherwise we return null.

 

The nice thing about this system is the user doesn’t have to worry about any translations applied to it’s parent or other translations that have occurred.  It’s also important to realize this is a pretty derived example.  If you removed the overriden hit() method, the default implementation would actually work better.  You do NOT need to provide a hit() method in your derived Actor classes unless the default doesn’t fit your needs.  This example is merely to show how the Scene2D hit detection works, and how to implement a custom detector.  Should you wish to say, implement pixel-perfect detection, you could do it this way.  I commented this example a bit more than I regularly do, to explain the bits I’ve glossed over.

Programming


9. December 2013

 

Back in Tutorial 3 I created a simple animation using a Timer and setting the frame manually from a TextureAtlas.  This is not the ideal way to perform animation using LibGDX and was done to illustrate how to use a TextureAtlas, not how to perform animation.  Instead the better way to perform animations is using the Animation class.  Here is an example using the same spritesheet from tutorial 3, remember you need to add it to the Android project assets folder.

 

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;

public class AnimationDemo implements ApplicationListener {
    private SpriteBatch batch;
    private TextureAtlas textureAtlas;
    private Animation animation;
    private float elapsedTime = 0;
    
    @Override
    public void create() {        
        batch = new SpriteBatch();
        textureAtlas = new TextureAtlas(Gdx.files.internal("data/spritesheet.atlas"));
        animation = new Animation(1/15f, textureAtlas.getRegions());
    }

    @Override
    public void dispose() {
        batch.dispose();
        textureAtlas.dispose();
    }

    @Override
    public void render() {        
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        
        batch.begin();
        //sprite.draw(batch);
        elapsedTime += Gdx.graphics.getDeltaTime();
        batch.draw(animation.getKeyFrame(elapsedTime, true), 0, 0);
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

When you run it, you should see:

Animation

 

The key here is the Animation, created here:

animation = new Animation(1/15f, textureAtlas.getRegions());

When creating an animation you pass in the amount of time per frame ( 15 frames per second in this case ) and an array of TextureRegions.  These represent the individual frames of animation within the TextureAtlas.  In this particular example we are simply using all of the frames available within the Atlas as a single animation.  The next key change is:

elapsedTime += Gdx.graphics.getDeltaTime();

batch.draw(animation.getKeyFrame(elapsedTime, true), 0, 0);

Here we are simply drawing the current frame from the animation to the screen.  We pass in the amount of time that has elapsed so the animation knows where it is in the sequence.  The true parameter is telling it to loop the animation.

 

Of course, you can have multiple animations from a single texture Atlas.  Consider the spritesheet we are currently working with.  If you take a look at the .atlas file, you will see each individual frame is named:

image

 

Looking at the spritesheet, you will see the animations are organized like such:

spritesheet

 

What we want to do is treat this as two sepeate animations.  Frames 1 through 10 represent an upward roll, while 11-20 are a downward roll.  Let’s take a look at how we do that:

public void create() {        
    batch = new SpriteBatch();
    textureAtlas = new TextureAtlas(Gdx.files.internal("data/spritesheet.atlas"));
    
    TextureRegion[] rotateUpFrames = new TextureRegion[10];
    
    // Rotate Up Animation
    // Create an array of TextureRegions
    rotateUpFrames[0] = (textureAtlas.findRegion("0001"));
    rotateUpFrames[1] = (textureAtlas.findRegion("0002"));
    rotateUpFrames[2] = (textureAtlas.findRegion("0003"));
    rotateUpFrames[3] = (textureAtlas.findRegion("0004"));
    rotateUpFrames[4] = (textureAtlas.findRegion("0005"));
    rotateUpFrames[5] = (textureAtlas.findRegion("0006"));
    rotateUpFrames[6] = (textureAtlas.findRegion("0007"));
    rotateUpFrames[7] = (textureAtlas.findRegion("0008"));
    rotateUpFrames[8] = (textureAtlas.findRegion("0009"));
    rotateUpFrames[9] = (textureAtlas.findRegion("0010"));

    rotateUpAnimation = new Animation(0.1f,rotateUpFrames);
    
    // Rotate Down Animation
    // Or you can just pass in all of the regions to the Animation constructor
    rotateDownAnimation = new Animation(0.1f,
            (textureAtlas.findRegion("0011")),
            (textureAtlas.findRegion("0012")),
            (textureAtlas.findRegion("0013")),
            (textureAtlas.findRegion("0014")),
            (textureAtlas.findRegion("0015")),
            (textureAtlas.findRegion("0016")),
            (textureAtlas.findRegion("0017")),
            (textureAtlas.findRegion("0018")),
            (textureAtlas.findRegion("0019")),
            (textureAtlas.findRegion("0020")));

    Gdx.input.setInputProcessor(this);
}

 

As you can see, it’s easy to create multiple animations from a single TextureAtlas.  Keep in mind, when creating a TextureAtlas using TexturePacker, it was the filename that you passed in that created the region names.  So what you would generally do is name your separate animations accordingly, such as WalkLeft, WalkRight, etc. 

Programming


GFS On YouTube

See More Tutorials on DevGa.me!

Month List