Libgdx Cross/platform Game Development Cookbook
上QQ阅读APP看书,第一时间看更新

Rendering sprite-sheet-based animations

So far, we have seen how to render textures and regions of an atlas using Libgdx. Obviously, you can move textures around over time to produce a sense of motion. However, your characters will not come to life until they are properly animated. Not only should they go from one side of the screen to the other, but they should also seem like they are walking, running, or jumping according to their behavior.

Typically, we refer to characters physically moving in the game world as external animation, while we use the term internal animation to talk about their body movement (for example, lifting an arm).

In this recipe, we will see how to implement sprite-sheet-based animation using mechanisms provided by Libgdx. We will do so by populating our previous jungle scene with animated versions of the same characters. A sprite sheet is nothing more than a texture atlas containing all the frames that conform a character's animation capabilities. Think of it as having a notebook with drawings in a corner. If you go through the pages really fast, you will perceive the illusion of motion.

Behold our caveman's jumping skills! In the following screenshot, you can find an example of sprite-sheet-based animation, more specifically, a complete jump cycle:

Rendering sprite-sheet-based animations

Getting ready

You will also need the sample projects in your Eclipse workspace as well as data/caveman.atlas and data/trex.atlas along with their corresponding PNG textures.

How to do it…

First things first, the code for this recipe is located inside the AnimatedSpriteSample.java file, which uses the known ApplicationListener pattern. We are going to use two texture atlases created with texturepacker-gui that contain a walk cycle animation for our caveman and the merciless Tyrannosaurus Rex respectively. Feel free to catch up with atlases in the More effective rendering with regions and atlases recipe.

The caveman walk cycle can be found in the caveman-sheet.png and caveman.atlas files while the dinosaur is stored in the trex-sheet.png and trex.atlas files. As usual, everything is located under the [cookbook]/samples/samples-android/assets/data folder. Here is a simplified version of the dinosaur sprite sheet:

How to do it…

The FRAME_DURATION determines for how long (in seconds) we should show each frame of our sprite sheet before advancing to the next one. Since our animations were tailored to be displayed at 30 frames per second, we set it to 1.0f / 30.0f:

private static final float FRAME_DURATION = 1.0f / 30.0f;

To achieve this demo's goal, we will need a camera, a sprite batch, and two atlases, one for the caveman and another one for the dinosaur walk cycles. We will also need a background texture if we do not want to show a dull black background:

private TextureAtlas cavemanAtlas;
private TextureAtlas dinosaurAtlas;
private Texture background;

Texture atlases simply provide a collection of texture regions we can retrieve by name. We need a way of specifying how our animations are going to be played. To that end, Libgdx provides us with the Animation class. Every distinct character animation should have its own Animation object to represent it. Therefore, we will use a dinosaurWalk instance and a cavemanWalk instance. Finally, we will control how our animations advance through the animationTime variable:

private Animation dinosaurWalk;
private Animation cavemanWalk;
private float animationTime;

Inside the create() method, we build the orthographic camera, rendering area, and batch. We also initialize our animationTime variable to 0.0f to start counting from then onwards:

camera = new OrthographicCamera();
viewport = new FitViewport(SCENE_WIDTH, SCENE_HEIGHT, camera);
batch = new SpriteBatch();
animationTime = 0.0f;

The next step is to load the atlases for the caveman and the dinosaur as well as the background texture:

cavemanAtlas = new TextureAtlas(Gdx.files.internal("data/caveman.atlas"));
dinosaurAtlas = new TextureAtlas(Gdx.files.internal("data/trex.atlas"));
background = new Texture(Gdx.files.internal("data/jungle-level.png"));

Later, we retrieve the collection of regions of both our atlases using the getRegions() method and sort them alphabetically because that is how we have arranged the frames in our animation atlases:

Array<AtlasRegion> cavemanRegions = new Array<AtlasRegion>(cavemanAtlas.getRegions());
cavemanRegions.sort(new RegionComparator());

Array<AtlasRegion> dinosaurRegions = new Array<AtlasRegion>(dinosaurAtlas.getRegions());
dinosaurRegions.sort(new RegionComparator());

Note

The Array<T> class is a Libgdx built-in container quite similar to the standard Java ArrayList<T>; the difference is that the former was written with performance in mind. Libgdx comes with more containers such as dictionaries, sets, binary arrays, heaps, and more. Using them rather than their standard counterparts is more than advisable, especially if you are targeting mobile devices.

The RegionComparator class is nothing more than a convenience inner class to sort our AtlasRegion arrays. AtlasRegion inherits from TextureRegion featuring additional data related to its packaging. In this case, we are interested in its name member so as to be able to sort the images alphabetically:

private static class RegionComparator implements Comparator<AtlasRegion> {
   @Override
   public int compare(AtlasRegion region1, AtlasRegion region2) {
      return region1.name.compareTo(region2.name);
   }
}

It is now time to create the Animation instances. The constructor takes the frame duration, Array<TextureRegion>, representing all the frames that form the animation and the playback mode. Just like the name hints, PlayMode.LOOP makes an animation play over and over again. We will learn more about other play modes later on in this recipe. The code is as follows:

cavemanWalk = new Animation(FRAME_DURATION, cavemanRegions, PlayMode.LOOP);
dinosaurWalk = new Animation(FRAME_DURATION, dinosaurRegions, PlayMode.LOOP);

Finally, we position the camera to be at half its width and height so we can see the background properly, which will be rendered with its bottom-left corner at the origin:

camera.position.set(VIRTUAL_WIDTH * 0.5f, VIRTUAL_HEIGHT * 0.5f, 0.0f);

We do not want to be leaking memory all over the place. That would be disgusting and unacceptable! That is why we make sure to dispose of all the resources in the conveniently named dispose() method of our ApplicationListener:

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

Finally, we get to the key bit, the render() method. As usual, we first clear the screen with a black background color and set the viewport, that is, our rendering area in screen space. The next step is to increment our animationTime variable with the time that has passed since our last game loop iteration. We can get such information from Gdx.graphics.getDeltaTime(). To prepare the terrain for rendering, we update the camera matrices and frustum planes, set the sprite batch projection matrix, and then call begin().

Which frame shall we draw? That is a good question. Luckily enough, as long as we provide animationTime, the Animation class has all the information it needs to figure it out: the list of frames, the frame time, and the playback mode. We can call getKeyFrame() passing in the time in seconds to obtain the frame to draw each frame and give it to the batch draw() method. That way, we can draw our two animated characters along the background texture. Easy as pie. The code is as follows:

public void render() {      
   ...   

   animationTime += Gdx.graphics.getDeltaTime();
      
   batch.begin();
   
...
   
   TextureRegion cavemanFrame = cavemanWalk.getKeyFrame(animationTime);
   width = cavemanFrame.getRegionWidth();
   height = cavemanFrame.getRegionHeight();
   float originX = width * 0.5f;
   float originY = height * 0.5f;
   
   batch.draw(cavemanFrame,
            1.0f - originX, 3.70f - originY,
            originX, originY,
            width, height,
            WORLD_TO_SCREEN, WORLD_TO_SCREEN,
            0.0f);      
   
   ...

   batch.end();
}

Now you can see our caveman and the dinosaur in motion!

Note

Animations can be flipped by calling flip() in the region returned by getKeyFrame(). This will toggle the flip state of the region, so avoid doing it for every frame or you will cause the sprite to flicker.

How it works…

The implementation of the Animation class is extremely simple, but it does a great job in helping us introduce animated characters in our games using sprite sheets. See for yourself by reading its implementation in the Libgdx GitHub repository. You will find that reading the Libgdx source at https://github.com/libgdx/libgdx/blob/master/gdx/src/com/badlogic/gdx/graphics/g2d/Animation.java is actually a great way of learning how it works internally; do not let it scare you away.

It holds a low-level Java array of TextureRegion references, the frame duration parameter, and caches the whole animation duration. The main part of the action takes place in the getKeyFrameIndex() method. It simply grabs the current frame index by dividing stateTime over frameDuration. Then, it accesses the region's array depending on the animation PlayMode. It makes sure not to pick an invalid frame and returns the appropriate index. Obviously, if you call the getKeyFrame() method directly, you will obtain the region straightaway.

There's more…

So far, we have seen the most basic operation you can do using the Animation class, simply retrieving the current frame on each iteration of the game loop for rendering purposes. However, if you want to use this system in slightly more complex situations, you will need further control. In this section, we will cover what different playback modes are at our disposal, how to check when a one-shot animation is done, and how to manage complex characters with tons of animations.

Using different play modes

Animation supports different ways of sequencing frames, which can be set and retrieved on a per instance basis through the setPlayMode() and getPlayMode() methods respectively. These take and return one of the PlayMode enumerated type values. Here is the complete list:

  • PlayMode.NORMAL: This sequentially plays all the animation frames only once
  • PlayMode.REVERSED: This plays all the animation frames once in reverse order
  • PlayMode.LOOP: This continuously plays all the animation frames
  • PlayMode.LOOP_REVERSED: This continuously plays all the frames in reverse order
  • PlayMode.LOOP_RANDOM: This picks a random frame every time from the available ones

Checking when an animation has finished

Usually, we want to play certain animations in an infinite loop for as long as an action is being carried out, a character running for example. However, other animations should only be played once per trigger, such as a sword slash attack. It is very likely that we require to know when such animation has finished playing to allow the character to do something else. The isAnimationFinished() method returns true when the given animation is finished provided it is played in PlayMode.NORMAL or PlayMode.REVERSED mode and with the looping flag set to false.

Handling a character with many animations

By now, you have probably figured out that having Animation objects hanging around is not very scalable when implementing a full game with dozens of characters. Also, manually providing the frames for every animation in code could become a truly gargantuan task. There is a need for some sort of abstraction and data-driven approach.

You could define your animated characters in XML or JSON providing all the necessary data to build a set of Animation objects. Here is a hypothetical example for our caveman:

<?xml version="1.0" encoding="UTF-8"?>
<animatedCharacter atlas="data/caveman.atlas"
            frameDuration="0.03333" >
   
   <animation name="idle" mode="loop" >
      <frame region="caveman0001" />
      <frame region="caveman0002" />
      <frame region="caveman0003" />
      ...
   </animation>

   <animation name="walk" mode="loop"> ... </animation>
   <animation name="jump" mode="normal">  ... </animation>
</animatedCharacter>

Naturally, you would need some code to manage all this in the game. Here is a skeleton of what could be an AnimatedCharacter class that reads the previous file format and loads the information into a dictionary of animation names to Animation objects. It also contains information of the current animation and the play time. The only thing it really does is provide a way of setting the current animation by name, controlling its play state, and retrieving the frame that should be shown at a given point in time.

Implementation details are left to you, but it should be pretty straightforward. The code is as follows:

public class AnimatedCharacter
{
   private ArrayMap<String, Animation> animations;
   private float time;
   private Animation currentAnimation;
   
   public AnimatedCharacter(FileHandle file);
   public void update(float deltaTime);
   public AtlasRegion getCurrentFrame();
   public String getAnimation();
   public void setAnimation(String name);
   public void setPlaybackScale(float scale);
   public void stop();
   public void pause();
   public void play();
}

See also

  • To animate some game elements such as items and UI, you can also use interpolations. To know more, check out the Smooth animations with Universal Tween Engine recipe in Chapter 11, Third-party Libraries and Extras.
  • Sprite sheet animations are often enough, but skeletal-based animations offer much cleaner results and a ton of other features worth considering. Go to the Skeletal animations with Spine recipe in Chapter 11, Third-party Libraries and Extras, to read more on the topic.