第4回 「アニメーションの表示」 | エドさんのAndroid AR講座

第4回 「アニメーションの表示」


アニメーションを表示するには、GIFアニメやフラッシュ(swf)を利用するなど、いくつかの方法が考えられます。ただ、ミサイルの飛ぶ軌跡に従って各フレームを表示し、指定されたターゲットの場所で爆発させるなどの細かいコントロールを行いたい場合には、出来合いのツールを使う方法ではかえって処理が重くなってしまう恐れがあります。また、画像の各フレームを同じ大きさで作る必要があるなど、かえって画像リソースが大きくなってしまう場合もあります。そこで、ここではタイマーを使って 1 フレームずつ画像を表示するという手作りの方法を採用することにします。


アニメーションを作るにあたって、まずターゲットのライフサイクルを考えてみます。今回は、画面をタップすると、その位置をターゲットとしてミサイルが飛んでいき、到達したら消滅するという単純なモデルを設定します。


タイミング照準マークミサイル
画面タップ------ ターゲット生成 ------
⇒ ⇒ ⇒ターゲット位置に表示発射位置とターゲット位置を結ぶ
軌跡上の所定の位置に表示
ターゲットに到達消滅消滅


これを記述するために、まず 1 つのターゲットの状態を保持するためのターゲットクラスを作成します。その内容は次のようになります。


Target.java

package com.artiscc.aimnshoot;

import java.util.Timer;
import java.util.TimerTask;
import android.content.Context;

class Target
{
private AnimationTimerTask atTask = null;
private Timer at = null;
private Context main = null;
public int x;
public int y;
public int tick;
public Target(Context ctx, int x, int y)
{
this.main = ctx;
this.x = x;
this.y = y;
this.tick = -1;
}
public void setPosition(int x, int y)
{
this.x = x;
this.y = y;
}
public void startTimer()
{
this.tick = 0;
this.atTask = new AnimationTimerTask();
this.at = new Timer(true);
this.at.schedule(atTask, 100L, 100L);
}
public void cancelTimer()
{
if(this.at != null){
this.at.cancel();
this.at = null;
}
}

class AnimationTimerTask extends TimerTask
{
@Override
public void run()
{
((AimnShoot)main).tickTimer();
}
}
}


まず、クラス変数を順に見ていきましょう。


  atTask
AnimationTimerTask のインスタンスで、タイマー発火時に起動されるタスクを保持します。タイマータスクは、このクラス内の末尾に定義してあります (後述)。

  at
ターゲットごとに1つ割り当てられるタイマーです。システム全体を制御する時計は存在せず、ターゲットはそれぞれ自分のタイミングで時を刻みます。

  main
全体の制御はメインの AimnShoot クラスが行うので、タイマーが発火したときにはメインに通知する必要があります。そこでタイマータスクに通知先を渡してやるためにメインへのポインタを保持しておきます。

  x, y
ターゲットの座標です。

  tick
ターゲットにとっての現在時刻、すなわちタイマーがスタートしてからの時の刻みを保持します。


各関数の内容は次のとおりです。まず、コンストラクタでは呼び出し元と x 座標、y 座標を受け取り、クラス変数に格納します。また、タイマーはまだスタートしていないので、tick の値を -1 にしておきます。


setPosition() は、ターゲットの位置が変化したときに呼ばれる関数として用意してありますが、今回は位置が変化しないので、実際に呼ばれることはありません。


タイマーをスタートさせるには startTimer() を呼び出します。これにより、ターゲットのアニメーション (ミサイルと照準マークの動き) が開始されます。この中では、まず tick の値を 0 とし、タイマータスクとタイマーとを生成します。さらに生成されたタイマーに対して schedule() を呼び出し、発火時の動作 (タイマータスク)、発火のタイミングを設定します。第2引数は最初にいつ発火するか、第3引数はその後の発火間隔を表していて、どちらもミリ秒を表す long 型の整数で指定します。なお、タイマー生成時の true という引数は、このタイマーをデーモンとして生成するという意味で、タイマーごとに別々のスレッドを立てることを表しています。


cancelTimer() は不要になったタイマーを削除するのに用います。これを呼ばないと、タイマーはいつまでも指定された間隔で発火を続けてしまいます。


最後に、上述のタイマータスクのクラスを定義します。後の回ではタイマータスクを独立させますが、今回は構造を単純化するためにターゲットクラスの内部に定義することにします。内容は非常に単純で、タイマーが発火したときに呼ばれる run() を定義するだけですが、ここではメインで定義されている tickTimer() を呼び出し、そこからメインスレッドに制御を渡します。


次に、メインのアクティビティを説明します。


コードがだんだん長くなってきたので、前回分から変更のない部分は適宜省略します。


AimnShoot.java

package com.artiscc.aimnshoot;

import android.app.Activity;
...

public class AimnShoot extends Activity
{
...
@Override
public void onCreate(Bundle savedInstanceState)
{
(省略)
}

private Target target1;
private void attackTarget(int x, int y)
{
target1 = new Target(this, x, y);
target1.startTimer();
}
private void showNextFrame()
{
target1.tick++;
if(! mARView.showNextFrame(target1)){
target1.cancelTimer();
}
}

/******************************************************************************
* Message Handler
******************************************************************************/
protected static final int MSG_TIMER_TICKED = 1;
Handler mHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
if(isFinishing()){
return;
}
switch(msg.what){
case MSG_TIMER_TICKED:
showNextFrame();
break;
}
}
};

/******************************************************************************
* Timer Task
******************************************************************************/
public void tickTimer()
{
mHandler.sendEmptyMessage(MSG_TIMER_TICKED);
}

/******************************************************************************
* Gesture Detection
******************************************************************************/
...
class OnGestureListener extends GestureDetector.SimpleOnGestureListener
{
@Override
public boolean onSingleTapUp(MotionEvent ev)
{
attackTarget((int)ev.getX(), (int)ev.getY());
return super.onSingleTapUp(ev);
}
}
}


まず、最も重要な変更点として、メッセージハンドラに関して説明します。メッセージハンドラとは、スレッド間での要求のやりとりに用いられるもので、今回はタイマースレッドからの要求 (発火時の動作指示) をメインスレッドに伝えることを目的としています。


上述のように、ターゲットのタイマーが発火すると、タイマータスクから tickTimer() が呼ばれます。この中では、ハンドラに対し MSG_TIMER_TICKED というメッセージを発行しています。メッセージには2個までの引数 (整数値) をつけることができますが、今回は引数がないので sendEmptyMessage() という関数を用います。このメッセージは、すぐ上のハンドラの定義の前で定数 1 として宣言されています。この定数値そのものは、メッセージ間の区別がつきさえすれば任意の整数値でかまいません。


メッセージハンドラの中では、handleMessage() という関数をオーバーライドして、メッセージを受け取った場合の動作を記述します。通常、メッセージは複数種類が使われるので、switch-case を用いてそれぞれを記述していきます。今回は MSG_TIMER_TICKED だけですが、一応その枠組みに従い、showNextFrame() という関数を呼び出すようにしてあります。


さて、次に冒頭で述べたターゲットのライフサイクル (生成~消滅) について説明しましょう。


まず、末尾のジェスチャ認識部では、画面がタップされるとその座標を引数として attackTarget() を呼び出します。attackTarget() の中では、指定された位置にターゲットのインスタンスを生成し、すぐにそのタイマーをスタートさせています。今回はターゲットは1個だけなので、ターゲットのインスタンスは target1 という変数に保持しておきます。またタイマーは、上述のようにスタートの 100 ミリ秒後に最初に発火し、その後 100 ミリ秒間隔で発火を続けます。


タイマーが発火すると、メッセージハンドラから showNextFrame() が呼び出されます。ここでは、ターゲットの内部時計を 1 だけ増やし、ARView の showNextFrame() を呼び出します。その結果は通常時は true ですが、ミサイルがターゲットに到達したときは false が戻るので、ターゲットのタイマーを削除して終わります。


最後に ARView の内容を説明します。ここでも、前回と変わらない部分は適宜省略します。


ARView.java

package com.artiscc.aimnshoot;

import android.content.Context;
...

public class ARView extends View
{
private static final String TAG = "ARView";
public ARView(Context context)
{
(省略)
}

@Override
public void onDraw(Canvas canvas)
{
Bitmap bmpReticle = (statusReticle == 0) ? null : ((statusReticle == 1) ? bmpReticleOff : bmpReticleOn);
if((bmpReticle != null) && (coordReticle != null)){
canvas.drawBitmap(bmpReticle, coordReticle.x - bmpReticle.getWidth()/2, coordReticle.y - bmpReticle.getHeight()/2, null);
}
if((bmpBomb != null) && (coordBomb != null)){
canvas.drawBitmap(bmpBomb, coordBomb.x - bmpBomb.getWidth()/2, coordBomb.y - bmpBomb.getHeight()/2, null);
}
}

private Bitmap bmpBomb = null;
private Bitmap bmpReticleOn = null;
private Bitmap bmpReticleOff = null;
private void prepareImages()
{
(省略)
}

public boolean showNextFrame(Target tar)
{
boolean ret = true;
if(tar.tick > 10){
setCoordReticle(null);
setCoordBomb(null);
statusReticle = 0;
ret = false;
}else{
setCoordReticle(new Point(tar.x, tar.y));
int bombX = tar.tick * tar.x / 10;
int bombY = tar.tick * tar.y / 10;
setCoordBomb(new Point(bombX, bombY));
statusReticle = 1 + (tar.tick % 2);
}
invalidate();
return ret;
}

private int statusReticle = 0;
...
}


ここでは、まず前回の isReticleLocked の代わりに statusReticle という変数を用いて照準マークの状態を 3 つに増やし、0:表示しない、1:非ロック状態、2:ロック状態という区別を表せるようにします。ただし、今回はタイマー機能の確認用にこうしてあるだけなので、この設定は後の回でまた変わります。


showNextFrame() はターゲットを引数として受け取り、本講義冒頭のライフサイクルの表に従った処理を行います。ここでは、ターゲットの内部時計を見て、それが 10 を超えていたらターゲットを消滅させるために戻り値を false に設定します。10 以下の場合は、照準マークをターゲットの位置に置き、ミサイルを時刻によって定まる位置に設定しています。この位置は、原点 (画面の左上隅) とターゲットを結ぶ線分上を 10 等分した点を、原点からターゲットの方向に順に動くようにしてあります。また、本質的な意味はありませんが、照準マークのロック状態を時刻の偶奇によってトグルさせています。


onDraw() に関しては、statusReticle を用いるように変えただけで、本質的な変更はありません。


以上により、画面をタップすると、その位置に向かって画面の左上隅からミサイルが飛んでくる、というアプリが完成しました。




第1部 ARアプリ作成の基本

第1回: カメラ画面の表示
    画面上にカメラのプレビュー画像を表示し続けるだけのアプリで、ARの基本中の基本です。
第2回: AR画面の表示
    カメラ画面の上にAR画面を載せ、カメラ画面上にいろいろな画像を重ね合わせます。
第3回: ジェスチャの認識
    タップ、ダブルタップ、長押し、フリックなどの画面操作を認識し、対応する処理を行います。
第4回: アニメーションの表示
    タイマーを用いてミサイルが目標に向かって飛んでいく単純なアニメーションを表示します。
第5回: 複雑なアニメーションの表示
    ミサイルが当たった後の爆発や残像の表示など、複雑なアニメーションを表示します。
第6回: 複数の目標の扱い
    同時に複数個の目標を設定し、それぞれをミサイルで撃破する枠組みを導入します。
第7回: ズーム機能の導入
    ズームボタンを設置し、カメラ画面をズームして表示する機構を手作りで実装します。
第8回: ネイティブコードの利用
    ズーム機構を高速化するため、C、C++ を使ったネイティブコードを導入します。

第2部 ARアプリ作成の応用

近日アップ予定! 乞う御期待!