[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> --- 以上