2012-06-25

#12 - libgdx Tutorial: 2D Animations

Libgdx version used on this post: 0.9.4 (download)
Like in many other gaming frameworks, a 2D animation is just a sequence of static images (frames) shown with a certain speed, creating an illusion of movement. On this post we'll use an animation to make the ship tilt to the side it's moving.

Important changes made to the existing code:

  • Enabled the use of OpenGL ES 2.0 in the both launchers: TyrianAndroidLauncher and TyrianDesktopLauncher. Libgdx will fallback to OpenGL ES 1.x in case the 2.0 isn't available. This can bring your game some valuable performance enhancements as stated here (section: Choosing an OpenGL API Version).
  • The Ship2D class was modified as follows:
    • We'll be using a simple MAX_SPEED constant (given in pixels/second), instead of the previous MAX_HORIZONTAL_SPEED and MAX_VERTICAL_SPEED.
    • The moveShip method was simplified regarding its interpretation of the accelerometer values. I also created a VectorUtils class to remove many ifs from this method, making it easier to read.
    • A deceleration was added to make the ship stop flying when there is no user input. I won't go into details on this because on the previous post I wrote about acceleration, so you should be able to easily understand this modification. If you have any questions, just post it as a comment.

Using animations

In libgdx we use the com.badlogic.gdx.graphics.g2d.Animation class to perform 2D animations. This class holds the animation's frames (in the form of TextureRegion instances) and sets the time each frame should be shown. This utility class performs no drawing at all. After it's created, we'll make use of the following method:
public TextureRegion getKeyFrame(float stateTime, boolean looping)
This method retrieves the next frame to be drawn. Let's understand each parameter:
  • float stateTime: this is an accumulator value that represents the time elapsed since the start of the animation. We should store this value somewhere and add the delta time to it before calling the method.
  • boolean looping: whether the animation loops or stops at the last frame.
In order to create an Animation instance we must supply the TextureRegion instances and the frame duration in seconds. As we're using the Texture Packer to create our image atlases, we can use our TextureAtlas instance to easily find all the frames the compose an animation just by calling textureAtlas.findRegions( "animation-name" ), which returns a list of AtlasRegion instances, which extend TextureRegion.
Note: we talked about the Texture Packer and image atlases in a previous post.
You can test and tune the frame duration value or come up with some formula, like: 2.5 / frameCount
That means the whole animation lasts 2.5 seconds. Here you can read more about animations in libgdx if you want.

Scene 2D and animations

Scene 2D has no animation actor, but that's not a problem because we can just change our image actor's region based on the TextureRegion returned by the Animation class. Simply put, we can do something like this:
animationStateTime += delta;
TextureRegion frame = animation.getKeyFrame( animationStateTime, false );
setRegion( frame );

Creating the tilt animation

The Texture Packer created the following image atlas for us:


Notice that we just have the tilt animation to the left. Later we'll use a trick to inverse the image horizontaly, so the ship will be able to tilt to both sides. Let's define the new animation related attributes:
    /**
     * The ship's tilt animation.
     */
    private final Animation tiltAnimation;

    /**
     * The ship's tilt animation's state time.
     */
    private float tiltAnimationStateTime;
In the constructor we create the tilt animation:
this.tiltAnimation = new Animation( 0.15f, tiltAnimationFrames );
But were these frames come from? In our factory method (Ship2D#create) we receive a TextureAtlas instance. So inside the factory method we can just call:
 List<AtlasRegion> regions = textureAtlas.findRegions( ship.getShipModel().getSimpleName() );
Ship2D ship2d = new Ship2D( ship, regions );
There is a small problem with this approach. Both the level screen and the start game screen have images for the ship, and as we use a convention to name our images, the retrieved regions contain the ship's image of the start game screen. This image is not a part of the tilt animation. We could just rename the images in order to avoid this conflict (and this is the best fix in my opinion), but there is a trick we can do. The Texture Packer stores the image's index when it's part of an animation. If the index is less than zero it's just an static image. The following code removes the undesired ship's image:
 List<AtlasRegion> regions = textureAtlas.findRegions( ship.getShipModel().getSimpleName() );
 Iterator<AtlasRegion> regionIterator = regions.iterator();
 while( regionIterator.hasNext() ) {
  if( regionIterator.next().index < 0 ) {
   regionIterator.remove();
  }
 }

Tilting the ship based on user input

We're almost done. All we have to do now is analyse the user input and tilt the ship. We're better off creating a specific method for that, so now our Ship2D#act method now looks like this:
    @Override
    public void act( float delta )
    {
        super.act( delta );
        moveShip( delta );
        tiltShip( delta );
    }
And now let's add the tiltShip method. Please analyse the code below and read the inline comments:
    /**
     * Tilts the ship to the direction its moving.
     */
    private void tiltShip(
        float delta )
    {
        // the animation's frame to be shown
        TextureRegion frame;

        // find the appropriate frame of the tilt animation to be drawn
        if( velocity.x < 0 ) {
            frame = tiltAnimation.getKeyFrame( tiltAnimationStateTime += delta, false );
            if( frame.getRegionWidth() < 0 ) frame.flip( true, false );
        } else if( velocity.x > 0 ) {
            frame = tiltAnimation.getKeyFrame( tiltAnimationStateTime += delta, false );
            if( frame.getRegionWidth() > 0 ) frame.flip( true, false );
        } else {
            tiltAnimationStateTime = 0;
            frame = tiltAnimation.getKeyFrame( 0, false );
        }

        // there is no performance issues when setting the same frame multiple
        // times as the current region (the call will be ignored in this case)
        setRegion( frame );
    }
Important aspects of the above method:
  • Based on the velocity vector we can tell if the ship is moving horizontaly.
  • We always increment the tiltAnimationStateTime variable with the given delta before requesting the next animation's frame to be drawn.
  • We flip the image horizontaly when needed. A positive width means the image is not inverted (it's being displayed as it is). A negative width means the image is inverted horizontaly.
  • If the ship is not moving horizontaly we reset the tiltAnimationStateTime variable and set the current frame to the first frame.

The following video shows the result:



Conclusion

The ship is now tilting to the side it's moving. We used some tricks to get this done, but the code is still readable. :) This is the Subversion tag for this post, and this is the full source code for the Ship2D class. Thanks for reading!

23 comments:

  1. Thank you, Gustavo. It's nice lesson.
    But this lesson needs a video of the result in the end, I think :)

    ReplyDelete
    Replies
    1. That's an awesome idea! I'll do it. :)

      Delete
    2. Now the lesson is perfect :)

      By the way, I have a question about Scene2D and OpenGL. I want draw textures without Scene2D in a game screen. But I want to use Scene2D in the game screen for "pause" menu(for instance). How can I do that?

      If you can answer me or tell me that I do something wrong please write me - a.einsam@gmail.com

      I will be very grateful to you :)

      Delete
    3. Theoretically that's not too difficult. You could setup a stage for the pause menu, and just draw it inside your render method if a condition is met.

      Something like this:

      public abstract class GameScreen implements Screen
      {
      Stage pauseMenuStage;

      public void render( float delta )
      {
      clearScreen();
      renderGame();
      if(gameIsPaused) {
      pauseMenuStage.act( delta );
      pauseMenuStage.draw();
      }
      }
      }

      Delete
  2. Hi, great work you are doing here with these tutorials, thank you for sharing. :)

    I think I found why your stage.dispose() does not work, that is because you made the stage variable final, I removed the keyword on mine and it worked fine.

    ReplyDelete
    Replies
    1. Thanks for the feedback, Alex!
      I tried removing the final keywork, but sadly with no luck. :(

      Delete
    2. Yeah, I found that the problem is deeper than I thought. I got rid of it by disposing the stage only inside the Game.dispose() method where all the Screens are disposed and the whole application is terminated, and not inside Screen.hide(). I don't know exactly if that is wrong, keeping stages unused undisposed, but I'm still trying to figure out the best way.

      I'm new to Java and Libgdx, and your tutorials are helping a lot! Best wishes.

      Delete
  3. Hey, I have a quick question regarding the licensing of the code you've made available.

    Would it be okay if I used parts of it for an ongoing Game Contest? The contest requires my source to be GPLv3, and seeing your code is APLv2 (At least it says so on Google Code), I am unsure if I am able to combine the code.

    I loved your tutorials, they helped me get up and running with the game and understand some parts I was unclear about, like the TextureAtlas or the Scene2D api. Thanks for the tutorials!

    ReplyDelete
    Replies
    1. Go ahead! You can use any code I've written for any purpose!
      Thanks for the feedback.

      Delete
    2. Awesome! Thanks man. The actual game's core, I'm writing from scratch, but your tutorials helped me understand how to do the menu, splashscreen and how to get the audio managers working, so at least initially, I'm just dragging those files into my project, while I modify them to best suit the game I'm working on :-)

      Delete
    3. Glad to help! If you want, you can send me some url of your game (after you finish it), and I'll write something about it in a future post!
      Good luck with the contest!

      Delete
    4. Sure Gustavo, my site is http://k3rnel.net. I'm writing blog posts every 2 - 3 days to summarize all I've done throughout the days.

      My code's hosted in github, at http://github.com/Nushio/Unsealed

      I recommended your blog to some other competitors that are also using LibGDX and they also found it extremely useful :-)

      Delete
    5. Oh, hey Gustavo. I decided to use the LibGDX nightlies, and a ton of things have changed since then (Mostly, API changes, and deprecation of the TableLayout).

      I uploaded my project on github, as previously mentioned:
      https://github.com/Nushio/Unsealed/

      It's not the prettiest, but it works. I'm on the nightlies now. Oh and stage.dispose(); is fixed in the nightlies, so you can uncomment that from the AbstractScreen.

      Delete
    6. Thanks for the recomendations! :)
      Yeah, I read some posts on their official blog, and my next post will be about all of these changes.
      I'm happy to know that the problem with the stage disposal is gone!

      Delete
    7. Gustavo: Can't say for sure if it was really fixed. I moved it to the bottom of the disposeable list and I can't get it to crash on the Desktop, but I had some issues on Android, and commenting stage.dispose(); appears to have fixed it, so maybe it's not entirely fixed? :-/

      Delete
  4. Hello all,
    I want to use multiple 2 D images and rotate them to give 3 D effect in android . I would like to know how can i use Libgdx as i read about scene 2D and Animation 2D but in my case multiple images are in different texture not in one to give 3d effect. So please help me regarding is it really helpful to use libgdx for android?

    any help would be appreciated!!
    thanks in advance
    Vinit

    ReplyDelete
    Replies
    1. Hi Vinit! You're better of using the official libgdx forum at: http://www.badlogicgames.com/forum/
      Good luck!

      Delete
  5. Really diggin' the interface. Great work!

    ReplyDelete
  6. Hello all,

    I did everything step by step like in this tutorial but always codes that we wrote before changed. Also version of libgdx is changed.Now my code give so many errors. I want to study in different way now. I want to learn by doing some changes on working code. Can someone send me working code or put somewhere that I can download. I mean exported code and then zipped. By the way this tutorial is very helpfull I have learned so many things. Thank you Steigert

    ReplyDelete
  7. Nice tutorial, but when I ran the game the sprite would flicker when turning/tilting right. Fixed it by using frame.isFlipX() and !frame.isFlipX() instead of frame.getRegionWidth() in the tiltShip method in Ship2D.

    ReplyDelete
  8. Hi Gustavo
    I want to design the game further, i can i proceed, means i want to design the levels, how shall i proceed??

    ReplyDelete
    Replies
    1. You can read the official docs at: http://code.google.com/p/libgdx/wiki/TableOfContents?tm=6

      You should begin with tile maps, altough this module is being refactored by the Libgdx team. In fact, I paused writing posts because many refactorings are being made.

      Good luck!

      Delete
  9. These tutorials were priceless for what I needed to learn, thanks. If anyone else hates the fact that the ship goes from totally tilted back to normal without tilting partially back first, this should do it in the tiltShip function (only showing one section):

    if (velocity.x < 0) {
    if (acceleration.x < 0) {
    frame = tiltAnimation.getKeyFrame(tiltAnimationStateTime += delta, false);
    } else {
    if (tiltAnimationStateTime >= delta) {
    tiltAnimationStateTime -= delta;
    }

    frame = tiltAnimation.getKeyFrame(tiltAnimationStateTime, false);
    }

    if (frame.getRegionWidth() < 0) {
    frame.flip(true, false);
    }
    }

    ReplyDelete