AndEngineでのブレーキとの戦い

最近、私たちのチームは、AndEngineエンジンでのAndroid用の2次元シューティングゲームの開発を完了しました。 その過程で、パフォーマンスの問題とエンジンのいくつかの機能を解決するための経験を積んだので、Habr読者と共有したいと思います。 シードとして、ゲームのスクリーンショットのスライスを挿入し、猫の技術的な詳細とコード例をすべて削除します。







AndEngineについては多くの情報があります。 これは、Android用の2次元ゲームを開発するための最も人気のあるエンジンの1つです。 Javaで書かれており、無料ライセンスの下で配布されており、すべてのコードはgithubで入手できます。 エンジンを選択する際に決定的になった利点のうち、注目に値するのは、グラフィックス(アニメーションスプライトを含む)の迅速なレンダリング、本格的な物理学による衝突処理(box2dを使用)、タイルタイルエディターのサポートです。



// Tiledは非常に便利なレベルエディタであり、別の記事に値します。 レベルの1つを次に示します。



2Dタイルエディター-タイル



しかし、AndEngineに戻ります。 私たちは非常に元気に始め、1か月の作業の後、いくつかのレベル、銃、モンスターを備えたプレイ可能なプロトタイプをすでに持っていました。 そして、ここで、新しいレベルをテストするときに、モンスターが集中してブレーキが滑り始めました。 問題は、合計数を予測できない物理オブジェクト(モンスター、弾丸など)を多数作成したことです(たとえば、クモの巣は数秒ごとに新しいクモを作成します)。同様に、ガベージコレクターは定期的に深刻なFPSの沈下を引き起こします。



物理を切り取る時間はなかったので、既存のコードを最適化する方法を探し始めました。 その結果、コード内の多くの問題のある場所を見つけて修正し、メモリ処理を大幅に改善しました。 さらに、問題を解決するための具体的なアプローチについて説明します。 おそらく、これらのヒントは誰かにとってささいなことのように思えるかもしれませんが、数か月前には、このような記事は多くの時間を節約してくれます。



カリング



AndEngineには、カメラの視野に入らないスプライトのレンダリングをスキップできるオプションがあります-カリング。 ゲーム画面よりもサイズが大幅に大きいレベルのゲームで実際に使用されます。 私たちの場合、カリングを含めるとパフォーマンスが大幅に向上しましたが、問題がありました。スプライトが少なくとも部分的にカメラの境界を越えるとすぐに描画されなくなります。 したがって、ゲームオブジェクトは画面の境界で突然現れたり消えたりするように見えました。



この問題を回避するために、独自の方法を使用して、レンダリングを停止する条件を決定しました。 次のようになります。



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)); }
      
      





プロファイリング後、スプライトのカメラの視野へのエントリをチェックすることも多くの時間を消費することが判明しました。 したがって、カメラクラスで独自のメソッドを作成し、全体的なパフォーマンスを大幅に向上させました。



 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; }
      
      







メモリを操作する



エフェクト、モンスター、弾丸、ボーナスを含む、絶対にすべてのクラスの新しいオブジェクトを絶えず作成することは、私たちの通常のプラクティスでした。 オブジェクトの作成中およびしばらくしてから(割り当てられたメモリがJavaマシンのガベージコレクターによって解放されるとき)、最も強力なスマートフォンでも、目立ったFPSドロップが1秒あたり数フレームまで観察されます。



この問題を解消するには、オブジェクトプール(オブジェクトプール)を使用する必要があります-オブジェクトを保存および再利用するための特別なクラスです。 レベルのロード中に、必要なすべてのゲームクラスのインスタンスが作成され、プールに配置されます。 メモリの新しい部分を割り当てる代わりに、新しいモンスターを作成する必要がある場合、「ストア」から取得します。 モンスターが殺されたとき、私たちはそれをプールに戻しました。 新しいメモリはガベージコレクタに割り当てられないため、新しいジョブはありません。



AndEngineには、プールを操作するためのクラスが含まれています。 箇条書きの例での実装を見てみましょう。 ゲームでは多くの種類の弾丸を使用するため、マルチプールを使用します。 プールを介して作成されるすべてのクラスは、PoolSpriteクラスから継承されます。



たくさんのコード
 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(); }
      
      





bulletクラスでは、すべての初期化をコンストラクターからinit()メソッドに削除します。 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); } }
      
      





すべてのプールのスーパークラスは次のようになります。

 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(); }
      
      





マルチプールを使用するコンストラクターのスーパークラス:

 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); } }
      
      





箇条書きの種類:

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





ブレットデザイナー:

 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())); } }
      
      





弾丸プールクラス:

 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; } } }
      
      





箇条書きオブジェクトの作成は次のようになります。

 SimpleBullet simpleBullet = (SimpleBullet) GameScene.getInstance().bulletConstructor.createObject(SimpleBullet.TYPE.SIMPLE.ordinal()); simpleBullet.init(targetCoords[0], targetCoords[1], mDamage, mSpeed, mOwner, mOwner.getGunSprite().getRotation() + disperse);
      
      





取り外し:

 gameScene.bulletConstructor.recycle(this);
      
      







同じ原則により、プールは残りのタイプのオブジェクト用に作成されました。 フレームレートは安定しましたが、ブレーキは各レベルの最初の数秒で弱いデバイスで開始しました。 したがって、まずプールを使用可能なオブジェクトで満たし、その後のみレベルロード画面を非表示にします。



TouchEventPoolおよびBaseTouchController



弱いスマートフォンでのゲームのプロファイリング中に、TouchEventPoolのエンジンによるメモリ割り当て中に、大幅なパフォーマンスの低下が認められました。 対応するロガーメッセージから明らかなこと:



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







そして



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







そのため、エンジンコードをわずかに変更し、最初にこれらのプールを拡張しました。 クラスorg.andengine.input.touch.TouchEventでは、コンストラクターで20個のオブジェクトを選択します。



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





また、TouchEventPool内部クラスにコストラクタを追加します。



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





org.andengine.input.touch.controller.BaseTouchControllerクラスでは、mTouchEventRunnablePoolUpdateHandlerを初期化するときに、コンストラクターに引数を追加します。



 … = new RunnablePoolUpdateHandler<TouchEventRunnablePoolItem>(<b>20</b>)
      
      





これらの操作の後、タッチに関与するクラスによるメモリの割り当ては、より控えめになりました。



フォーカスを失ったときの対処方法





これで、ゲームプレイ自体の最適化が終了し、ゲームの他の側面に移りました。 Google Play ServiceとTapjoyを接続した後、深刻な問題が発生しました。 プレーヤーがこれらのサービスの画面と対話すると、ゲームのアクティビティはフォーカスを失います。 アクティビティに戻った後、テクスチャが再ロードされます-短時間の間、すべてがフリーズします。 この問題を解決するには、メインアプリケーションアクティビティに次のコードを追加します。



 this.mRenderSurfaceView.setPreserveEGLContextOnPause(true);
      
      







占有メモリの量を減らす



一部のテクスチャでは、RGB8888の代わりにRGBA4444の切り捨てられた色範囲を使用するのが理にかなっています。 TexturePackerでは、画像形式オプションを使用してこれを行うことができます。 グラフィックパーツが少数の色のスタイルで作成されている場合(たとえば、漫画グラフィックの場合)、これによりメモリが大幅に節約され、パフォーマンスが少し向上します。



テクスチャパッカー



長いコンパイル時間



AndEngineで開発する際の最も厄介なことの1つは、コンパイルの開始からゲームのテストまでの待機時間です。 apkファイルの構築に加えて、コンピューターからAndroidデバイスにコピーする時間も必要です。 開発の終わりに、私は1分ほど待たなければなりませんでした。 この問題で多くの時間を失いました。 この点で、Unityのような他のエンジンは私たちにとって楽園のように思えました。アセンブリは非常に高速で、デスクトップで直接テストできます。 この問題は、次のゲームの開発時に行った別のエンジンに切り替えることによってのみ解決されます。



AndEngineの開発の欠如



リポジトリの最後のコミットの日付は2013年12月11日、公式ブログエントリは1月22日です。 明らかに、プロジェクトは凍結しました。



結果は何ですか?



開発の終了後、AndEngineを使用しないことを決定しました。 小さなゲームには適していますが、代替エンジンにはないいくつかの欠点があります。



最も人気のあるエンジンを比較し、libGDXを選択しました。 コミュニティは巨大で、エンジンは活発に開発されており、優れたドキュメントと多くの例があります。 大きな利点は、libGDXがJavaで記述されていることです。 デスクトップ上でゲームを構築することが可能であるため、ゲームの開発とテストは大幅に加速されます。 私は、開発がすべての一般的なモバイルプラットフォームですぐに実行されるという事実については話していない。 もちろん、微妙な違いがあり、プラットフォームごとに特定のコードを記述する必要がありますが、新しいプラットフォームの本格的な開発よりもはるかに高速で安価です。 現在、私たちはlibGDXの2番目のゲームの作業を終えており、これまでのところ私たちを幸せにしているだけです。



ご清聴ありがとうございました!



All Articles