marunomaruno-memo

marunomaruno-memo

[Android][AppInventor] MoleMash の Java 版 (1) 失敗例

2012年01月04日 | Android
[Android][AppInventor] MoleMash の Java 版 (1) 失敗例
================================================================================

AppInventor のチュートリアルのサンプル「MoleMash」を Java を使って実装してみる。
★今回のアプリケーションは失敗例★

MoleMash は以下のようなアプリ。
---
MoleMash は、ゲーム場のランダムな位置にひょっこり現れた一匹のモグラを、消えてし
まわないうちに叩いてポイントを稼ぐゲームです。
---
(ソフトウェア技術ドキュメントを勝手に翻訳 - AppInventor チュートリアル、
http://www.techdoctranslator.com/appinventor/learn/tutorials より)

ただし、オリジナルは、画面の縦横が変わっても点数が保持されているが、これは保持し
ていない。

【注意】今回のサンプルはよくないので、これをもとに実際のアプリケーションとして組
み立ててはいけない。これだと、タッチするタイミングと描画するタイミングがうまく合
わない。
実際は、SurfaceView を使って、マルチスレッドとして作るべきである(次のサンプル参
照)。


今回作ったクラスはつぎの 3 つ。

MoleMashActivity    アクティビティ
CanvasView          カスタム・ビュー(このクラスで描画)
Mole                もぐら


■ アクティビティ

□ MoleMashActivity
---
package jp.marunomaruno.android.molemash;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * アクティビティ。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class MoleMashActivity extends Activity {
    private CanvasView canvasView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onResume() {
        Log.d("TEST", "MoleMashActivity.onResume()");
        super.onResume();

        canvasView = (CanvasView) findViewById(R.id.canvasView);

    }

    /**
     * 「リセット」ボタン・クリック
     * @param v
     */
    public void onResetButtonClick(View v) {
        Log.d("TEST", String.format(
                "MoleMashActivity.onResetButtonClick() - [%s]", v.toString()));
        canvasView.resetScore();    // (1)
    }
}
---

(1) スコアのリセットの処理

    canvasView.resetScore();    // (1)


■ カスタム・ビュー

描画のキャンパスになっているカスタム・ビューのクラス。

□ CanvasView.java
---
package jp.marunomaruno.android.molemash;

import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * モグラを出現させてたたくキャンバス。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class CanvasView extends ImageView {

    private Mole mole; // モグラ // (1)

    public CanvasView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize(context);
    }

    public CanvasView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize(context);
    }

    public CanvasView(Context context) {
        super(context);
        initialize(context);
    }

    /**
     * 初期化する。
     * 
     * @param context
     */
    private void initialize(Context context) {
        // タッチ領域のスケールを指定する
        float scale = Math
                .abs((getResources().getInteger(R.integer.scaleRate) - 100) 
                        / 100F); // (2)

        // モグラを生成する
        mole = new Mole(BitmapFactory.decodeResource(context.getResources(),
                R.drawable.mole), scale); // (3)
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        setAppearancePoint(mole); // 出現座標を設定する
        canvas.drawBitmap(mole.getPicture(), mole.getX(), mole.getY(), null);

        invalidate(); // (4)

        try {
            Thread.sleep(getResources().getInteger(R.integer.appearanceMillis));
                                                                         // (5)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * もぐらの出現座標を設定する。
     * @return 座標設定後のモグラ
     */
    private Mole setAppearancePoint(Mole mole) {
        // タッチ領域のスケールを指定する
        float scale = Math
                .abs((getResources().getInteger(R.integer.scaleRate) - 100) 
                        / 100F); // (2)

        // タッチ領域がすべて画面に入るように座標を設定する
        PointF point = new PointF();
        point.x = (float) (Math.random()
                * (getWidth() - mole.getPicture().getWidth() * (1 + scale * 2)) 
                        + mole
                .getPicture().getWidth()
                * scale); // (6)
        point.y = (float) (Math.random()
                * (getHeight() - mole.getPicture().getHeight()
                        * (1 + scale * 2)) + mole.getPicture().getHeight()
                * scale);

        mole.setPoint(point);
        Log.d("TEST", mole.toString());

        return mole;
    }

    /*
     * (non-Javadoc)
     * @see android.view.View.OnTouchListener#onTouch(android.view.View,
     * android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: // (7)
            PointF touchPoint = new PointF(event.getX(), event.getY());

            // タッチされたら、加点する
            if (isTouch(touchPoint, mole)) {
                // 得点をカウントアップして表示する
                TextView scoreView = (TextView) ((Activity) getContext())
                        .findViewById(R.id.score); // (8)
                scoreView.setText(String.format("%d", mole.scoreUp())); // (9)

                // 実機を振動する
                Vibrator vibrator = (Vibrator) getContext().getSystemService(
                        Context.VIBRATOR_SERVICE); // (10)
                vibrator.vibrate(getResources().getInteger(R.integer
                        .vibMillis)); // (11)
            }

            invalidate(); // (12)
            break;

        default:
            assert true; // 何もしない
            break;

        }

        return true;
    }

    private boolean isTouch(PointF touchPoint, Mole mole) {
        RectF rect = mole.getMoleRect();

        Log.d("TEST", String.format("[%.1f, %.1f] in [%s]: %b", touchPoint.x,
                touchPoint.y, rect.toString(), rect.contains(touchPoint.x,
                        touchPoint.y)));

        return rect.contains(touchPoint.x, touchPoint.y); // (13)
    }

    /**
     * スコアをリセットする。
     */
    public void resetScore() {
        Log.d("TEST", String.format("  - RESET"));

        TextView scoreView = (TextView) ((Activity) getContext())
                .findViewById(R.id.score);
        scoreView.setText(String.format("%d", mole.resetScore())); // (14)
    }

}
---

(1) モグラのオブジェクト

    private Mole mole;    // モグラ    // (1)


(2) タッチ領域のスケールを指定する

実際の指定は、res/values/properties.xml の scaleRate の要素の値。
これは、図形(モグラ)の枠の幅・高さを何%のスケールで拡大・縮小するか、という指定
になっている。
これを、扱いやすいように、変換する。
変換後の値は、基の図形に対する追加分のスケール。

    float scale = Math
        .abs((getResources().getInteger(R.integer.scaleRate) - 100) / 100F);// (2)

100 が指定してあると、この scale は 0.0 になり、基の図形の大きさと変わらない。
200 が指定してあると、この scale は 1.0 になり、基の図形の大きさに対して、たて、
横がそれぞれの方向に 1 つ分大きくなる。すなわち、面積としては 9 倍になる。


(3) モグラを生成する

図形とスケールを指定して、モグラのオブジェクトを生成する。

    mole = new Mole(BitmapFactory.decodeResource(context.getResources(),
            R.drawable.mole), scale);    // (3)


(4) 再描画する

    invalidate();    // (4)


(5) モグラの出現時間

モグラの出現時間分、スリープする。

    Thread.sleep(getResources()
            .getInteger(R.integer.appearanceMillis));    // (5)


(6) タッチ領域がすべて画面に入るように座標を設定する
    point.x = (float) (Math.random()
          * (getWidth() - mole.getPicture().getWidth() * (1 + scale * 2)) + mole
          .getPicture().getWidth()
          * scale);    // (6)


(7)(8)(9) タッチされたら、加点する

タッチされたときに、モグラにタッチしたかどうかを判断する。

    case MotionEvent.ACTION_DOWN: // (7)


モグラにタッチされれば、得点を表示するビューに、カウントアップした得点を設定する。

得点を表示するビューは、アクティビティが持っているので、ここから findViewById() 
メソッドでそのオブジェクトの参照を取得する。
なお、このカスタム・ビューには、Activity 型でなく、そのスーパークラスである Conte
xt 型で保持しているので、 Activity 型にキャストしないと findViewById() メソッド
が使えない。

    TextView scoreView = (TextView) ((Activity) context)
            .findViewById(R.id.score);    // (8)

得点をカウントアップする。

    scoreView.setText(String.format("%d", mole.scoreUp()));    // (9)


(10)(11) 実機を振動させる

実機を振動させるには、Vibrator クラスの vibrate() メソッドを使う。
Vibrator クラスのオブジェクトは、以下のように、getSystemService() メソッドを使う。

    Vibrator vibrator = (Vibrator) context.getSystemService(
            Context.VIBRATOR_SERVICE); // (10)

vibrate() メソッドで、振動させる時間を指定する。

    vibrator.vibrate(getResources().getInteger(R.integer.vibMillis));    // (11)

□ Vibrator クラス


java.lang.Object
   +     android.os.Vibrator


□ メソッド
---
void     cancel()                             振動をやめる
boolean  hasVibrator()                        バイブレーターを持っているかどうか
void     vibrate(long[] pattern, int repeat)  パターンにしたがって振動する
void     vibrate(long milliseconds)           指定時間振動する
---


(12) 再描画

    invalidate();    // (12)


(13) タッチされた点が図形の四角形に入っているかどうかの判断

図形のタッチ領域の中に実際にタッチされた座標が入っているかどうかを判断する。
タッチ領域は、図形(モグラ)を基本にして、スケールによって調整されたものである。
タッチ領域は、Mole クラスの getMoleRect() メソッドで取得する。

    return rect.contains(touchPoint.x, touchPoint.y);    // (13)


(14) スコアをリセットする。

    scoreView.setText(String.format("%d", mole.resetScore()));    // (14)


■ データ

描画するデータを保持するモグラ・クラス。
つぎのデータを持つ。
    ・画像
    ・スコア
    ・出現ポイント
    ・タッチされる領域のスケール

スコアは、クラス変数として、すべてのモグラ・オブジェクトで共通にしている。今回は
モグラは 1 匹しか出現させないが、ゲームによっては、2 匹以上を出現させうることを
考慮して、共通で点数を持つ可能性があるので、クラス変数としている。

タッチされる領域のスケールは、本来必要ない物であるが、この失敗例では、タッチとモ
グラ出現タイミングが合わず、タッチされたことを認識する領域を拡大させているので、
このスケールも持つことにした。
今後は、ゲームの難易度の調整に使用できる。


□ Mole.java
---
package jp.marunomaruno.android.molemash;

import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.RectF;

/**
 * AppInventor のサンプル MoleMash の Java 版。
 * もぐら。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class Mole {

    private Bitmap picture;    // 画像    // (1)
    private static int score;    // スコア(すべてのモグラで共通)    // (2)
    private PointF point;    // 出現ポイント    // (3)
    private float scale;    // タッチされる領域のスケール    // (4)

    public Mole(Bitmap picture, float scale) {
        this.picture = picture;
        this.point = new PointF();
        this.scale = scale;
        resetScore();
    }

    public Bitmap getPicture() {
        return picture;
    }

    public int getScore() {
        return score;
    }

    
    public PointF getPoint() {
        return point;
    }

    public void setPoint(PointF point) {
        this.point = point;
    }

    /**
     * 出現ポイントのX座標を返す。
     * @return 出現ポイントのX座標
     */
    public float getX() {
        return point.x;
    }

    /**
     * 出現ポイントのY座標を返す。
     * @return 出現ポイントのY座標
     */
    public float getY() {
        return point.y;
    }

    /**
     * スコアをリセットする。
     * @return リセット後のスコア
     */
    public int resetScore() {
        score = 0;
        return score;
    }

    /**
     * スコアに加点する。
     * @return 加点後のスコア
     */
    public int scoreUp() {
        score++;
        return score;
    }

    /**
     * タッチ領域を返す。
     * @return タッチ領域
     */
    public RectF getMoleRect() {
        // タッチ領域を、スケールを考慮して設定する
        RectF rect = new RectF(point.x - picture.getWidth() * scale, point.y
                - picture.getHeight() * scale, point.x + picture.getWidth()
                * (1 + scale), point.y + picture.getHeight() * (1 + scale));    
                                                                    // (5)

        return rect;
    }

    @Override
    public String toString() {
        return String
                .format("Mole=[%d, [%.1f, %.1f], %.1f]", score, point.x, point.y,
 scale);
    }
}
---

(1)-(4) 変数

Mole オブジェクトが保持する変数。

    private Bitmap picture;    // 画像    // (1)
    private PointF point;    // 出現ポイント    // (3)
    private float scale;    // タッチされる領域のスケール    // (4)

スコアは、今後、モールの数が増えて表示させたときに備えて、static としておく。

    private static int score;    // スコア(すべてのモグラで共通)    // (2)


(5) タッチ領域を、スケールを考慮して設定する

図形の描画領域を中心に、このスケール分を追加した領域を設定する。

    RectF rect = new RectF(point.x - picture.getWidth() * scale, point.y
            - picture.getHeight() * scale, point.x + picture.getWidth()
            * (1 + scale), point.y + picture.getHeight() * (1 + scale));  // (5)

scale = 0.0 のときは、 (x, y, x + w, y + h) の領域、すなわち、図形の大きさ自身
scale = 1.0 のときは、 (x - w, y - h, x + w * 2, y + h * 2) の領域、すなわち、図
形 9 つ分の大きさ


■ レイアウト

□ res/layout/main.xml
---

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    
    <jp.marunomaruno.android.molemash.CanvasView
        android:id="@+id/canvasView"
        android:layout_width="300px"
        android:layout_height="300px" />

    <TextView
        android:id="@+id/score"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <Button
        android:id="@+id/resetButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onResetButtonClick"
        android:text="@string/resetButton" />

</LinearLayout>
---


■ 設定データ

□ res/values/strings.xml
---

<resources>
    <string name="app_name">MoleMash</string>
    <string name="resetButton">リセット</string>
</resources>
---


出現時間などを設定値として記述する。

□ res/values/properties.xml
---

<resources>
    

    <integer name="appearanceMillis">1000</integer>    
        
    
    <integer name="scaleRate">200</integer>    

    
    <integer name="vibMillis">300</integer>    
</resources>
---

(1) 出現時間(ms)

    <integer name="appearanceMillis">1000</integer>    

1000 は、デバッグ用。リリース用は 500 にする。

        
(2) タッチする領域の大きさ(基絵に対する%)

    <integer name="scaleRate">200</integer>    

基本図形に対して上下左右に1つ分拡大する。すなわち、9 倍の大きさ。


(3) 振動させる時間(ミリ秒)

    <integer name="vibMillis">300</integer>    

得点したときに実機を振動させる時間。


■ マニュフェスト

実機を振動させるためには、許可が必要。

□ AndroidManifest.xml
---

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.marunomaruno.android.molemash"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />
    <uses-permission android:name="android.permission.VIBRATE"/>    

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".MoleMashActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
---

(1) 実機の振動を許可

    <uses-permission android:name="android.permission.VIBRATE"/>