marunomaruno-memo

marunomaruno-memo

[Android] インベーダー・ゲームを作る (1) シンプル

2012年04月05日 | Android
[Android] インベーダー・ゲームを作る (1) シンプル
================================================================================

インベーダー・ゲームを作ってみる。

ゲームの概要:
・画面上部にインベーダーがあり、これが左右に動く(端まで行ったら折り返す程度)
・画面下部に砲台があり、ユーザーのタッチムーブ(ドラッグ)によって左右に動く
・画面をタッチすると、砲台から弾が上部向けてに発射する
・弾がインベーダーに当たると得点する

ゲーム自体は、ImageView クラスを継承したクラス CanvasView を作って、それの上で行
う。


▲ アクティビティ

□ Invader01Activity.java
---
package jp.marunomaruno.android.invader;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class InvaderActivity extends Activity {

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

        TextView scoreView = (TextView) findViewById(R.id.score);
        CanvasView canvasView = (CanvasView) findViewById(R.id.canvasView);
        canvasView.setScoreView(scoreView);
    }
}
---


▲ ビュー

ゲーム自体は、ImageView クラスを継承したクラス CanvasView を作って、それの上で行
う。

CanvasView は以下のようなメンバーからなる。

定数
---
int     BLOCK_SIZE          描画のときのブロックの大きさ
int     BULLET_COLOR        弾の色
int     BULLET_HEIGHT       弾の描画高
int[][] BULLET_IMAGE        弾の図形イメージ
int     BULLET_MOVING_UNIT  弾の移動する単位(ピクセル)
int     BULLET_WIDTH        弾の描画幅
int     DIRECTION_LEFT      移動方向 左
int     DIRECTION_RIGHT     移動方向 右
int     DRAWING_INTERVAL    描画間隔(ミリ秒)
int     GUN_COLOR           砲台の色
int     GUN_HEIGHT          砲台の描画高
int[][] GUN_IMAGE           砲台の図形イメージ
int     GUN_MOVING_UNIT     弾の移動する単位(ピクセル)
int     GUN_WIDTH           砲台の描画幅
int     INVADER_COLOR       正常時のインベーダーの色
int     INVADER_HEIGHT      インベーダーの描画高
int     INVADER_HITL_COLOR  当たったときのインベーダーの色
int[][] INVADER_IMAGE       インベーダーの図形イメージ
int     INVADER_MOVING_UNIT インベーダーの移動する単位(ピクセル)
int     INVADER_WIDTH       インベーダーの描画幅
int     NOT_VISIBLE         非表示
---

フィールド
---
private  int       bulletPointX        弾の x 座標
private  int       bulletPointY        弾の y 座標
private  int       gunPointX           砲台の x 座標
private  int       gunPointY           砲台の y 座標
private  int       invaderColor        インベーダーの表示色
private  int       invaderMovingUnit   インベーダーの移動方向
private  int       invaderPointX       インベーダーの x 座標
private  int       invaderPointY       インベーダーの y 座標
private  int       score               得点
private  TextView  scoreView           得点を表示するビュー
---

public メソッド
---
TextView getScoreView()                   scoreView を取得する。
void     setScoreView(TextView scoreView) scoreView を設定する。
void     shot()                           弾を発射する。
---

private メソッド
---
void  clearScreen(Canvas canvas)           canvasをクリアする。
void  drawBullet(Canvas canvas, int x, int y, int color)  弾を描画する。
void  drawGun(Canvas canvas, int x, int y, int color)     砲台を描画する。
void  drawImage(Canvas canvas, Paint paint, int x, int y, int[][] image)
                                                          図形を描画する。
void  drawInvader(Canvas canvas, int x, int y, int color) インベーダーを描画する。
void  drawScore(int score)                 スコアを表示する。
void  init()                               すべてのコンストラクターで共通の処理。
boolean isBorder(int x, int width)         端かどうかを判断する。
boolean isHit()                            端が当たったかどうかをを判断する。
void  moveGun(int x)                       砲台の位置をずらす。
---

オーバーライドしているメソッド
---
void    onDraw(Canvas canvas) 
void    onSizeChanged(int w, int h, int oldw, int oldh) 
boolean onTouchEvent(MotionEvent event) 
---


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

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * インベーダー・ゲーム。
 *
 * @author marunomaruno
 * @version 1.0, 2012-03-06
 */
/**
 * @author marunomaruno
 *
 */
public class CanvasView extends ImageView {

    // 描画イメージ
    /**
     * 描画のときのブロックの大きさ
     */
    public static final int BLOCK_SIZE = 20; // (1)
    /**
     * 非表示
     */
    public static final int NOT_VISIBLE = -1;
    /**
     * 描画間隔(ミリ秒)
     */
    public static final int DRAWING_INTERVAL = 50;

    // インベーダー関係
    /**
     * インベーダーの図形イメージ
     */
    public static final int[][] INVADER_IMAGE = {
            { 0, 0, 0, 1, 0, 1, 0, 0, 0 },
            { 1, 0, 1, 1, 1, 1, 1, 0, 1 },
            { 1, 1, 1, 0, 1, 0, 1, 1, 1 },
            { 0, 0, 1, 1, 1, 1, 1, 0, 0 },
            { 0, 0, 1, 1, 0, 1, 1, 0, 0 },
            { 0, 0, 1, 0, 0, 0, 1, 0, 0 },
    };    // (2)

    /**
     * インベーダーの描画幅
     */
    public static final int INVADER_WIDTH = INVADER_IMAGE[0].length
            * BLOCK_SIZE;// (3)
    /**
     * インベーダーの描画高
     */
    public static final int INVADER_HEIGHT = INVADER_IMAGE.length * BLOCK_SIZE;// (4)

    /**
     * 正常時のインベーダーの色
     */
    public static final int INVADER_COLOR = Color.GREEN;
    /**
     * 当たったときのインベーダーの色
     */
    public static final int INVADER_HITL_COLOR = Color.RED;

    /**
     * インベーダーの移動する単位(ピクセル)
     */
    public static final int INVADER_MOVING_UNIT = 1;
    /**
     * 移動方向 左
     */
    public static final int DIRECTION_LEFT = -1;
    /**
     * 移動方向 右
     */
    public static final int DIRECTION_RIGHT = 1;

    // 砲台関係
    /**
     * 砲台の図形イメージ
     */
    public static final int[][] GUN_IMAGE = {
            { 0, 0, 1, 1, 0, 0 },
            { 1, 1, 1, 1, 1, 1 },
    };

    /**
     * 砲台の描画幅
     */
    public static final int GUN_WIDTH = GUN_IMAGE[0].length * BLOCK_SIZE;
    /**
     * 砲台の描画高
     */
    public static final int GUN_HEIGHT = GUN_IMAGE.length * BLOCK_SIZE;

    /**
     * 弾の移動する単位(ピクセル)
     */
    public static final int GUN_MOVING_UNIT = 5;

    /**
     * 砲台の色
     */
    public static final int GUN_COLOR = Color.BLUE;

    // 弾関係
    /**
     * 弾の図形イメージ
     */
    public static final int[][] BULLET_IMAGE = {
            { 1 },
    };

    /**
     * 弾の描画幅
     */
    public static final int BULLET_WIDTH = BULLET_IMAGE[0].length * BLOCK_SIZE;
    /**
     * 弾の描画高
     */
    public static final int BULLET_HEIGHT = BULLET_IMAGE.length * BLOCK_SIZE;

    /**
     * 弾の移動する単位(ピクセル)
     */
    public static final int BULLET_MOVING_UNIT = 5;

    /**
     * 弾の色
     */
    public static final int BULLET_COLOR = Color.GREEN;

    /**
     * 得点
     */
    private int score = 0;

    // インベーダーの設定
    /**
     * インベーダーの x 座標
     */
    private int invaderPointX = 0;
    /**
     * インベーダーの y 座標
     */
    private int invaderPointY = 0;
    /**
     *  インベーダーの移動方向
     */
    private int invaderMovingUnit = DIRECTION_RIGHT * INVADER_MOVING_UNIT; // (5)
    /**
     * インベーダーの表示色
     */
    private int invaderColor = INVADER_COLOR;

    // 砲台の設定
    /**
     * 砲台の x 座標
     */
    private int gunPointX;
    /**
     * 砲台の y 座標
     */
    private int gunPointY;

    // 弾の設定
    /**
     * 弾の x 座標
     */
    private int bulletPointX = NOT_VISIBLE; // 弾は最初は非表示
    /**
     * 弾の y 座標
     */
    private int bulletPointY = NOT_VISIBLE; // 弾は最初は非表示

    /**
     * 得点を表示するビュー
     */
    private TextView scoreView;

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

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

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

    /**
     * すべてのコンストラクターで共通の処理。
     */
    private void init() {
        score = 0; // 得点

        // インベーダーの設定
        invaderPointX = 0;
        invaderPointY = 0;
        invaderMovingUnit = DIRECTION_RIGHT * INVADER_MOVING_UNIT; 
                                                    // インベーダーの移動方向
        invaderColor = INVADER_COLOR;

        // 弾の設定
        bulletPointX = NOT_VISIBLE; // 弾は最初は非表示
        bulletPointY = NOT_VISIBLE; // 弾は最初は非表示

        System.out.printf("Canvas: (%d, %d)%n", getWidth(), getHeight()); // (6)
        System.out.printf("Invader: (%d, %d)%n", invaderPointX, invaderPointY);
        System.out.printf("Gun: (%d, %d)%n", gunPointX, gunPointY);
        System.out.printf("Bullet: (%d, %d)%n", bulletPointX, bulletPointY);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {    // (7)
        super.onSizeChanged(w, h, oldw, oldh);
        System.out.printf("onSizeChanged: (%d, %d)%n", w, h);

        // 砲台の設定
        gunPointX = (w - GUN_WIDTH) / 2;
        gunPointY = h - GUN_HEIGHT;

        System.out.printf("Gun: (%d, %d)%n", gunPointX, gunPointY);
    }

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

        // 画面をクリアする
        clearScreen(canvas);

        // インベーダーを表示する
        drawInvader(canvas, invaderPointX, invaderPointY, invaderColor);
        invaderColor = INVADER_COLOR;

        // 弾を表示する
        drawBullet(canvas, bulletPointX, bulletPointY, BULLET_COLOR);

        // 砲台を表示する
        drawGun(canvas, gunPointX, gunPointY, GUN_COLOR);

        // インベーダーを指定方向に移動する
        invaderPointX = invaderPointX + invaderMovingUnit;

        // 左右の端にぶつかったら反転する
        if (isBorder(invaderPointX, INVADER_WIDTH)) {
            invaderMovingUnit = -invaderMovingUnit;
        }

        // 弾を移動する
        bulletPointY = bulletPointY - BULLET_MOVING_UNIT;

        // 弾が当たったら得点する
        if (isHit()) {
            score++;
            invaderColor = INVADER_HITL_COLOR;
            bulletPointX = NOT_VISIBLE; // 弾の表示を消す
            bulletPointY = NOT_VISIBLE; // 弾の表示を消す
            // 得点を表示する
            drawScore(score);
        }

        // 指定ミリ秒分ウエイトする
        try {
            Thread.sleep(DRAWING_INTERVAL);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: // 指をタッチした
            assert true; // 何もしない
            break;

        case MotionEvent.ACTION_MOVE: // 指を動かしている    // (8)
            moveGun((int) event.getX());
            break;

        case MotionEvent.ACTION_UP: // 指を離した    // (9)
            shot();
            break;

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

        invalidate();

        return true;
    }

    /**
     * 端が当たったかどうかをを判断する。
     *
     * @return 当たったときは true
     */
    private boolean isHit() {
        return bulletPointY >= 0    // 弾が表示されている
              && (bulletPointY + BULLET_HEIGHT < invaderPointY + INVADER_HEIGHT)
              && (invaderPointX < bulletPointX)
              && (bulletPointX + BULLET_WIDTH < invaderPointX + INVADER_WIDTH); 
                                                                       // (10)
    }

    /**
     * 端かどうかを判断する。
     *
     * @param x
     *            X座標
     * @param width
     *            図形の幅
     * @return 端のときは true
     */
    private boolean isBorder(int x, int width) {
        return (x < 0) || (x > getWidth() - width);
    }

    /**
     * canvasをクリアする。
     */
    private void clearScreen(Canvas canvas) {
        invalidate();
    }

    /**
     * インベーダーを描画する。
     *
     * @param x
     *            X座標
     * @param y
     *            Y座標
     * @param color
     *            図形の色
     */
    private void drawInvader(Canvas canvas, int x, int y, int color) {
        Paint paint = new Paint();
        paint.setColor(color);
        drawImage(canvas, paint, x, y, INVADER_IMAGE);
    }

    /**
     * 弾を描画する。
     *
     * @param x
     *            X座標
     * @param y
     *            Y座標
     * @param color
     *            図形の色
     */
    private void drawBullet(Canvas canvas, int x, int y, int color) {
        Paint paint = new Paint();
        paint.setColor(color);
        drawImage(canvas, paint, x, y, BULLET_IMAGE);
    }

    /**
     * 砲台を描画する。
     *
     * @param x
     *            X座標
     * @param y
     *            Y座標
     * @param color
     *            図形の色
     */
    private void drawGun(Canvas canvas, int x, int y, int color) {
        Paint paint = new Paint();
        paint.setColor(color);
        drawImage(canvas, paint, x, y, GUN_IMAGE);
    }

    /**
     * 図形を描画する。
     *
     * @param x
     *            X座標
     * @param y
     *            Y座標
     * @param image
     *            図形を表現した2次元配列
     */
    private void drawImage(Canvas canvas, Paint paint, int x, int y,
            int[][] image) {
        if (x == NOT_VISIBLE || y == NOT_VISIBLE) {
            return;
        }

        for (int i = 0; i < image.length; i++) {
            for (int j = 0; j < image[0].length; j++) {
                if (image[i][j] == 1) {
                    canvas.drawRect(x + j * BLOCK_SIZE, y + i * BLOCK_SIZE, x
                            + j * BLOCK_SIZE + BLOCK_SIZE, y + i * BLOCK_SIZE
                            + BLOCK_SIZE, paint);    // (11)
                }
            }
        }
    }

    /**
     * スコアを表示する。
     *
     * @param score
     *            スコア
     */
    private void drawScore(int score) {
        assert scoreView != null;

        scoreView.setText(Integer.toString(score));
    }

    /**
     * 弾を発射する。
     */
    public void shot() {
        bulletPointX = gunPointX + (GUN_WIDTH - BULLET_WIDTH) / 2;
        bulletPointY = gunPointY;
    }

    /**
     * 砲台の位置をずらす。
     *
     * @param x
     *            位置
     */
    private void moveGun(int x) {
        gunPointX = Math.max(Math.min(x, getWidth() - GUN_WIDTH), 0);
    }

    /**
     * scoreView を取得する。
     * @return scoreView オブジェクト
     */
    public TextView getScoreView() {
        return scoreView;
    }

    /**
     * scoreView を設定する。
     * @param scoreView
     */
    public void setScoreView(TextView scoreView) {
        this.scoreView = scoreView;
    }

}
---

(1)(2)(11) 図形の描画

ドット絵のように、単位となる四角(ブロック)を決めて、それの組み合わせで描画する。

単位となる四角の大きさを 20 と決める。

    public static final int BLOCK_SIZE = 20; // (1)

図形イメージは、2次元配列で定義する。このブロックを描画するところが 1 に設定して
いる。

    public static final int[][] INVADER_IMAGE = { // インベーダーの図形イメージ
			{ 0, 0, 0, 1, 0, 1, 0, 0, 0 },
			{ 1, 0, 1, 1, 1, 1, 1, 0, 1 },
			{ 1, 1, 1, 0, 1, 0, 1, 1, 1 },
			{ 0, 0, 1, 1, 1, 1, 1, 0, 0 },
			{ 0, 0, 1, 1, 0, 1, 1, 0, 0 },
			{ 0, 0, 1, 0, 0, 0, 1, 0, 0 },
    };    // (2)

ブロックの2次元配列を使って、描画するのは、drawRect() を使う。左上の座標が 
(x, y) になっている。

    canvas.drawRect(x + j * BLOCK_SIZE, y + i * BLOCK_SIZE, x
            + j * BLOCK_SIZE + BLOCK_SIZE, y + i * BLOCK_SIZE
            + BLOCK_SIZE, paint);    // (11)


(3)(4) 図形の幅と高さ

描画する図形の幅と高さを、BLOCK_SIZE と、図形の2次元配列から算出する。

    public static final int INVADER_WIDTH = INVADER_IMAGE[0].length
            * BLOCK_SIZE; // インベーダーの描画幅    // (3)
    public static final int INVADER_HEIGHT = INVADER_IMAGE.length * BLOCK_SIZE;
                                            // インベーダーの描画高    // (4)



(5) インベーダーの移動単位

移動方向(DIRECTION_RIGHT) と、移動単位(INVADER_MOVING_UNIT) を使って決める。
インベーダーは、最初は画面左上から出現して、右方向に移動する。

    private int invaderMovingUnit = DIRECTION_RIGHT * INVADER_MOVING_UNIT; // (5)


(6)(7) キャンバスのサイズ

コンストラクターの中からなので、この時点では幅・高さともに 0 である。

    System.out.printf("Canvas: (%d, %d)%n", getWidth(), getHeight());    // (6)

キャンバスの実際の幅・高さは、このオブジェクトが生成されないとできない。このため、

    protected void onSizeChanged(int w, int h, int oldw, int oldh) {    // (7)

をオーバーライドして設定する。onSizeChanged() メソッドは、View のサイズが変更さ
れたときに、呼び出されるメソッド。

この onSizeChanged() を利用して、砲台の初期位置を設定する。
砲台は、画面下部の中央に出るようにする。


(8) 指の動きにあわせて砲台を移動する

    case MotionEvent.ACTION_MOVE: // 指を動かしている    // (8)
        moveGun((int) event.getX());


(9) 指を離したら、弾を発射する

    case MotionEvent.ACTION_UP: // 指を離した    // (9)
        shot();


(10) インベーダー枠の中に弾の枠が入ったらあたったことにする

    return bulletPointY >= 0    // 弾が表示されている
        && (bulletPointY + BULLET_HEIGHT < invaderPointY + INVADER_HEIGHT)
        && (invaderPointX < bulletPointX)
        && (bulletPointX + BULLET_WIDTH < invaderPointX + INVADER_WIDTH); // (10)


▲ レイアウト

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<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.invader.CanvasView
        android:id="@+id/canvasView"
        android:layout_width="match_parent"
        android:layout_height="400px" />

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/scoreTitle"
            android:textAppearance="?android:attr/textAppearanceMedium" />

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

</LinearLayout>
---


▲ リソース

□ res/values/strings.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">インベーダー</string>
    <string name="scoreTitle">"得点: "</string>
</resources>
---

                                                                            以上