AndEngineで写真を分割する

 アプリ作成に気合が入りません。(これ、今朝も書いたね。)
 気持ちを奮い立たせるためにも、ブログに AndEngine の Tip のようなことを書きましょう。
 
 公開から既に2ヶ月近く経とうとしているのに、有効インストール数が5件とパッとしないことこの上ないアプリ「秘密のスライドパズル」なのですが、プログラム的には相当頑張った内容となっています。
 
 このアプリでは、写真を分割してスライドパズルをリアルタイムで作っています。「写真を分割する」なんてスゴいプログラムのような気がしません?
 AndEngine でのこの処理をご紹介します。「写真」ではなく「イラスト」でもよいですし、「分割」ではなく「切り出し」でも同じことです。
device-2014-07-14-005356
 写真を分割して並べ替えるとこうなります。
 
 まず、事前に画像を読み込んだ ITextureRegion picture_region を切り出すメソッドを作ります。読み込み部分は割愛。

public ITextureRegion loadPicture(float x, float y, float width, float height){
	picture_region.set(x, y, width, height);
	return picture_region;
}

 えっ、これだけ?
 これだけです。
 
 画面に Sprite を貼る時に位置と幅高さを指定します。こんな感じ。

	// それぞれの変数は名前から推し量ってください。
	for(int i = 0 ; i < panelCountX ; i++){
		for(int j = 0 ; j < panelCountY ; j++){
			x = offsetX + (panelSize*i);
			y = offsetY + (panelSize*(panelCountY-j-1));
			picture = new Sprite(x, y, loadPicture(offsetX + (i*panelSize), offsetY + (j*panelSize), panelSize, panelSize), vbom);
			// あとでのパネル毎の処理のためにタグを付けておく。
			picture.setTag((i*panelCountY) + j);
			// 中心座標で配置するとズレる場合があるので (0, 0) 座標を基準とする。
			picture.setOffsetCenter(0, 0);
			this.attachChild(picture);
		}
	}

 あまり使い道ないか・・。

AndEngine で ClipEntity を実装する

 拙作アプリ「形か色をタップせよ!」の中で、メニューから「遊び方」ボタンを押すと
device-2014-08-16-234348
このような画面となります。
 開いたダイアログボックスの中で、遊び方を説明している画像がスワイプによって次の画像に切り替わるようになっています。ダイアログボックス内で画像がスライドして切り替わります。えぇい説明が面倒だわい。インストールして見てください。見終わったらアンインストールしていただいて結構です。
 
 この処理は ClipEntity を使っています。

			// ダイアログより少し小さめの ClipEntity を作る
			ClipEntity clipEntity = new ClipEntity(x, y, width, height);
			// ダイアログ内に配置
			clipEntity.setPosition(px, py);
			dialogEntity.attachChild(clipEntity);

			// 説明画像を貼り付ける幅広の Entity。
			Entity entity = new Entity(width + width/2, height/2, width*3, height){
				float startX;
				@Override
				public boolean onAreaTouched(final TouchEvent pAreaTouchEvent, final float x, final float y){
					if(pAreaTouchEvent.getAction() == TouchEvent.ACTION_DOWN){
						startX = x;
					}
					else if(pAreaTouchEvent.getAction() == TouchEvent.ACTION_MOVE){
					}
					else if(pAreaTouchEvent.getAction() == TouchEvent.ACTION_UP){
						float moveX = x - startX;
						float time = 0.5f;
						float curX = this.getX();
						// 左方向にスワイプしたら、次の画像に MoveXModifier で移動
						if(curX > width/2 - width && moveX < 0){
							index++; // 別の箇所で定義済み
							addressSprite.setCurrentTileIndex(index);
							MoveXModifier moveModifier = new MoveXModifier(time, curX, curX - width, EaseCircularOut.getInstance());
							moveModifier.setAutoUnregisterWhenFinished(true);
							this.registerEntityModifier(moveModifier);
						}
						// 右方向にスワイプしたら、前の画像に MoveXModifier で移動
						else if(curX < width/2 + width && moveX > 0){
							index--;
							addressSprite.setCurrentTileIndex(index);
							MoveXModifier moveModifier = new MoveXModifier(time, curX, curX + width, EaseCircularOut.getInstance());
							moveModifier.setAutoUnregisterWhenFinished(true);
							this.registerEntityModifier(moveModifier);
						}
					}
					return true;
				}
			};
			// 3枚の画像を entity に貼り付ける
			Sprite e01 = new Sprite(width/2, height/2, resourcesManager.e01_region, vbom);
			entity.attachChild(e01);
			Sprite e02 = new Sprite(width/2 + width, height/2, resourcesManager.e02_region, vbom);
			entity.attachChild(e02);
			Sprite e03 = new Sprite(width/2 + width + width, height/2, resourcesManager.e03_region, vbom);
			entity.attachChild(e03);
			// entity を clipEntity に貼り付ける
			clipEntity.attachChild(entity);
			// entity のタッチイベント取得は hud に登録
			hud.registerTouchArea(entity);

 指にくっついてくるヌルヌルとした動きではないのですが、簡易なページめくりでよいなら、ぜひ活用してください。
 適当な画像3枚を用意して、以前掲載したサンプルプロジェクトに実装して確かめてみるとよいかも知れません。

同じ処理でも backボタンからだとエラーとなる

 AndEngine をベースとしたアプリ「秘密のスライドパズル」では、最大32×57+4隅の1828もの Sprite を貼り付けています。
 これだけの数を貼り付けると Android 2.3.3を積む我が愛機の snapdragon 1GHz では、さすがにもたつきます。描画が遅れるのは、まあ、仕方ないですね。
 
 ところが、描画時ではなく Scene 遷移時の Sprite の除去時にエラーが出るのです。しかも、まったく同じ処理であっても、画面上のタッチイベントから処理した場合はエラーとならず、ハード上の back ボタンから処理した場合に ArrayList.throwIndexOutOfBoundsException error を吐くのです。
 具体的には、Scene 遷移時の detachChildren()、clearEntityModifiers()、clearTouchAreas()、clearUpdateHandlers()、detachSelf()、dispose() 処理時にエラーとなります。
 
 どゆこと?
 
 実は、以前のアプリにもこのエラーが出る場合もあったのですが、TimerHandler で処理タイミングずらして何となく解決していたのですね。
 今回は、Sprite 数が多いからでしょうか。タイミングずらしても確実にエラーとなり、根本的に解決しなければならないようです。
 
 エラーは「ArrayList のインデックスありませんよ」ですからね、何の ArrayList でしょう。
 ログを見るとEntity の onManagedDraw() での描画リストが処理時に空となっているからのようです。削除しているのに描画でのエラーとは、意表をつかれました。
 back ボタンからの処理時のみ描画リストがないということは、back ボタンの処理は UIスレッドで動いているのでしょうね。処理が多いときに完了を待たずに UI 別スレッドから描画処理が入るものだから、件のエラーが出ると・・・、全く確認していないのですが、そう間違ってもいないのでしょう(適当)。
 
 解決方法を Google 先生に聞いたところ、Andengine フォーラムに記事がありました。
 detach 処理を別スレッドにしてしまう方法とdetach 処理前に Engine を一時ロックしてしまう方法があるようです。う~む、別スレッドに入れて解決するのであれば、前段の仮定は間違っているということでしょうか?
 
 両方を試してみたところ、別スレッド処理は同じエラーを吐くようです。この解決方法は違う原因のエラーなのでしょうかね。英語がわかるようになりたいものです。
 
 兎にも角にも

		final EngineLock engineLock = mEngine.getEngineLock();
		engineLock.lock();
		gameScene.disposeScene(); // 子 Entity の削除やらをたっぷり処理
		gameScene = null;
		engineLock.unlock();

と、エンジンをロックして問題解決です。
 
 まだまだ知らないクラスが山ほどあるね。

ロングタップ後のアップ判定をどうする?

 AndEngine でアニメーションダイアログを実装した際に「ダイアログが開いた後に指を離した際、離した位置にコーラが描画されていると即座にダイアログが閉じてしまう」問題が発生したのですが、このようにして解決しました。

Sprite colaSprite = new Sprite(dialogSprite.getWidth() / 2, dialogSprite.getHeight() / 2, resourcesManager.cola_region, vbom){
	boolean isNewTouch = false;
	@Override
	public boolean onAreaTouched(final TouchEvent pAreaTouchEvent, final float x, final float y){
		if(isTouchOK){
			if(pAreaTouchEvent.getAction() == TouchEvent.ACTION_DOWN){
				isNewTouch = true;
			}
			else if(pAreaTouchEvent.getAction() == TouchEvent.ACTION_UP){
				if(isNewTouch){
					detachSelf();
					closeDialog(sec);
				}
				isNewTouch = false;
			}
		}
		return true;
	}
};

 Sprite に boolean 値 isNewTouch(カップヌードルではない)を加えて、ACTION_DOWN と ACTION_UP がセットで動作しないと detachSelf(); や closeDialog(sec); を実行しないようにしました。
 ロングタップでキャンセルされた ACTION_DOWN の後の ACTION_UP は実行されないのです。
 
 シンプルに解決できるロジックが閃いたこの瞬間が、なんとも気持ちイイ!

 プログラミングの醍醐味です。

AndEngineでアニメーションダイアログを実装する(その3)

 前回からの続きです。
 
 SplashScene の変更は、BaseScene のメソッドをオーバーライドするだけ。エラーになるからね。

	@Override
	protected void drawDialog(final float sec){}

 
 GameScene は、

	@Override
	protected void createScene(){
		// 前回分に hud の処理を追記
		hud = new HUD();
		hud.setTouchAreaBindingOnActionDownEnabled(true);
		camera.setHUD(hud);
	}

	@Override
	public void disposeScene(){
		background.detachSelf();
		background.dispose();
		sprite.detachSelf();
		sprite.dispose();
		// hud の処理を追記
		hud.clearTouchAreas();
		hud.detachSelf();
		hud.dispose();
		this.detachSelf();
		this.dispose();
	}

	@Override
	public void onHold(HoldDetector pHoldDetector, long pHoldTimeMilliseconds, int pPointerID, float pHoldX, float pHoldY){
		if(!isHold && pHoldTimeMilliseconds > holdTime){
			isHold = true;
			// BaseScene のメソッド呼び出しに変更
			openDialog(MainActivity.CAMERA_WIDTH / 2, MainActivity.CAMERA_HEIGHT / 2, MainActivity.CAMERA_WIDTH - 300, MainActivity.CAMERA_HEIGHT - 400);
		}
	}

	// ダイアログの中身をここで書く
    @Override
	protected void drawDialog(final float sec){
    	Sprite colaSprite = new Sprite(dialogSprite.getWidth() / 2, dialogSprite.getHeight() / 2, resourcesManager.cola_region, vbom){
			@Override
			public boolean onAreaTouched(final TouchEvent pAreaTouchEvent, final float x, final float y){
				if(isTouchOK){
					if(pAreaTouchEvent.getAction() == TouchEvent.ACTION_UP){
						detachSelf();
						closeDialog(sec);
					}
				}
				return true;
			}
		};
    	dialogSprite.attachChild(colaSprite);
    	hud.registerTouchArea(colaSprite);
        isTouchOK = true;
	}

 コーラの Sprite の onAreaTouched() をオーバーライドして、自身をタッチされた時に自身を消した上でダイアログを閉じるようにしています。
 
 こんな感じで、サンプルプロジェクトを改造してみました。
 解凍してコードを見るもよし、Eclpse にインポートして動かすもよし(AndEngine の参照が必要です)。
 
 
と、満足していたら、問題発生。
 ロングタップしてダイアログが開いた後に指を離した際、離した位置にコーラが描画されていると即座にダイアログが閉じてしまう。
 ダイアログが開いた後に数秒の待ちを入れるか・・それでも指を離さなければダメだし・・、開いた後の最初の ACTION_UP で isTouchOK を true にしようか・・って、それじゃタッチイベント拾えないし、ACTION_DOWN で動作させようか・・。
 
 さあ、皆さんならどうします?
 
 つらつら考えて、一応の対応策はできたのですが、次回までの練習問題として?のままとしておきます。