Dienstag, 9. Dezember 2014

AndEngine Tutorial: Displaying some graphics - the right way

Hi.

We had a lot of stuff to do in the last couple of days, but now I thought it was time again to post something.

Based on some trouble I notice a lot of new AndEngine users have, I wanted to write a little something about working with sprites. So let's cover the basics at first real quick.

Notice that we use AndEngine GLES2 (not AC), so the code snippets probably won't work 1-1 if you use AnchorCenter. But the basic stuff still applies. Do not skip the UpdateThread part of this tutorial, as this is something almost every beginner is doing wrong, getting really ugly errors.

Basics

When you write a game one of the most important things is to display some graphics, these are usually called sprites in the 2D game development scene. And this is what they are called in AndEngine as well.

There are a few sprite types you can use in AndEngine:
  • Sprite 
  • TiledSprite
  • AnimatedSprite
A normal Sprite is just a simple graphic you can display and move around the screen by setting it's position.





TiledSprite is a Sprite that is divided/"cut" into tiles. The image you use for that is getting "cut" into rectangles. So if you specify a 3x2 TiledSprite, the image you use for that will be cut into 6 equally big tiles and a TiledSprite will display only one of this tiles at the same time. Of course you can tell the TiledSprite which tile you want to display.





The AnimatedSprite is a TiledSprite with the advantage, that you can animate it by telling which of the tiles it should display in which order and for how long. So you can create something that is often called a sprite sheet, with different states of some graphic that you want to animate on the same image file. For example the different positions of a stick figure that is doing some movements.


TextureAtlas & TextureRegion

Before you can create your Sprite, you need to create a TextureAtlas and a TextureRegion. There are already plenty of good tutorials that explain what exactly these things are, so I am not going to repeat this stuff. The most improtant fact that you need to know at first is, that you need to create them before you can create your sprite.

Creating a Sprite

So lets see how we can create a Sprite with AndEngine GLES2 (not AC! But it should be pretty similar.).
At first you have to create the TextureAtlas:

 myTextureAtlas = new BitmapTextureAtlas(activity.getTextureManager(), width, height, TextureOptions.BILINEAR_PREMULTIPLYALPHA);  

The variables width and height should be at least the size of your image file you want to load.

And using this TextureAtlas and the BitmapTextureAtlasTextureRegionFactory class, we can easily create our TextureRegion.
 BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");  
 myTextureRegion = BitmapTextureAtlasTextureRegionFactory.createFromAsset(myTextureAtlas, activity, "myImage.png",0, 0);   

Notice the first line, where we set the AssetBasePath to "/gfx". This means that the "myImage.png" file will be looked for in your assets/gfx/ directory. You do not need to specify this every time you create a new TextureAtlas/Region and Sprite, the Factory will remember the last thing you configured. But it is safe to call it if you have more than one directory you load images from and constantly switch the AssetBasePath, otherwise it can lead to nasty bugs where AndEngine can not find a certain file all of a sudden, simply because you changed the path in your last call to something else.

In order for your TextureAtlas to be loaded into memory, you need to call "load()" on it. Like that:
 myTextureAtlas.load();  


Now we have everything we need to create a Sprite. This is done like that:
 Sprite mySprite = new Sprite(pX, pY, myTextureRegion,activity.getVertexBufferObjectManager());  

pX and pY will be the position of the Sprite, but of course you can change this later by calling "mySprite.setPosition(x,y);".

The whole thing in one snippet would be the following:
 myTextureAtlas = new BitmapTextureAtlas(activity.getTextureManager(), width, height, TextureOptions.BILINEAR_PREMULTIPLYALPHA);   
 BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");   
 myTextureRegion =BitmapTextureAtlasTextureRegionFactory.createFromAsset(myTextureAtlas, activity, "myImage.png",0, 0);    
 myTextureAtlas.load();  
 Sprite mySprite = new Sprite(pX, pY,myTextureRegion,activity.getVertexBufferObjectManager());   

Also please notice: You can create as many Sprites(of the same kind) out of one TextureAtlas+Region as you like and you need to "load()" every TextureAtlas only once, so you do not have to create and load new ones for every new Sprite that uses the same texture (image). You just need to create this stuff for every new image you want to load into memory.

Creating a TiledSprite

The same mechanics are used to create the other Sprite types as well, the only difference is that you need to call "createTiledFromAsset(...)" on the BitmapTextureAtlasTextureRegionFactory. The whole code in one thing:
 myTextureAtlas = new BitmapTextureAtlas(activity.getTextureManager(), width, height, TextureOptions.BILINEAR_PREMULTIPLYALPHA);   
 BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");   
 myTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(myTextureAtlas, activity,"mySpriteSheet.png", 0, 0, 3, 2);  
 myTextureAtlas.load();  
 TiledSprite sprite = new TiledSprite(y, x, myTiledTextureRegion,activity.getVertexBufferObjectManager());  

The last two arguments of the "createTiledFromAsset(...)" method are the columns and the rows.
So you will get three columns and two rows, resulting in 6 different tiles.

Your TiledSprite will display the tile with index 0 per default. You can change the current displayed tile by calling "setCurrentTileIndex(index)" on your TiledSprite. The following image demonstrates how the above code would divide the image file and what the indices of the tiles would be:


Creating an AnimatedSprite

And again, creating the AnimatedSprite is pretty much the same, you create your TiledTextureRegion but instead of creating a TiledSprite out of it, you create your AnimatedSprite. Like this:
 myTextureAtlas = new BitmapTextureAtlas(activity.getTextureManager(), width, height, TextureOptions.BILINEAR_PREMULTIPLYALPHA);   
 BitmapTextureAtlasTextureRegionFactory.setAssetBasePath("gfx/");   
 myTiledTextureRegion = BitmapTextureAtlasTextureRegionFactory.createTiledFromAsset(myTextureAtlas, activity,"mySpriteSheet.png", 0, 0, 3, 2);  
 myTextureAtlas.load();  
 AnimatedSprite sprite = new AnimatedSprite (y, x, myTiledTextureRegion,activity.getVertexBufferObjectManager());  


The magic happens, when you call the "animate(...)" method on your AnimatedSprite. My favorite one is the following, which gives you quite good control over what happens:
 boolean loop = true;  
 sprite.animate(new long[] { 140, 140,140 }, new int[] {0,1,2}, loop);  

The first parameter is a long array. This one will have the durations in milliseconds that the frames should be displayed. So the 140 means that the frame (that will be specified in the next parameter) should be displayed for 140 milliseconds before the next one will be displayed.

The second parameter is an int array. This one specifies the indices of your tiles in the order you want them to be displayed.

The last parameter is a boolean which specifies if you want to loop the animation. This means that after it displayed tile 0, 1 and 2 it would start from the beginning displaying 0 etc. again.

Actually displaying the Sprite: UpdateThread

Creating your Sprite instance will of course not lead to the instant displaying of it on your screen. Before your sprite can be drawn/displayed, it has to be attached to the current Scene instance or to some Entity that itself (or one of its ancestors/parents) is attached to the Scene (or the HUD).
The second approach should be favored if you want to have something like layers, but this can be explained in another post. 

The simple approach that is often seen in tutorials is to simply do this:
 mScene.attachChild(sprite);  

While this is true, the very important part about the whole thing is missing! You ALWAYS have to attach your stuff (like Shapes or Sprites) you want to be drawn on the UpdateThread! Otherwise this will lead to something like "FATAL EXCEPTION: UpdateThread java.lang.IndexOutOfBoundsException: Invalid index 11, size is 0..."

Something that is really ugly to debug because the Exception trace usually does not give you any hint which portion of YOUR code produced the Exception.

To avoid this and have a clean code base, attach AND detach the stuff you want to be drawn always on the UpdateThread like this:
 activity.runOnUpdateThread(new Runnable() {  
                                    @Override  
                                    public void run() {  
                                         mScene.attachChild(mySprite);  //or mySprite.detachSelf()
                                    }  
                               });  
If you use the AnchorCenter version of AndEngine, you have to call "runOnUpdateThread(...)" on an instance of Engine, not on (BaseGame)Activity.

Easier way attaching/detaching

As you can see attaching your Sprites (Entites) the right way leads to a very bloated code plus it leads to heavy code duplication. To avoid blowing up your code unnecessarily, you should create yourself a Util class that hides away all the unnecessary stuff. I share my own version with you :). But notice again, this will not work with AndEngine GLES2 Anchor Center, as you need to call the "runOnUpdateThread" method on Engine instead of Activity. I am not sure if there are other differences one would need to adapt. But I am sure you will be able to change this little differences if you need to. :D

 You could use my class like this to safely attach or detach your stuff:
 SafeAttachDetach.attach(activity, mScene, mySprite);  

The parameters in this call are your activity, your parent (on which your Sprite should be attached) and your Sprite itself of course.

The class looks like this:
 import java.util.ArrayList;  
 import java.util.List;  
 import org.andengine.entity.IEntity;  
 import org.andengine.ui.activity.BaseGameActivity;  
 public class SafeAttachDetach{  
      public static void attach(BaseGameActivity activity, final IEntity parent, final IEntity child)  
      {  
           activity.runOnUpdateThread(new Runnable()  
           {  
                public void run()  
                {  
                     if(child != null && parent != null && child.hasParent() == false)  
                     parent.attachChild(child);  
                }  
           });  
      }  
      public static void detach(BaseGameActivity activity, final IEntity entityToDetach)  
      {  
           activity.runOnUpdateThread(new Runnable()  
           {  
                public void run()  
                {  
                     if(entityToDetach != null )  
                     entityToDetach.detachSelf();  
                }  
           });  
      }  
      public static void massAttach(BaseGameActivity activity, final IEntity parent, final List<IEntity> children)  
      {  
           activity.runOnUpdateThread(new Runnable()  
           {  
                public void run()  
                {  
                     ArrayList<IEntity> copyOfList = new ArrayList<IEntity>(children);  
                     for(IEntity entity : copyOfList)  
                     {  
                          if(entity != null && parent != null && entity.hasParent() == false)  
                          parent.attachChild(entity);  
                     }  
                }  
           });  
      }  
      public static void massDetach(BaseGameActivity activity,  final List<IEntity> children)  
      {  
           activity.runOnUpdateThread(new Runnable()  
           {  
                public void run()  
                {  
                     ArrayList<IEntity> copyOfList = new ArrayList<IEntity>(children);  
                     for(IEntity entity : copyOfList)  
                     {  
                          if(entity != null)   
                          entity.detachSelf();  
                     }  
                }  
           });  
      }  
 }  


Keine Kommentare:

Kommentar veröffentlichen