LibGDX Tutorial 9: Scene2D Part 4–UI Skins

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 ,




LibGDX Tutorial 3C: Scene management

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 , ,




Learning Scala, found a great book

10. December 2013

 

So lately I’ve been working on my LibGDX tutorial series and I am certainly a fan of the library.  Java on the other hand, after years of using C#, just seems flawed.  Not that it is a bad language, just that it’s a bit kludgy.  Fortunately there are a number of languages built over top of the Java Virtual Machine, allowing you to make use of most of the Java eco-system, while working in a different language.  Some of the more popular options are Groovy, a scripting language that targets the JVM and Clojure, a functional LISP like language.  I don’t personally want a fully dynamic language ( I like typed languages for large projects ) so Groovy is out, while LISP might as well be Klingon.  I like some functional programming, but my brain just doesn’t work that way… to warped by years of procedural programming.

 

Fortunately there is Scala.

 

I’ve only just started playing with it but I am already impressed.  It’s almost as if someone took all the aspects of Java I dont like and set out to fix them.  Things I like:

  • runs on the Java VM, so can use libraries like LibGDX without issue but still feels familiar
  • type inference.  Feel like a dynamic language while staying dynamically typed.  I miss var from C#!
  • Functional programming lite.  High Order functions.
  • It’s got REPL ( command line programmability ) even if it’s faked.  Great way to learn the language.
  • Everything is an object, one of Java’s biggest warts
  • Pattern matching… it’s like an uber switch statement and looks to be a huge time saver
  • traits and sealed… I think.  Basically a trait is an interface with codability, while sealed allows a class to define which classes can extend it.  It will take some use, but both seem to solve commonly encountered problems, but both may have huge downsides.
  • makes the language much more compact while still feeling like Java.
  • operator overloading.  This was simply a stupid Java mistake.
  • best conditional expression evaluation I have ever seen.  Optional semi colons.

 

I'm still just at the beginner phase, but I have to say I’ve already had a ton of AHAH moments.  There are a few annoyances, at least initially.  For example, I dont like the variable coming after the variable name… there might be a huge win here somewhere, but it feels very unnatural coming from Java. 

 

Anyway, back when I started looking to catch up on Java I looked for a book that taught Java but did so making certain assumptions about the programmers experiences.  For example, I know what a class is, how a loop works, etc…  Sadly I never found such a book.  This time however, for learning Scala, I did.

 

Scala for the Impatient

cover This book is exactly what I was looking for when I was looking for a Java book for experienced programmers.  In the authors own words:

I wrote this book for impatient readers who want to start programming in Scala right away. I assume you know Java, C#, or C++, and I don’t bore you with explaining variables, loops, or classes. I don’t exhaustively list all the features of the language, I don’t lecture you about the superiority of one paradigm over another, and I don’t make you suffer through long and contrived examples. Instead, you will get the information that you need in compact chunks that you can read and review as needed.

From what I have read this is exactly true.  I have no prior Scala experience, but I have never found myself once struggling with any concepts presented in this book.  Nor frankly have I been bored, something I often struggle with for programming books.

I need to make something extremely clear.  If you do not have a solid prior programming foundation in C# or Java ( or possibly C++ ), this is not the book for you!  The book basically covers how Scala deviates from other languages, so if you don't know the fundamentals, you will struggle.  It is also not a language reference.  If you are new to programming or want a language reference, Programming in Scala is probably the book you want.

One other thing I really appreciate about this book is the authors writing style.  It’s an easy read and he has a sense of humour.  Here for example is his tip on operating overloading:

In Java, you cannot overload operators, and the Java designers claimed this is a good thing because it stops you from inventing crazy operators like !@$&* that would make your program impossible to read. Of course, that’s silly; you can make your programs just as hard to read by using crazy method names like qxywz. Scala allows you to define operators, leaving it up to you to use this feature with restraint and good taste.

That paragraph is pretty typical of how the book goes.

So I’m heading off on this Scala adventure in my spare time.  Expect a few related posts here and there as I go.

Programming ,




LibGDX Tutorial 3B: Simple Animation

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 , ,




LibGDX Tutorial 9: Scene2D Part 2– Actions

9. December 2013

 

In the second part of the LibGDX Scene2D tutorial we will now look at Actions.  Actions are a convenient ( and completely optional! ) way to get your game’s Actors to “do stuff”.

 

Let’s jump straight in to an example:

package com.gamefromscratch;

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.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.MoveToAction;


public class SceneDemo3 implements ApplicationListener {
    
    public class MyActor extends Actor {
        Texture texture = new Texture(Gdx.files.internal("data/jet.png"));
        public boolean started = false;

        public MyActor(){
            setBounds(getX(),getY(),texture.getWidth(),texture.getHeight());
        }
        
        @Override
        public void draw(Batch batch, float alpha){
            batch.draw(texture,this.getX(),getY());
        }
    }
    private Stage stage;
    
    @Override
    public void create() {        
        stage = new Stage();
        Gdx.input.setInputProcessor(stage);
        
        MyActor myActor = new MyActor();
        
        MoveToAction moveAction = new MoveToAction();
        moveAction.setPosition(300f, 0f);
        moveAction.setDuration(10f);
        myActor.addAction(moveAction);
        
        stage.addActor(myActor);
    }

    @Override
    public void 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 example, it will slowly move the jet until it is at the position 300,0.

MoveToActionReduced

 

Much of this code is recycled from the previous Scene2D example, so we will only discuss the new portions, which reside in the create() function:

@Override
public void create() {        
    stage = new Stage();
    Gdx.input.setInputProcessor(stage);
    
    MyActor myActor = new MyActor();
    
    MoveToAction moveAction = new MoveToAction();
    moveAction.setPosition(300f, 0f);
    moveAction.setDuration(10f);
    myActor.addAction(moveAction);
    
    stage.addActor(myActor);
}

Here we create a MoveToAction, which moves the attached Actor to a given position over time.  You set the position to move to with the method setPosition() and the total duration of the action using setDuration().  You assign the action to the Actor using addAction().  All actions are derived from the Action class, while MoveToAction is derived from TemporalAction, which is an action that has a duration.  In addition to MoveToAction, there are a number of others such as MoveByAction, ScaleToAction, ColorAction, DelayAction, RepeatAction, RotateByAction, etc.

 

There is one important thing to be aware of here.  Back in part one of the Scene2D tutorial our custom Actor MyActor overrode the act method.  It is the act() method of Actor that updates all of the Actions connected to an Actor.  Therefore if you create your own Actor class, be sure to call it’s parent’s act() or call each attached Action manually like so:

@Override
public void act(float delta){
    for(Iterator<Action> iter = this.getActions().iterator(); iter.hasNext();){
        iter.next().act(delta);
    }
}

If you run more than one actions like so:

MoveToAction moveAction = new MoveToAction();
RotateToAction rotateAction = new RotateToAction();
ScaleToAction scaleAction = new ScaleToAction(); 

moveAction.setPosition(300f, 0f);
moveAction.setDuration(5f);
rotateAction.setRotation(90f);
rotateAction.setDuration(5f);
scaleAction.setScale(0.5f);
scaleAction.setDuration(5f);

myActor.addAction(moveAction);
myActor.addAction(rotateAction);
myActor.addAction(scaleAction);

stage.addActor(myActor);

 

You will see:

RotateScaleMoveActionReduced

As you can see, all of your actions run concurrently by default.

 

There is one important thing to be aware of.  The draw() method in MyActor we’ve been using until this point is not capable of displaying rotation or scaling by default.  If you wish to enabled scaled/rotated drawing like above, you need to make a minor adjustment to draw(), like so:

 

@Override
public void draw(Batch batch, float alpha){
    batch.draw(texture,this.getX(),getY(),this.getOriginX(),this.getOriginY(),this.getWidth(),
            this.getHeight(),this.getScaleX(), this.getScaleY(),this.getRotation(),0,0,
            texture.getWidth(),texture.getHeight(),false,false);
}

 

Obviously there are going to be many times when you want to delay an action, or run them in sequence.  Fortunately LibGdx supports exactly this.  Say you wanted to scale then rotate then move your Actor, you can accomplish this using a SequenceAction like so:

 

final MyActor myActor = new MyActor();

SequenceAction sequenceAction = new SequenceAction();

MoveToAction moveAction = new MoveToAction();
RotateToAction rotateAction = new RotateToAction();
ScaleToAction scaleAction = new ScaleToAction(); 

moveAction.setPosition(300f, 0f);
moveAction.setDuration(5f);
rotateAction.setRotation(90f);
rotateAction.setDuration(5f);
scaleAction.setScale(0.5f);
scaleAction.setDuration(5f);

sequenceAction.addAction(scaleAction);
sequenceAction.addAction(rotateAction);
sequenceAction.addAction(moveAction);


myActor.addAction(sequenceAction);

stage.addActor(myActor);

 

Simply add all the actors to the SequenceAction, then one will run, followed by the next then the next.

By statically importing Actions, like so:

import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;

You can also chain actions, so it’s possible to express the above action like:

myActor.addAction(sequence(scaleTo(0.5f,0.5f,5f),rotateTo(90.0f,5f),moveTo(300.0f,0f,5f)));
stage.addActor(myActor);

 

Or run in parallel, like so:

myActor.addAction(parallel(scaleTo(0.5f,0.5f,5f),rotateTo(90.0f,5f),moveTo(300.0f,0f,5f)));
stage.addActor(myActor);

There are several other actions, such as Int or FloatAction, that allow you to modify a value over time, DelayAction for running an action, um, after a delay, RepeatAction, for performing an Action over and over.  However, they all follow the same basic layout, so you should be able to figure them out easily at this point.  One last thing to point out, all Actions are poolable.  This means you can keep a pool of actions instead of allocating them over and over.  Here is one such example of a Pool of MoveToActions:

final MyActor myActor = new MyActor();
Pool<MoveToAction> actionPool = new Pool<MoveToAction>(){
    protected MoveToAction newObject(){
        return new MoveToAction();
    }
};

MoveToAction moveAction = actionPool.obtain();
moveAction.setDuration(5f);
moveAction.setPosition(300f, 0);

myActor.addAction(moveAction);

stage.addActor(myActor);

 

You can also have an action simply be code.  Consider the above example, let’s say you wanted to log when the MoveToAction was complete.  You could accomplish this by doing the following:

myActor.addAction(sequence(moveAction,
        run(new Runnable(){
            @Override
            public void run() {
                Gdx.app.log("STATUS", "Action complete");
            }
            
})));

 

A Runnable action simply runs the code specified in the run method when, um, run. That’s about it for Actions.  On their own they are pretty simply, but can easily be chained to create very complex interactions.

Programming , ,