Recently, our team completed the development of a two-dimensional Android shooter on the AndEngine engine. In the process, some experience was gained in solving performance problems and some features of the engine that I want to share with Habr's readers. For the seed, I will insert a screenshot of the game from the game, and I will remove all the technical details and code examples under the cat.

There is a lot of information about AndEngine. This is one of the most popular engines for developing 2D games for Android. It is written in Java, distributed under a free license, and all code is available on
github . Of the goodies that have become decisive for us when choosing the engine, it is worth noting: quick drawing of graphics (including animated sprites), handling collisions with full-fledged physics (using box2d) and support for the Tiled editor.
// Tiled is a fairly convenient level editor and deserves a separate article. Here is one of our levels:
')

But back to AndEngine. We started quite cheerfully and after a month of work we already had a playable prototype with several levels, cannons and monsters. And here, when testing new levels, brakes began to slip at large clusters of monsters. The problem was that we created many physical objects (monsters, bullets, etc.) the total amount of which could not be foreseen (for example, the spider's nest creates a new spider every few seconds) and even if you allocate memory for them in advance, then all Equally the garbage collector will periodically cause strong FPS subsidence.
There was no time to cut out physics, and we started looking for ways to optimize the existing code. As a result, we found and fixed many problem areas in the code, and also significantly improved our work with memory. Then I will talk about specific approaches to solving problems. Perhaps these tips may seem trivial to someone, but a few months ago such an article would save us a lot of time.
Culling
In AndEngine there is an option that allows you to skip rendering for sprites that are not in the camera's field of view - Culling. Actual for games with levels that are much larger than the game screen. In our case, the inclusion of Culling significantly increased the speed, but a problem appeared: as soon as the sprite at least partially goes beyond the camera, it is no longer drawn. Thus it seemed that the game objects suddenly appear and disappear on the borders of the screen.
To get around this problem, we used our method to determine the conditions for termination of 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, they wrote their method in the camera class, which significantly accelerated the overall speed:
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
We had a common 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 drawdowns are observed up to several frames per second even on the most powerful smartphones.
To eliminate this problem, you need to use object pools (object pool) - a special class for storing and reusing objects. During the level load instances of all the 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 is killed, we put it back in the pool. Since the new memory is not allocated for the garbage collector, there is simply no new work.
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:
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 class of a bullet, we remove all initialization from the constructor to the init () method. Override 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<PoolSprite> { 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(); }
Superclass for constructor that uses multipool:
public abstract class ObjectConstructor { protected MultiPool<PoolSprite> 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<PoolSprite>(); 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);
Uninstall:
gameScene.bulletConstructor.recycle(this);
By the same principle, pools were created for other types of objects. The frame rate has stabilized, but the brakes started on weak devices in the first seconds of each level. Therefore, we first fill the pools with ready-to-use objects and only after that hide the level loading screen.
TouchEventPool and BaseTouchController
During the profiling of the game on weak smartphones, significant performance slowdowns were observed during the memory allocation engine in TouchEventPool. What was clear from the corresponding logger messages:
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 org.andengine.input.touch.TouchEvent class, we select 20 objects in the constructor:
private static final TouchEventPool TOUCHEVENT_POOL = new TouchEventPool(20);
And also in the internal class of TouchEventPool we add a constructor:
TouchEventPool(int size) { super(size); }
In the org.andengine.input.touch.controller.BaseTouchController class, when mTouchEventRunnablePoolUpdateHandler is initialized, we add an argument to the constructor:
… = new RunnablePoolUpdateHandler<TouchEventRunnablePoolItem>(<b>20</b>)
After these manipulations, the allocation of memory to the classes responsible for touching has become much more modest.
What to do when you lose focus
At this, the optimization of the gameplay itself ended and we switched to other aspects of the game. Serious problems appeared after connecting 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 activity, the texture is reloaded; for a short time, everything hangs. To solve this problem, add the following code in the main application activity:
this.mRenderSurfaceView.setPreserveEGLContextOnPause(true);
We reduce the amount of memory occupied
For some textures, it makes sense to use a trimmed 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), then this will significantly save memory and slightly increase the speed.

Long compilation time
One of the most annoying things when developing on AndEngine is the wait time from the start of the compilation to the testing of the game. In addition to the assembly of the apk-file, you also need time to copy it from a computer to an Android device. At the end of the development we had to wait around one minute. We have lost a lot of time on this issue. In this regard, other engines like Unity seemed like a paradise to us - the assembly takes place very quickly and can be tested immediately on the desktop. This problem is solved only by switching to another engine, which we did during the development of the next game.
The absence of the development of AndEngine
The last commit in the repository is dated December 11, 2013, the entry in the official blog is January 22. Obviously, the project is frozen.
What is the result?
After the development, we decided that we would no longer work with AndEngine. It is good for small games, but it has some drawbacks that are not found in alternative engines.
We compared the most popular engines and chose libGDX. The community is huge, engines are actively developing, good documentation + many examples. The big plus was that libGDX was written in Java. Since there is an opportunity to collect the game on the desktop, the development and testing of the game is greatly 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 development for a new platform. Now we are finishing work on the second game on libGDX and so far it only pleases us.
Thank you all for your attention!