How we fought brakes in AndEngine

Recently, our team has completed the development of a two-dimensional shooter-shooter for Android on the AndEngine engine. In the process, certain experience was gained on solving performance problems and some features of the engine, which I would like to share with Habr readers. For the seed I’ll insert a slice of the screenshot from the game, and I’ll remove all technical details and code examples for the cat.



There is a lot of information about AndEngine since This is one of the most popular engines for developing two-dimensional games for Android. It is written in Java, distributed under a free license and all code is available on github. Of the goodies that became decisive for us when choosing an engine, it is worth noting: quick rendering of graphics (including animated sprites), collision handling with full-fledged physics (using box2d), and support for the Tiled tile editor.

// Tiled is just a pretty convenient level editor and deserves a separate article. Here is one of our levels:

2d Tile editor - Tiled

But back to AndEngine. We started quite cheerfully and after a month of work we already had a playable prototype with several levels, guns and monsters. And here, when testing new levels, brakes began to slip with large concentrations of monsters. The problem turned out to be that we created many physical objects (monsters, bullets, etc.) whose total number could not be predicted (for example, a spider’s nest creates a new spider every few seconds) and even if we allocate memory for them in advance, then all equally, the garbage collector will periodically cause severe FPS subsidence.

There was no time to cut out physics and we started looking for ways to optimize existing code. As a result, we found and fixed many problematic places in the code, as well as significantly improved memory handling. Further I will talk about specific approaches to solving problems. Perhaps these tips will seem trite to someone, but a few months ago such an article would save us a lot of time.

Culling


AndEngine has an option that allows you to skip rendering for sprites that do not fall into the field of view of the camera - Culling. Actual for games with levels that are significantly larger in size than the game screen. In our case, the inclusion of Culling significantly increased performance, but there was a problem: as soon as the sprite at least partially goes beyond the boundaries of the camera, it is no longer drawn. Thus, it seemed that game objects suddenly appear and disappear at the borders of the screen.

To get around this problem, we used our own method to determine the conditions for stopping rendering. It looks like this:

private void optimize() {
    	  setVisible(RectangularShapeCollisionChecker.isVisible(new Camera(ResourcesManager.getInstance().camera.getXMin() - mFullWidth,
                ResourcesManager.getInstance().camera.getYMin() - mFullHeight,
                ResourcesManager.getInstance().camera.getWidth() + mFullWidth,
                ResourcesManager.getInstance().camera.getHeight() + mFullHeight), this));
}

After profiling, it turned out that checking the sprite's entry into the camera's field of view also consumes a lot of time. Therefore, we wrote our own method in the camera class, which significantly accelerated the overall performance:

public boolean contains(int pX, int pY, int pW, int pH) {
        int w = (int) this.getWidth() + pW * 2;
        int h = (int) this.getHeight() + pH * 2;
        if ((w | h | pW | pH) < 0) {
            return false;
        }
        int x = (int) this.getXMin() - pW;
        int y = (int) this.getYMin() - pH;
        if (pX < x || pY < y) {
            return false;
        }
        w += x;
        pW += pX;
        if (pW <= pX) {
            if (w >= x || pW > w) return false;
        } else {
            if (w >= x && pW > w) return false;
        }
        h += y;
        pH += pY;
        if (pH <= pY) {
            if (h >= y || pH > h) return false;
        } else {
            if (h >= y && pH > h) return false;
        }
        return true;
    }


Work with memory


It was our usual practice to constantly create new objects for absolutely all classes, including effects, monsters, bullets, bonuses. During the creation of objects and after some time (when the allocated memory will be freed by the garbage collector of the Java machine), noticeable FPS drops are observed up to several frames per second even on the most powerful smartphones.

To eliminate this problem, you need to use object pools - a special class for storing and reusing objects. During level loading, instances of all necessary game classes are created and placed in pools. When you need to create a new monster, instead of allocating a new portion of memory, we get it from the “storage”. When the monster was killed, we put it back in the pool. Since new memory is not allocated to the garbage collector, there is simply no new job.

AndEngine includes a class for working with pools. Let's look at its implementation on the example of bullets. Since the game uses many types of bullets, we will use MultiPool. All classes that are created through the pool are inherited from the PoolSprite class:

A lot of code
public abstract class PoolSprite extends AnimatedSprite {
	public int poolType;
	public PoolSprite(float pX, float pY, ITiledTextureRegion pTextureRegion, VertexBufferObjectManager pVertexBufferObjectManager) {
    	super(pX, pY, pTextureRegion, pVertexBufferObjectManager);
	}
	public abstract void onRemoveFromWorld();
}

In the bullet class, we remove all initialization from the constructor into the init () method. Overriding onRemoveFromWorld ():
@Override
	public void onRemoveFromWorld() {
    	try {
        	mBody.setActive(false);
        	mBody.setAwake(false);
        	mPhysicsWorld.unregisterPhysicsConnector(mBulletConnector);
        	mPhysicsWorld.destroyBody(mBody);
        	detachChildren();
        	detachSelf();
        	mIsAlive = false;
    	} catch (Exception e) {
        	Log.e("Bullet", "Recycle Exception", e);
    	} catch (Error e) {
        	Log.e("Bullet", "Recycle Error", e);
    	}
}

The superclass for all pools looks like this:
public abstract class ObjectPool extends GenericPool {
	protected int type;
	public ObjectPool(int pType) {
    	type = pType;
	}
	@Override
	protected void onHandleRecycleItem(final PoolSprite pObject) {
    	pObject.onRemoveFromWorld();
	}
	@Override
	protected void onHandleObtainItem(final PoolSprite pBullet) {
    	pBullet.reset();
	}
	@Override
	protected PoolSprite onAllocatePoolItem() {
    	return getType();
	}
	public abstract PoolSprite getType();
}

A superclass for a constructor that uses a multipool:
public abstract class ObjectConstructor {
	protected MultiPool pool;
	public ObjectConstructor() {
	}
	public PoolSprite createObject(int type) {
    	return this.pool.obtainPoolItem(type);
	}
	public void recycle(PoolSprite poolSprite) {
    	this.pool.recyclePoolItem(poolSprite.poolType, poolSprite);
	}
}

Types of bullets:
public static enum TYPE {
    	SIMPLE, ZOMBIE, LASER, BFG, ENEMY_ROCKET, FIRE, GRENADE, MINE, WEB, LAUNCHER_GRENADE
	}

Bullet Designer:
public class BulletConstructor extends ObjectConstructor {
	public BulletConstructor() {
    	this.pool = new MultiPool();
        this.pool.registerPool(SimpleBullet.TYPE.SIMPLE.ordinal(), new BulletPool(SimpleBullet.TYPE.SIMPLE.ordinal()));
        this.pool.registerPool(SimpleBullet.TYPE.ZOMBIE.ordinal(), new BulletPool(SimpleBullet.TYPE.ZOMBIE.ordinal()));
        this.pool.registerPool(SimpleBullet.TYPE.LASER.ordinal(), new BulletPool(SimpleBullet.TYPE.LASER.ordinal()));
    	this.pool.registerPool(SimpleBullet.TYPE.BFG.ordinal(), new BulletPool(SimpleBullet.TYPE.BFG.ordinal()));
        this.pool.registerPool(SimpleBullet.TYPE.ENEMY_ROCKET.ordinal(), new BulletPool(SimpleBullet.TYPE.ENEMY_ROCKET.ordinal()));
    	this.pool.registerPool(SimpleBullet.TYPE.FIRE.ordinal(), new BulletPool(SimpleBullet.TYPE.FIRE.ordinal()));
        this.pool.registerPool(SimpleBullet.TYPE.GRENADE.ordinal(), new BulletPool(SimpleBullet.TYPE.GRENADE.ordinal()));
    	this.pool.registerPool(SimpleBullet.TYPE.MINE.ordinal(), new BulletPool(SimpleBullet.TYPE.MINE.ordinal()));
        this.pool.registerPool(SimpleBullet.TYPE.WEB.ordinal(), new BulletPool(SimpleBullet.TYPE.WEB.ordinal()));
        this.pool.registerPool(SimpleBullet.TYPE.LAUNCHER_GRENADE.ordinal(), new BulletPool(SimpleBullet.TYPE.LAUNCHER_GRENADE.ordinal()));
	}
}

Bullet Pool Class:
public class BulletPool extends ObjectPool {
	public BulletPool(int pType) {
    	super(pType);
	}
	public PoolSprite getType() {
    	switch (this.type) {
        	case 0:
            	return new SimpleBullet();
        	case 1:
            	return new ZombieBullet();
        	case 2:
            	return new LaserBullet();
        	case 3:
            	return new BfgBullet();
        	case 4:
            	return new EnemyRocket();
        	case 5:
            	return new FireBullet();
        	case 6:
            	return new Grenade();
        	case 7:
            	return new Mine();
        	case 8:
            	return new WebBullet();
        	case 9:
            	return new Grenade(ResourcesManager.getInstance().grenadeBulletRegion);
        	default:
            	return null;
    	}
	}
}

Creating a bullet object looks like this:
SimpleBullet simpleBullet = (SimpleBullet) GameScene.getInstance().bulletConstructor.createObject(SimpleBullet.TYPE.SIMPLE.ordinal());
simpleBullet.init(targetCoords[0], targetCoords[1], mDamage, mSpeed, mOwner, mOwner.getGunSprite().getRotation() + disperse);

Removal:
gameScene.bulletConstructor.recycle(this);


By the same principle, pools were created for the remaining types of objects. The frame rate stabilized, but the brakes started on weak devices in the first seconds of each level. Therefore, we first fill the pools with objects ready for use and only after that we hide the level loading screen.

TouchEventPool and BaseTouchController


During the profiling of the game on weak smartphones, significant performance slowdowns were noticed during the memory allocation by the engine in TouchEventPool. What was clear from the corresponding messages of the logger:

TouchEventPool was exhausted, with 2 item not yet recycled. Allocated 1 more.

and

org.andengine.util.adt.pool.PoolUpdateHandler$1 was exhausted, with 2 item not yet recycled. Allocated 1 more.

therefore, we slightly changed the engine code and initially expanded these pools. In the class org.andengine.input.touch.TouchEvent, we select 20 objects in the constructor:

private static final TouchEventPool TOUCHEVENT_POOL = new TouchEventPool(20);

And also in the inner class TouchEventPool add a costructor:

TouchEventPool(int size) {
	super(size);
}

In the org.andengine.input.touch.controller.BaseTouchController class, when initializing mTouchEventRunnablePoolUpdateHandler, we add an argument to the constructor:

… = new RunnablePoolUpdateHandler(20)

After these manipulations, the allocation of memory by the classes responsible for touching became much more modest.

What to do when you lose focus



On this, the optimization of the gameplay itself ended and we moved on to other aspects of the game. Serious problems appeared after connecting the Google Play Service and Tapjoy. When a player interacts with the screens of these services, the activity of the game loses focus. After returning to the activity, the texture is reloaded - for a short time everything freezes. To solve this problem, add the following code in the main application activity:

this.mRenderSurfaceView.setPreserveEGLContextOnPause(true);


Reduce the amount of occupied memory


For some textures, it makes sense to use a truncated color range: RGBA4444 instead of RGB8888. TexturePacker allows you to do this through the Image format option. If the graphic part is made in the style with a small number of colors (for example, for cartoon graphics), this will significantly save memory and slightly increase performance.

Texture packer

Long compilation time


One of the most annoying things when developing on AndEngine is the wait time from the start of compilation to testing the game. In addition to building the apk file, you also need time to copy it from the computer to the Android device. At the end of development, I had to wait in the region of one minute. We lost a lot of time on this issue. In this regard, other engines like Unity seemed like a paradise to us - the assembly is very fast and you can test it right on the desktop. This problem is solved only by switching to another engine, which we did when developing the next game.

The lack of development of AndEngine


The last commit in the repository is dated December 11, 2013, the official blog entry is January 22. Obviously, the project froze.

What is the result?


After the end of development, we decided that we would no longer work with AndEngine. It is good for small games, but it has some disadvantages that are not in alternative engines.

We compared the most popular engines and chose libGDX. The community is huge, the engine is actively developing, good documentation + many examples. The big plus was that libGDX was written in Java. Since it is possible to build a game on desktops, the development and testing of the game is significantly accelerated. I'm not talking about the fact that the development is carried out immediately on all popular mobile platforms. Of course, there are some nuances and you will need to write some specific code for each platform, but it is much faster and cheaper than full-fledged development for the new platform. Now we are finishing work on the second game on libGDX and so far it only makes us happy.

Thank you all for your attention!

Also popular now: