3
\$\begingroup\$

I'm making a similar game to the talking tom game where a character is animated on the screen and talks back.

I have about 100 transparent PNGs for each animation. There are 10 Animations. Each PNG has been compressed to the max, max file size is 30KB.

I've been having issues with memory management before ( fixed it ), but this is what I've tried before :

  • Loading all PNGs into the assets manager on splashscreen : Takes too long to load, crashes at the end of the loading, no errors, no log.
  • Packing all the PNGs into atlas files : Each PNG is 480*800, so I get about 5 pages of 2048*2048 atlas files for each. Takes too much time to load in the Assets manager, and same as before, crashes with no errors.
    • I can't make the PNGs smaller in dimensions, please don't suggest that.
    • I don't want to use GIFs or Videos for the animations.

What has worked for me is using a timer, no assets manager, and just loading whatever frame I need. It's working great on the 3 Android phones I have at my house + Friends, but they're all modern Samsung & Nexus devices. I've also tested on a few emulators, and it's working just fine.

I published the game a few days ago, and I'm getting some reviews about the game crashing right away, and since there is no errors or log to be reported, I'm not sure how to reproduce the issue or debug it.

Here's the code of my Actor that takes care of the animation :

public class AnimActor extends Actor {

private SpriteBatch batch;
private TextureAtlas textureAtlas;
private Sprite sprite;
private int currentFrame = 1;
private int TotalFrames ;
FileHandle[] filesxx;
String currentFolder;
Boolean currentLoop;
float stateTime;
TalkerGame Game;
AssetManager assets_manager;
String[] fileList;
TextureRegion tex1;
Texture tex = null;
Timer Timerx = null;
Timer.Task TheTask = null;
public AnimActor(TalkerGame talkerGame, AssetManager manager)
{
    Game = talkerGame;
    assets_manager = manager;
    batch = new SpriteBatch();
    sprite = new Sprite();

    float scale = (float)( (float)Gdx.graphics.getWidth() / (float) (480));

    sprite.setPosition(0, 0);
    sprite.setBounds(0,0,(float)Gdx.graphics.getWidth() ,(float)Gdx.graphics.getHeight() );
//    sprite.setScale(scale);
}

public void PlayAnimation(String FolderName,Boolean loop)
{

    currentLoop = loop;
    currentFolder = FolderName;
    currentFrame = 1;
    //String[] fileList = new String[0];
    ArrayList<String> FileListArray = new ArrayList<String>();
    final String Path = FolderName + "/";

     filesxx = Gdx.files.internal(Path).list();
    for (FileHandle file : filesxx) {
        // do something interesting here
        if( !file.name().contentEquals("icon.png") && file.name().contains(".png") )
        {
            FileListArray.add(file.name());

        }
    }


    Collections.sort
            (FileListArray, String.CASE_INSENSITIVE_ORDER);
    fileList = FileListArray.toArray(new String[0]);

    // Get all files in Folder

    TotalFrames = fileList.length-1;

    if(TheTask != null)
    {
        TheTask.cancel();
    }
   TheTask = new Timer.Task() {
        @Override
        public void run() {
            currentFrame++;
            if ((currentFrame > TotalFrames) && (currentLoop == false))
            {
                // Play Idle
                PlayAnimation("idle", true);

                currentLoop = true;
                currentFolder = "idle";

                // Stop Music if was playing
                Game.StopMusic();
                // Start/Restart Recording
                Game.StartInterfaceRecord();
            }
            if ((currentFrame > TotalFrames) && (currentLoop == true))
            {
                currentFrame = 1;
            }

           /* if ((currentFrame > TotalFrames))
            {
                currentFrame = 1;
            }*/
            sprite.setRegion(getNextRegion(currentFrame));
        }
    };

    if(Timerx != null)
    {
        Timerx.stop();
        Timerx.clear();
    }
    Timerx = new Timer();
    Timerx.schedule(TheTask, 0, 1 / 30.0f );

}

public TextureRegion getNextRegion(int CurrentFramex)
{
   // System.gc();

    String file_name = fileList[CurrentFramex]; //  String.valueOf(CurrentFramex)+".png";
    if(tex!=null)
    {
      //  Game.aInterface.Log("Disposing Texture");
        tex.dispose();
    }
    String num = "0";
    if(CurrentFramex <10)
    {
        num = "00";
    }
   // System.gc();
    FileHandle file = Gdx.files.internal(currentFolder+"/"+file_name);
        tex = new Texture(file);
        TextureRegion tex1= new TextureRegion(tex);
    return tex1;
}
@Override
public void draw(Batch batch, float parentAlpha) {
    // TODO Auto-generated method stub
    sprite.draw(batch);

}

@Override
public void act(float delta) {
    // TODO Auto-generated method stub
    super.act(delta);

}


public String getCurrentFrameFile()
{
    return currentFolder+"/"+fileList[currentFrame];
}

public void dispose(){
    batch.dispose();
    sprite.getTexture().dispose();
}

}

Thanks

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

As I can see, you probably think that if your image takes 30kb on disk it will allocate 30kb of RAM - it won't!

Classic 32 bit PNG file (with transparency) has 4 values - R (255 combinations), G (also 255), B (255) and A - alpha (also 255 combinations). It's for one single pixel.

255 is one byte so one single pixel takes 4 bytes of RAM.

Now let's calculate:

You have 5 2048x2048 images - each of them is loaded to the memory. one image 2048 * 2048 gives you 4,194,304 (4 million) pixels, each pixel takes 4 bytes of memory so we have to multiply by 4 which gives: 4,194,304 * 4 = 16777216 (nearly 16 million bytes). Let's convert it to megabytes:

16777216 bytes = 16 MB of ram - this is still one atlas!

You have 5 images = 16MB * 5 = 80MB of RAM

Now let's assume that you have other .png images, so you've already allocated 100 mbytes of RAM.

Now, GDX allocates some memory on itself (without any assets) so you have ~120 mbytes of RAM allocated.

Do you get it? :D

You've already allocated at least 120 MB of RAM. Android device can't stand this much. For example:

Hearthstone game uses ~54 MB of RAM (on Galaxy S5 and J7) and they have all of these cards graphics, particle effects etc. 100 mbs of RAM for a normal device these day is MAX.

There's only one solution for you: .png compression to .etc1. It will remove transparency from your images, but instead of 120 mbytes of ram you'll end with 30mb allocated.

Read here: https://github.com/libgdx/libgdx/wiki/Texture-Compression

Use transparent PNGs only when you really need them - transparent.equals(death).

ps. Remember. If you make your image non-transparent it won't make your compression. You have to use the LibGDX guide and use a tool like ImageMagick to convert your images to .etc format and then load them normally to an app.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you for this simple explanation about textures and RAM allocation. \$\endgroup\$
    – Neerkoli
    Nov 7, 2016 at 11:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .