Bringing a scene to life with sprites

Here, we come to the topic which might be considered to be the most necessary aspect to creating any 2D game. Sprites allow us to display 2D images on our scene which can be used to display buttons, characters/avatars, environments and themes, backgrounds, and any other entity in our game which may require representation by means of an image file. In this recipe, we'll be covering the various aspects of AndEngine's Sprite entities which will give us the information we need to continue to work with Sprite objects later on in more complex situations.

Getting ready…

Before we dive into the inner-workings of how sprites are created, we need to understand how to create and manage AndEngine's BitmapTextureAtlas/BuildableBitmapTextureAtlas objects as well as the ITextureRegion object. For more information, please refer to the recipes, Working with different types of textures and Applying texture options in Chapter 1, AndEngine Game Structure.

Once these recipes have been covered, create a new empty AndEngine project with the BaseGameActivity class, provide a PNG formatted image of any size up to 1024 x 1024 pixels in dimension, naming it as sprite.png and place it in the assets/gfx/ folder of the project, then continue to the How to do it... section of this recipe.

How to do it…

Sprites can be created and applied to our Scene object in just a few quick steps. We must first set up the necessary texture resources that the sprite will use, we must create the Sprite object, and then we must attach the Sprite object to our Scene object. See the following steps for more detail:

  1. We will start by creating the texture resources in the onCreateResources() method of our BaseGameActivity class. Make sure the mBitmapTextureAtlas and mSpriteTextureRegion objects are global variables, so that they can be reached throughout the various life cycle methods of our activity:
      BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");
        
        /* Create the bitmap texture atlas for the sprite's texture region */
        BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 256, 256, TextureOptions.BILINEAR);
        
        /* Create the sprite's texture region via the BitmapTextureAtlasTextureRegionFactory */
        mSpriteTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(mBitmapTextureAtlas, this, "sprite.png");
        
        /* Build the bitmap texture atlas */
        try {
          mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 1, 1));
        } catch (TextureAtlasBuilderException e) {
          e.printStackTrace();
        }
        /* Load the bitmap texture atlas into the device's gpu memory */
        mBitmapTextureAtlas.load();
  2. Next, we will create the Sprite object. We can create and attach the Sprite object to the Scene object in either the onCreateScene() or the onPopulateScene() methods of our activity. The parameters to supply in its constructor include, in this order, the sprites initial x coordinate, initial y coordinate, ITextureRegion object, and finally the mEngine object's vertex buffer object manager:
        final float positionX = WIDTH * 0.5f;
        final float positionY = HEIGHT * 0.5f;
        
        /* Add our marble sprite to the bottom left side of the Scene initially */
        Sprite mSprite = new Sprite(positionX, positionY, mSpriteTextureRegion, mEngine.getVertexBufferObjectManager());
    The last step is to attach our Sprite to the Scene, as is necessary in order to display any type of Entity on the device's display:
        /* Attach the marble to the Scene */
        mScene.attachChild(mSpriteTextureRegion);

How it works…

As it might appear in the steps in the previous section, setting up the mBitmapTextureAtlas and mSpriteTextureRegion objects actually require more work than creating and setting up the mSprite object specifically. For this reason, it is encouraged to complete the two recipes mentioned in the Getting started... section beforehand.

In the first step, we will create our mBitmapTextureAtlas and mSpriteTextureRegion objects, suitable to the needs of our sprite.png image. Feel free to use any texture options or texture format in this step. It is a very good idea to get to know them well.

Once we've our ITextureRegion object created and it's ready for use, we can move to step two where we will create the Sprite object. Creating a sprite is a straightforward task. The first two parameters will be used to define the initial position of the sprite, relative to its center point. For the third parameter, we will pass in the ITextureRegion object that we created in step one in order to provide the sprite with its appearance as an image on the scene. Finally, we pass in the mEngine.getVertexBufferObjectManager() method, which is necessary for most entity subtypes.

Once our Sprite object is created, we must attach it to the Scene object before it will be displayed on the device, or we can attach it to another Entity object which is already attached to the Scene object. See the Understanding AndEngine entities recipe given in this chapter for more information on entity composition, placement, and other various must-know aspects of Entity objects.

There's more...

No game is complete without some sort of sprite animation. After all, a player can only return to a game so many times before getting bored in a game where the characters slide around the screen without moving their feet, don't swing their weapon when attacking an enemy, or even when a grenade simply disappears rather than causing a nice explosion effect. In this day and age, people want to play games which look and feel nice, and nothing says, "Nice!", like buttery smooth animating sprites, right?

In the Working with different types of textures recipe in Chapter 1, AndEngine Game Structure, we'd covered how to create a TiledTextureRegion object which allows us to import useable sprite sheets into our game as a texture. Now let's find out how we can use that TiledTextureRegion object with an AnimatedSprite object in order to add some animation to our game's sprites. For this demonstration, the code will be working with an image of 300 x 50 pixels in dimension. The sprite sheet can be something as simple as the following figure, just to get an idea of how to create the animation:

There's more...

The sprite sheet in the previous figure can be used to create a TiledTextureRegion object with 12 columns and 1 row. Creating the BuildableBitmapTextureAtlas and TiledTextureRegion objects for this sprite sheet can be done with the following code. However, before importing this code, be sure to make a global declaration of the texture region—TiledTextureRegion mTiledTextureRegion—in your test project:

    /* Create the texture atlas at the same dimensions as the image (300x50)*/
    BuildableBitmapTextureAtlas mBitmapTextureAtlas = new BuildableBitmapTextureAtlas(mEngine.getTextureManager(), 300, 50, TextureOptions.BILINEAR);
    
    /* Create the TiledTextureRegion object, passing in the usual parameters,
     * as well as the number of rows and columns in our sprite sheet for the 
     * final two parameters */
    mTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(mBitmapTextureAtlas, this, "gfx/sprite_sheet.png", 12, 1);
    
    /* Build and load the mBitmapTextureAtlas object */
    try {
      mBitmapTextureAtlas.build(new BlackPawnTextureAtlasBuilder<IBitmapTextureAtlasSource, BitmapTextureAtlas>(0, 0, 0));
    } catch (TextureAtlasBuilderException e) {
      e.printStackTrace();
    }
    mBitmapTextureAtlas.load();

Now that we've got the mTiledTextureRegion sprite sheet to play with in our project, we can create and animate the AnimatedSprite object. If you are using a sprite sheet with black circles as seen in the previous figure, don't forget to change the color of the Scene object to a non-black color so we can see the AnimatedSprite object:

    /* Create a new animated sprite in the center of the scene */
    AnimatedSprite animatedSprite = new AnimatedSprite(WIDTH * 0.5f, HEIGHT * 0.5f, mTiledTextureRegion, mEngine.getVertexBufferObjectManager());
    
    /* Length to play each frame before moving to the next */
    long frameDuration[] = {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200};
    
    /* We can define the indices of the animation to play between */
    int firstTileIndex = 0;
    int lastTileIndex = mTiledTextureRegion.getTileCount();
    
    /* Allow the animation to continuously loop? */
    boolean loopAnimation = true;
    
    * Animate the sprite with the data as set defined above */
    animatedSprite.animate(frameDuration, firstTileIndex, lastTileIndex, loopAnimation, new IAnimationListener(){

      @Override
      public void onAnimationStarted(AnimatedSprite pAnimatedSprite,
          int pInitialLoopCount) {
        /* Fired when the animation first begins to run*/
      }

      @Override
      public void onAnimationFrameChanged(AnimatedSprite pAnimatedSprite,
          int pOldFrameIndex, int pNewFrameIndex) {
         /* Fired every time a new frame is selected to display*/
      }

      @Override
      public void onAnimationLoopFinished(AnimatedSprite pAnimatedSprite,
          int pRemainingLoopCount, int pInitialLoopCount) {
        /* Fired when an animation loop ends (from first to last frame) */
      }

      @Override
      public void onAnimationFinished(AnimatedSprite pAnimatedSprite) {
        /* Fired when an animation sequence ends */
      }
      );
    
    mScene.attachChild(animatedSprite);

Creating the AnimatedSprite object can be done following the steps in this recipe while creating a regular Sprite object. Once it's created, we are able to set up its animation data, including individual frame duration, first and last tile indices to animate through, and whether or not to loop the animation continuously. Note that the frameDuration array must be equal to the frame count! Failing to follow this rule will result in an IllegalArgumentException exception. Once the data has been set up, we can call the animate() method on the AnimatedSprite object, supplying all of the data and adding an IAnimationListener listener if we wish. As the comments in the listener suggest, we gain a large portion of control over the animations with AndEngine's AnimatedSprite class.

Using OpenGL's dithering capability

When developing visually appealing games on the mobile platform, it is a likely scenario that we'll want to include some gradients in our images, especially when dealing with 2D graphics. Gradients are great for creating lighting effects, shadows, and many other objects we'd otherwise not be able to apply to a full 2D world. The problem lies in the fact that we're developing for mobile devices, so we unfortunately do not have an unlimited amount of resources at our disposal. Because of this, AndEngine down-samples the surface view color format to RGB_565 by default. Regardless of the texture format we define within our textures, they will always be down-sampled before being displayed on the device. We could alter the color format applied to AndEngine's surface view, but it's likely that the performance-hit will not be worth it when developing larger games with many sprites.

Here, we have two separate screenshots of a simple sprite with a gradient texture; both textures are using the RGBA_8888 texture format and BILINEAR texture filtering (the highest quality).

Using OpenGL's dithering capability

The image on the right-hand side is applied to the Scene object without any modifications, while the image on the left-hand side has OpenGL's dithering capability enabled. The difference between the two otherwise identical images is immediately noticeable. Dithering is a great way for us to combat down-sampling applied by the surface view without us having to rely on maximum color quality formats. In short, by dithering low-levels of randomized noise within our image's colors, it results in a smoother finish which is found in the image to the left.

Enabling dithering is simple to apply to our entities in AndEngine, but as with everything, it's wise to pick and choose which textures apply dithering. The algorithm does add a little bit of extra overhead, where if used too often could result in a larger performance loss than simply reverting our surface view to RGBA_8888. In the following code, we are enabling dithering in our preDraw() method and disabling it in our postDraw() method:

@Override
protected void preDraw(GLState pGLState, Camera pCamera) {
  // Enable dithering
  pGLState.enableDither();
  super.preDraw(pGLState, pCamera);
}

@Override
protected void postDraw(GLState pGLState, Camera pCamera) {
  // Disable dithering
  pGLState.disableDither();
  super.postDraw(pGLState, pCamera);
}

Dithering can be applied to any subtype of AndEngine's Shape class (Sprites, Text, primitives, and so on.).

Note

For more information about OpenGL ES 2.0 and how to work with all of the different functions, visit the link at http://www.khronos.org/opengles/sdk/docs/man/.

See also

  • Working with different types of textures in Chapter 1, Working with Entities
  • Applying texture options in Chapter 1, Working with Entities.
  • Understanding AndEngine entities in this chapter.