Android で ブロック崩し(完成)

2009/6/25 25841hit

Androidで作っていたブロック崩しが完成しました。
といっても、非常にシンプルなゲームだけど
レイアウトを表すMain.xml


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<org.firespeed.CrashBall.CrashBallView
android:id="@+id/ball"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
<TextView
android:id="@+id/stock_balls"
android:text="@string/stock_ball_count"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="top"
android:textColor="#000000"
android:textSize="20sp"/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="@+id/message"
android:text="@string/ready_message"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ff8888ff"
android:textSize="24sp"/>
</RelativeLayout>

</FrameLayout>

今回新たにstock_ballsという残りのボールの数を表示するテキストを追加してる。

文字を保存するstring.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ball">ball:0</string>
<string name="app_name">CrashBall</string>
<string name="ready_message">*** はろー(^o^) ***</string>
<string name="pause_message">Pause</string>
<string name="game_over_message">○○またねー○○</string>
<string name="game_clear_message">(^.^)(^.^)クリアー(^.^)(^.^)</string>
<string name="new_ball_help">↓キーを押すと新しいボールが出るよ</string>
<string name="stock_ball_count">残りのボール</string>
</resources>

ゲームクリアー時のメッセージ
画面にボールがないときのヘルプ
残りのボールの数を表示するメッセージを追加している、

動的オブジェクトのインターフェイスである
ActiveObject.java

package org.firespeed.CrashBall;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
public interface ActiveObject{
void update();
void draw(Canvas canvas, Paint paint);
Rect getRect();
}

新たにgetRect()を追加
これはオブジェクトの四隅を返すオブジェクトで、これを使うことで、画面の再描画位置を指定するのに使っている。 詳しくは後述のCrashBallView.javaで

Ball.java

package org.firespeed.CrashBall;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;

public class Ball implements ActiveObject
{
private int mScreenWide = 100;

// Ball Size
private static final int SIZE = 16;
private static final int HALF_SIZE = SIZE / 2;
private int mState = 50;

// Ball Point
public static final int LEFT_TOP = 0;
public static final int RIGHT_TOP = 1;
public static final int LEFT_DOWN = 2;
public static final int RIGHT_DOWN = 3;

// Ball Speed
public float xSpeed;
public float ySpeed;
public float maxYSpeed = 8f;
public float maxXSpeed = 3f;
// Ball related variables.
public float x;
public float y;

public float g = 0.15f;

public float getlx(){
return x + SIZE;
}
public float getcx(){
return x + HALF_SIZE;
}
public float getly(){
return y + SIZE;
}

public Rect getRect(){
return new Rect(
(int)this.x - 1,
(int)this.y - 1,
(int)this.getlx() + 1,
(int)this.getly() + 1
);

}

public Ball(float x, float y, float xSpeed, float ySpeed,int w,int h){
this.x = x;
this.y = y;
this.xSpeed = xSpeed;
this.ySpeed = ySpeed;
this.mScreenWide = w;
this.mState = CrashBallView.STATE;
}

public void setXSpeed(float speed){
this.xSpeed = getNewSpeed(speed, maxXSpeed);
}

public void setYSpeed(float speed){
this.ySpeed = getNewSpeed(speed, maxYSpeed);
}

private float getNewSpeed(float newSpeed, float maxSpeed){
if(maxSpeed < Math.abs(newSpeed)){
if(newSpeed > 0){
return maxSpeed;
}else{
return -maxSpeed;
}
}
return newSpeed;
}

private int getAfterCrashPoint (float speed,float position, int wall ){
int intPosition = (int)position;
int intSpeed = (int)speed;
int newPoint = ((wall * 2) - intPosition - intSpeed);
//壁のめりこみ対策
if((speed > 0 && newPoint > wall)|| (speed < 0 && newPoint < wall)){
// newPoint = wall;
}
return (newPoint);
}

public void update(){
ySpeed += g;
if(x + xSpeed <= 0 ){
x = getAfterCrashPoint(xSpeed, x, 0);
xSpeed *= -1.01f;
xSpeed = getNewSpeed(xSpeed, maxXSpeed);
}else{
float lx = getlx();
if(lx + xSpeed >= mScreenWide){
x = getAfterCrashPoint(xSpeed, lx, mScreenWide)-SIZE;
xSpeed *= -1.01f;
xSpeed = getNewSpeed(xSpeed, maxXSpeed);
}else{
x += xSpeed;
}
}
if(y + ySpeed <= mState){
y = getAfterCrashPoint(ySpeed, y, mState);

ySpeed *= -0.9f;
ySpeed = getNewSpeed(ySpeed, maxYSpeed);
}else{
y += ySpeed;
}
}

public void draw(Canvas canvas, Paint paint){
paint.setColor(Color.WHITE);
paint.setAntiAlias(true);
canvas.drawCircle(x+HALF_SIZE, y+HALF_SIZE, HALF_SIZE, paint);
}

public void topCrash(int index){
y+= ySpeed;
ySpeed = Math.abs(ySpeed);
}
public void downCrash(int index){
y+= ySpeed;
ySpeed = -Math.abs(ySpeed);
}
public void leftCrash(int index){
x += xSpeed;
xSpeed = Math.abs(xSpeed);
}
public void rightCrash(int index){
x += xSpeed;
xSpeed = -Math.abs(xSpeed);
}
}

画面を飛び回るBall
前回からの変更点を中心に解説

private static final int SIZE = 16;
private static final int HALF_SIZE = SIZE / 2;
private int mState = 50;

まず、解像度に合わせてボールを少し小さくした
mStateは画面上部のステータスバーの高さを示す


// Ball Point
public static final int LEFT_TOP = 0;
public static final int RIGHT_TOP = 1;
public static final int LEFT_DOWN = 2;
public static final int RIGHT_DOWN = 3;

CrashBallViewで衝突判定に使えるようにボールの四隅を定数で宣言してpublicにしている。

public float maxYSpeed = 8f;
public float maxXSpeed = 3f;

最高速度は縦と横で別に持つようにした。
また、この値はゲーム中に少しずつ増えていく。

public Rect getRect(){
return new Rect(
(int)this.x - 1,
(int)this.y - 1,
(int)this.getlx() + 1,
(int)this.getly() + 1
);
}

画面再描画範囲を返すためのメソッド
本体の領域より1ドット多めに取っているのはintへの丸め処理で切り捨てがあるから

public Ball(float x, float y, float xSpeed, float ySpeed,int w,int h){
this.x = x;
this.y = y;
this.xSpeed = xSpeed;
this.ySpeed = ySpeed;
this.mScreenWide = w;
this.mState = CrashBallView.STATE;
}

コンストラクタではステータスバーのサイズを渡す処理を追加



public void setXSpeed(float speed){
this.xSpeed = getNewSpeed(speed, maxXSpeed);
}

public void setYSpeed(float speed){
this.ySpeed = getNewSpeed(speed, maxYSpeed);
}

private float getNewSpeed(float newSpeed, float maxSpeed){
if(maxSpeed < Math.abs(newSpeed)){
if(newSpeed > 0){
return maxSpeed;
}else{
return -maxSpeed;
}
}
return newSpeed;
}

スピードを縦と横で別々に持つようになったのに伴い、メソッドも分けた


private int getAfterCrashPoint (float speed,float position, int wall ){
int intPosition = (int)position;
int intSpeed = (int)speed;
int newPoint = ((wall * 2) - intPosition - intSpeed);
//壁のめりこみ対策
if((speed > 0 && newPoint > wall)|| (speed < 0 && newPoint < wall)){
// newPoint = wall;
}
return (newPoint);
}

前回、説明した壁の反射処理をメソッドにした。
壁のめりこみ対策はとりあえずコメントアウトにしているけど、復活させた方が良いかも、今後再検討の必要有り



public void topCrash(int index){
y+= ySpeed;
ySpeed = Math.abs(ySpeed);
}
public void downCrash(int index){
y+= ySpeed;
ySpeed = -Math.abs(ySpeed);
}
public void leftCrash(int index){
x += xSpeed;
xSpeed = Math.abs(xSpeed);
}
public void rightCrash(int index){
x += xSpeed;
xSpeed = -Math.abs(xSpeed);
}

ブロックに衝突したときの処理
ここでは衝突したときにどうするか?だけを表現し、衝突判定自体はCrashBallViewで行わせている。

最初にスピード分動かしているのは、ブロックの中にボールを食い込ませるため

次はいよいよ肝心のブロック
破壊不可能なブロックや何度も叩かないと行けないブロックなどを作れるように抽象クラスにしている。
Brick.Java

package org.firespeed.CrashBall;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
abstract class Brick implements ActiveObject{

public int x;
public int y;
public int lx;
public int ly;

public Brick(int x,int y){
this.x = x * WIDE;
this.y = y * HEIGHT + CrashBallView.STATE;
this.ly = this.y + HEIGHT;
this.lx = this.x + WIDE;
}
// Pad Size
public static final int HEIGHT = 20;
public static final int WIDE = 32;
public Rect getRect(){
return(new Rect(x,y,lx,ly));
}
public void update(){

}
public void draw(Canvas canvas, Paint paint){
canvas.drawRect(x, y, lx-1, ly-1, paint);
}
public boolean crash(Ball ball,int crashPoint){
return true;
}

}

特に大きな事はなく、基本的に画面に描画しているだけ

それを継承するのがBrickNormal.java
その名の通り、1回叩くと壊れる普通のブロック

package org.firespeed.CrashBall;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Color;

public class BrickNormal extends Brick {
public BrickNormal(int x,int y){
super(x, y);
}
@Override
public void draw(Canvas canvas, Paint paint){
paint.setColor(Color.RED);
super.draw(canvas, paint);
}
}

こちらも基本的には何もせず

実際の処理は、CrashBallView.javaで行う


>package org.firespeed.CrashBall;

import java.util.ArrayList;

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Message;

import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import android.graphics.Paint;

public class CrashBallView extends View {

private int mMode = READY;
public static final int PAUSE = 0; // 一時停止中
public static final int READY = 1; // スタート画面
public static final int RUNNING = 2;// 実行中
public static final int LOSE = 3; // ゲームオーバー
public static final int CLEAR = 4; // クリア
private static final int BRICK_ROW = 10;
private static final int BRICK_COL = 10;
//ステータスバーの高さ
public static final int STATE = 50;
private int w = 320;
private int h = 480;

// 最大リフレッシュレート
private static final long DELAY_MILLIS = 1000/60;

// 画面表示用のメッセージ
private TextView mMessage;
private TextView mPoint;
private RefreshHandler mFieldHandler = new RefreshHandler();
private Paint mPaint = new Paint();
private Pad mPad;
private ArrayList<Ball> mBalls = new ArrayList<Ball>();
private Brick[][] mBricks = new Brick[BRICK_COL][BRICK_ROW];
private int mBallsCount = 0;
private int mStockBallCount = 0;
private int mBricksCount = 0;

// 一定時間待機後Updateを実行させる。 Updateは再度Sleepを呼ぶ
class RefreshHandler extends Handler {
public void sleep(long delayMillis) {
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0), delayMillis);
}
@Override
public void handleMessage(Message msg) {
CrashBallView.this.update();
}
};

//コンストラクタ
public CrashBallView(Context context, AttributeSet attrs){
super(context,attrs);
initialProcess();
}
public CrashBallView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
initialProcess();
}

private void initialProcess(){
setFocusable(true);
}

//新ゲームの作成
private void newGame(){

mBalls.clear();
mBricksCount = 0;
mStockBallCount = 5;
mBallsCount = 0;
for(int i=0; i < BRICK_COL; i++){
for(int j=0; j < BRICK_ROW; j++){
mBricksCount++;
mBricks[i][j] = new BrickNormal(i,j);
}
}
CrashBallView.this.invalidate();
}

private MotionEvent mTouchEvent;
public boolean onTouchEvent(MotionEvent event){
mTouchEvent = event;
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
switch(mMode){
case READY:
setMode(RUNNING);
break;
case LOSE:
setMode(READY);
break;
case CLEAR:
setMode(READY);
break;
}
}
return true;
}

public void setMode(int newMode){
int oldMode = mMode;
mMode = newMode;

if(newMode == RUNNING){
if (oldMode == READY ) {
newGame();

Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.new_ball_help );
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);
}else if(oldMode == PAUSE){
if(mBallsCount == 0){
Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.new_ball_help );
mMessage.setText(newMessage);
}else{
mMessage.setVisibility(View.INVISIBLE);
}
}
if(oldMode != RUNNING){
update();
}
return;
}

CharSequence newMessage = "";
Resources resource = getContext().getResources();
switch(newMode){
case PAUSE:
newMessage = resource.getText(R.string.pause_message);
break;
case READY:
newMessage = resource.getText(R.string.ready_message);
break;
case LOSE:
newMessage = resource.getText(R.string.game_over_message);
break;
case CLEAR:
newMessage = resource.getText(R.string.game_clear_message );
break;
}
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mPad = new Pad(w, h );
setMode(READY);
}

private boolean isBricksCrash(int xIndex,int yIndex){
if(yIndex >=BRICK_ROW || xIndex >= BRICK_COL){
return false;
}
if(mBricks[xIndex][yIndex] != null){
return true;
}
return false;
}

private void crashBrick(int xIndex, int yIndex){
if(yIndex >=BRICK_ROW || xIndex >= BRICK_COL || mBricks[xIndex][yIndex]== null){
return;
}
CrashBallView.this.invalidate(mBricks[xIndex][yIndex].getRect());
mBricks[xIndex][yIndex] = null;
mBricksCount--;
if(mBricksCount<=0){
setMode(CLEAR);
}

}

public void update(){
if(mMode == RUNNING){

if(mTouchEvent!=null){
CrashBallView.this.invalidate(mPad.getRect());
mPad.onTouchEvent(mTouchEvent);
mPad.update();
CrashBallView.this.invalidate(mPad.getRect());
}

int xCrash ;
int yCrash ;
for (int i = 0; i < this.mBallsCount; i++){
xCrash = 0;
yCrash = 0;
Ball ball = this.mBalls.get(i);

CrashBallView.this.invalidate(ball.getRect());
ball.update();
CrashBallView.this.invalidate(ball.getRect());

int xIndex = (int)(ball.x/Brick.WIDE);
int yIndex = (int)((ball.y - STATE)/Brick.HEIGHT) ;
int lxIndex = (int)(ball.getlx()/Brick.WIDE);
int lyIndex = (int)((ball.getly() - STATE)/Brick.HEIGHT);

if(isBricksCrash(xIndex, yIndex) ){
xCrash ++;
yCrash ++;
}
if(isBricksCrash(lxIndex, yIndex)){
xCrash --;
yCrash ++;
}
if(isBricksCrash(xIndex, lyIndex)){
xCrash ++;
yCrash --;
}
if(isBricksCrash(lxIndex, lyIndex)){
xCrash --;
yCrash --;
}
crashBrick(xIndex,yIndex);
crashBrick(xIndex,lyIndex);
crashBrick(lxIndex,yIndex);
crashBrick(lxIndex,lyIndex);

if(yCrash > 0){
ball.topCrash(yIndex);
}else if(yCrash < 0){
ball.downCrash(lyIndex);
}
if(xCrash >0){
ball.leftCrash(xIndex);
}else if(xCrash < 0){
ball.rightCrash(lxIndex);
}

if(mPad.y <= ball.getly() && mPad.getly() >= ball.y && mPad.x <= ball.getlx() && mPad.getlx() >= ball.x){
float newXSpeed;
float newYSpeed;

newXSpeed = ball.xSpeed +(ball.getcx() - mPad.getcx())/5;
newYSpeed = - (ball.ySpeed - Math.abs(ball.getcx() - mPad.getcx()) * 1.2f);
if(newYSpeed > -10){
newYSpeed = -10;
}
ball.setXSpeed(newXSpeed);
ball.setYSpeed(newYSpeed);

if(ball.maxYSpeed < 15){
ball.maxYSpeed += 0.1f;
}
}
else if(ball.y > this.h){
this.mBalls.remove(i);
mBallsCount--;
if(mBallsCount == 0){

if(mStockBallCount > 0){
Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.new_ball_help );
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);

}else{
setMode(LOSE);
return;
}
}
}

}
mFieldHandler.sleep(DELAY_MILLIS);
}else{
CrashBallView.this.invalidate();
}
}

@Override
public void onDraw(Canvas canvas){

canvas.drawColor(Color.rgb(120,140,160));
mPaint.setColor(Color.BLACK);
canvas.drawRect(0, STATE, w, h, mPaint);

mPad.draw(canvas, mPaint);

for (int i = 0; i <this.mBallsCount; i++){
this.mBalls.get(i).draw(canvas, mPaint);
}

for(int i = 0; i < BRICK_COL; i++ ){
for(int j= 0; j < BRICK_ROW; j++){
if(mBricks[i][j]!= null){
mBricks[i][j].draw(canvas, mPaint);
}
}

}

}

public void setTextView(TextView message){
this.mMessage = message;
}
public void setPointTextView(TextView point){
this.mPoint = point;
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event){

if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
if (mMode == READY) {
setMode(RUNNING);
update();
return (true);
}
if (mMode == RUNNING){
setMode(PAUSE);
update();
return (true);
}
if (mMode == PAUSE) {
setMode(RUNNING);
update();
return (true);
}
return (true);
}
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_SPACE){
addBall();
}

return super.onKeyDown(keyCode, event);

}

private void addBall(){
if(mMode == RUNNING && mStockBallCount > 0){
if(mBallsCount == 0){
mMessage.setVisibility(View.INVISIBLE);
}
mBalls.add(new Ball(w/2,300,-0.2f,-5,w, h));
mBallsCount ++;
mStockBallCount --;
Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.stock_ball_count );
mPoint.setText(newMessage+ Integer.toString(mStockBallCount));
}
}

//State load
public void restoreState(Bundle icicle){
setMode(PAUSE);
mMode = icicle.getInt("mode");
mBalls = flaotsToBalls(icicle.getFloatArray("balls"));
}

private ArrayList<Ball>flaotsToBalls(float[] rawArray) {
ArrayList<Ball> balls = new ArrayList<Ball>();

int coordCount = rawArray.length;
for (int index = 0; index < coordCount; index += 4) {
Ball ball = new Ball(rawArray[index], rawArray[index + 1], rawArray[index+2], rawArray[index+3],w, h);
balls.add(ball);
}
return balls;
}

//State save
public Bundle saveState(Bundle icicle){
icicle.putInt("mode", mMode);
icicle.putFloatArray("balls",ballsToFloats(mBalls));
return icicle;

}

private float[] ballsToFloats(ArrayList<Ball> cvec){
int count = cvec.size();
float[] rawArray = new float[count * 4];
for (int index = 0; index < count; index++) {
Ball setBall = (Ball)cvec.get(index);
rawArray[4 * index] = setBall.x;
rawArray[4 * index + 1] = setBall.y;
rawArray[4 * index + 2] = setBall.xSpeed;
rawArray[4 * index + 3] = setBall.ySpeed;
}
return rawArray;
}

}


ここはダイブ変わってる


public static final int CLEAR = 4; // クリア
private static final int BRICK_ROW = 10;
private static final int BRICK_COL = 10;
//ステータスバーの高さ
public static final int STATE = 50;
private int w = 320;
private int h = 480;

モードにクリア状態が増えたのでモードの定数を追加
加えてブロックの縦数と横数、
ボールのステータスバーの高さも指定する。



private int mStockBallCount = 0;
private int mBricksCount = 0;

mStockBallCountはまだ画面に出していないキープしているボールの数
mBricksCountはまだ壊れていないブロックの数
ソース的には冗長だけどあえて変数にすることでパフォーマンスを改善している。

// 一定時間待機後Updateを実行させる。 Updateは再度Sleepを呼ぶ
class RefreshHandler extends Handler {
public void sleep(long delayMillis) {
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0), delayMillis);
}
@Override
public void handleMessage(Message msg) {
CrashBallView.this.update();
}
};

以前は無条件にCrashBallView.this.invalidate();を実行して全画面を再描画していたけど、
パフォーマンスを考えて、これはオブジェクトの移動時に描画位置を指定するようにした。


//新ゲームの作成
private void newGame(){

mBalls.clear();
mBricksCount = 0;
mStockBallCount = 5;
mBallsCount = 0;
for(int i=0; i < BRICK_COL; i++){
for(int j=0; j < BRICK_ROW; j++){
mBricksCount++;
mBricks[i][j] = new BrickNormal(i,j);
}
}
CrashBallView.this.invalidate();
}

新ゲーム開始時に各種変数をリセットするとともに、ブロックを追加する。
ブロックは2次元配列に格納している。
ここでは全ての要素にブロックを入れているけど、ここでブロックを入れたり入れなかったりすることでブロックの配置を調整できる。
2次元配列に入れているのは後述の衝突判定のため


private MotionEvent mTouchEvent;
public boolean onTouchEvent(MotionEvent event){
mTouchEvent = event;
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
switch(mMode){
case READY:
setMode(RUNNING);
break;
case LOSE:
setMode(READY);
break;
case CLEAR:
setMode(READY);
break;
}
}
return true;
}

onTouchEventの変更点は2つ
一つはクリア後再度タッチすると準備完了画面に戻るようにしたこと
もう一つがTouchイベントを即座に反映するのではなく、一度変数に格納し、
PadのUpdateのタイミングで処理するようにしている。
これは、タッチし続けているとその間イベントが発行され続けて、
そのたびにpadの移動処理をしていると処理がすこぶる重くなったため


public void setMode(int newMode){
int oldMode = mMode;
mMode = newMode;

if(newMode == RUNNING){
if (oldMode == READY ) {
newGame();

Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.new_ball_help );
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);
}else if(oldMode == PAUSE){
if(mBallsCount == 0){
Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.new_ball_help );
mMessage.setText(newMessage);
}else{
mMessage.setVisibility(View.INVISIBLE);
}
}
if(oldMode != RUNNING){
update();
}
return;
}

CharSequence newMessage = "";
Resources resource = getContext().getResources();
switch(newMode){
case PAUSE:
newMessage = resource.getText(R.string.pause_message);
break;
case READY:
newMessage = resource.getText(R.string.ready_message);
break;
case LOSE:
newMessage = resource.getText(R.string.game_over_message);
break;
case CLEAR:
newMessage = resource.getText(R.string.game_clear_message );
break;
}
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);
}

setModeはクリアの処理を追加したのと、ゲーム中に画面上からボールが1つも無くなった場合、新しいボールを出すヘルプを描画するようにした。


private boolean isBricksCrash(int xIndex,int yIndex){
if(yIndex >=BRICK_ROW || xIndex >= BRICK_COL){
return false;
}
if(mBricks[xIndex][yIndex] != null){
return true;
}
return false;
}

今回の目玉、衝突判定処理
今回ブロックは最大100個、ボールは最大10個、コレを全て真面目に衝突判定しようとすると
100ブロックx10ボール×4隅で4000回の衝突判定が必要になる。
これじゃ処理に時間がかかりすぎ

そこで、今回、ブロックを配列に格納しているのを利用する。
配列は2次元配列になっていて、1次元目で横位置を、2次元目で縦位置を表す
配列の要素は画面に敷き詰めるように存在するので、ある座標が2つ以上の要素にまたがることはない。
そこで位置から配列の要素を1つに特定できるようになるので

この方法ならそこにブロックがアルかどうか、だけで判断できるので
1ブロックx10ボールx4隅の40回で衝突判定が終わる。

この処理は、まず最初に、指定した座標が配列の要素内に入っているかをチェックし、
要素外の領域だったら無条件で衝突無し
要素内だったらオブジェクトを確認し、null以外なら衝突としている。

private void crashBrick(int xIndex, int yIndex){
if(yIndex >=BRICK_ROW || xIndex >= BRICK_COL || mBricks[xIndex][yIndex]== null){
return;
}
CrashBallView.this.invalidate(mBricks[xIndex][yIndex].getRect());
mBricks[xIndex][yIndex] = null;
mBricksCount--;
if(mBricksCount<=0){
setMode(CLEAR);
}

}

これは衝突が発生した場合の処理、
ブロックの位置を再描画位置に指定してオブジェクトを消し、
ブロックの残り数を一減らす
ブロックの残り数が0になったらクリア処理に移る

updateメソッドも大きく変わった一つ
長いので細切れに解説

if(mTouchEvent!=null){
CrashBallView.this.invalidate(mPad.getRect());
mPad.onTouchEvent(mTouchEvent);
mPad.update();
CrashBallView.this.invalidate(mPad.getRect());
}

TouchEventで処理していたpadの移動処理をUpdateに持ってきている。
パッドの移動前と移動後を画面再描画位置に指定している。


CrashBallView.this.invalidate(ball.getRect());
ball.update();
CrashBallView.this.invalidate(ball.getRect());

ボールの配列を動かす前後でボールの位置を画面再描画位置に指定している。


int xCrash ;
int yCrash ;
for (int i = 0; i < this.mBallsCount; i++){
xCrash = 0;
yCrash = 0;
Ball ball = this.mBalls.get(i);

CrashBallView.this.invalidate(ball.getRect());
ball.update();
CrashBallView.this.invalidate(ball.getRect());

int xIndex = (int)(ball.x/Brick.WIDE);
int yIndex = (int)((ball.y - STATE)/Brick.HEIGHT) ;
int lxIndex = (int)(ball.getlx()/Brick.WIDE);
int lyIndex = (int)((ball.getly() - STATE)/Brick.HEIGHT);

if(isBricksCrash(xIndex, yIndex) ){
xCrash ++;
yCrash ++;
}
if(isBricksCrash(lxIndex, yIndex)){
xCrash --;
yCrash ++;
}
if(isBricksCrash(xIndex, lyIndex)){
xCrash ++;
yCrash --;
}
if(isBricksCrash(lxIndex, lyIndex)){
xCrash --;
yCrash --;
}
crashBrick(xIndex,yIndex);
crashBrick(xIndex,lyIndex);
crashBrick(lxIndex,yIndex);
crashBrick(lxIndex,lyIndex);

if(yCrash > 0){
ball.topCrash(yIndex);
}else if(yCrash < 0){
ball.downCrash(lyIndex);
}
if(xCrash >0){
ball.leftCrash(xIndex);
}else if(xCrash < 0){
ball.rightCrash(lxIndex);
}

ボールの衝突判定をやっている。
ボールの4隅それぞれが配列のどこに位置するかを
ボールの位置をブロックのサイズで割ることで求めている。
(STATEを引いているのはステータスバーの分配列が下にずれているため)
それぞれ4隅を衝突判定しているんだけど、

このとき、一度に複数箇所衝突することがあり得るので、
上下方向と左右方向をそれぞれ集計して、最後に衝突方向を判断している。
例えば右上と左上がぶつかった場合、
上に2回なので上下方向は上にぶつかった
右に1回左に1回なので左右方向はぶつかっていない という判定をしている。

なぜ、こんなめんどくさい事をしているかというと、
例えば上方向にぶつかったからと言っていきなり上下を反転させてしまうと
縦にまっすぐ積まれているブロックに左からぶつかった場合、
壁にぶつかるのと同じように、上下方向はそのまま、左右だけ反転したいところだけど
右上がぶつかったから上方向がぶつかったと判断すると、下方向に落ちてしまうし、
右下がぶつかったから下方向がぶつかったと判断すると、上方向に跳ね上がってしまう。

ということで、一度集計して、衝突後の動きを決めてる


if(mPad.y <= ball.getly() && mPad.getly() >= ball.y && mPad.x <= ball.getlx() && mPad.getlx() >= ball.x){
float newXSpeed;
float newYSpeed;

newXSpeed = ball.xSpeed +(ball.getcx() - mPad.getcx())/5;
newYSpeed = - (ball.ySpeed - Math.abs(ball.getcx() - mPad.getcx()) * 1.2f);
if(newYSpeed > -10){
newYSpeed = -10;
}
ball.setXSpeed(newXSpeed);
ball.setYSpeed(newYSpeed);

if(ball.maxYSpeed < 15){
ball.maxYSpeed += 0.1f;
}

パッドの衝突判定は前回は段階的に挙動を変えていたけど、もっとシンプルに計算で動きを変えるようにした。
ボールの動きが繊細になったので面白くなったと思う。



@Override
public void onDraw(Canvas canvas){

canvas.drawColor(Color.rgb(120,140,160));
mPaint.setColor(Color.BLACK);
canvas.drawRect(0, STATE, w, h, mPaint);
mPad.draw(canvas, mPaint);

for (int i = 0; i <this.mBallsCount; i++){
this.mBalls.get(i).draw(canvas, mPaint);
}
for(int i = 0; i < BRICK_COL; i++ ){
for(int j= 0; j < BRICK_ROW; j++){
if(mBricks[i][j]!= null){
mBricks[i][j].draw(canvas, mPaint);
}
}
}
}

onDrawはステータスバーを描画するようにしたのと、
ブロックを描画するようにした程度



@Override
public boolean onKeyDown(int keyCode, KeyEvent event){

if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
if (mMode == READY) {
setMode(RUNNING);
update();
return (true);
}
if (mMode == RUNNING){
setMode(PAUSE);
update();
return (true);
}
if (mMode == PAUSE) {
setMode(RUNNING);
update();
return (true);
}
return (true);
}
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_SPACE){
addBall();
}

return super.onKeyDown(keyCode, event);

}



onKeyDownではクリア系の処理を追加するとともに、下ボタンを押すとボールが新たに出現するようになっている。


private void addBall(){
if(mMode == RUNNING && mStockBallCount > 0){
if(mBallsCount == 0){
mMessage.setVisibility(View.INVISIBLE);
}
mBalls.add(new Ball(w/2,300,-0.2f,-5,w, h));
mBallsCount ++;
mStockBallCount --;
Resources resource = getContext().getResources();
CharSequence newMessage = resource.getText(R.string.stock_ball_count );
mPoint.setText(newMessage+ Integer.toString(mStockBallCount));
}
}

これがボールを追加する処理
ストックしてあるボールが有れば新しくボールを作っている。


以上超駆け足で紹介。
あとで修正しないと
ソースもアップする予定
Androidブロック崩しゲーム開始スナップショット
ゲーム開始時の画面、画面の色遣いが酷い。。。

↓キーでゲームが開始する。
あえてタッチパネルではないのは誤操作防止のため
↑はsnakeが一時停止になっていたので合わせて一時停止にしたんだけど
トラックボールの場合ボールを跳ね上げる感覚になるので↑の方が良いのかも と迷い中

Androidブロック崩しゲーム中スナップショット
ゲーム中の画面、複数のボールを出すとガシガシ消せるが、二兎追うもので、どっちも落とすってこともあって難しい


Androidブロック崩しゲームオーバースナップショット
ゲームオーバー ボールがストックも画面からも無くなるとゲームオーバー

Androidブロック崩しゲームクリアスナップショット
そしてクリアー 難易度ゆるめだけど嬉しい

前:交通事故に遭遇しました 次:意外と使えるBing!

関連キーワード

[Android][Java][IT]

コメント

名前:kensei|投稿日:2009/06/25 13:34

すげ〜!!いつかFireSpeedForgeたてれるよ♪オレも頑張ろっと☆

名前:kenz|投稿日:2009/06/29 20:32

携帯アプリ楽しいよ
性能の制約が大きいから昔のプログラムみたい

名前:名無しさん|投稿日:2013/07/29 13:29

Pad.javaがわからない・・

名前:kenz|投稿日:2013/08/04 16:23

名前:名無しさん|投稿日:2013/08/19 12:30

メソッド setText(TextView) は型 CrashBallView で未定義です
メソッド getcx() は型 Pad で未定義です

完璧にリスペクトしてもエラーが出るんですが、なぜでしょうか・・・?

名前:kenz|投稿日:2013/09/04 23:15

すいません、どこか処理が抜けているのかもです
いかんせん古いプログラムでソースがなくて

コメントを投稿する

名前URI
コメント