Android ブロック崩し パットを作る

2009/6/7 16563hit

ゲームとして基本的な部分を作るために
ブロック崩しのパットを作ったよ
合わせて、複数のボールに対応させたのと
ボールが奈落に落ちたらゲームオーバーにするようにした。

加えて、いろいろ考えてプログラムの作りを少し変えてパフォーマンスチューニングも実施。
JAVA的には良くないコーディングの所も幾つかあるけど、
携帯ってことでCPUとバッテリーの負荷を減らすために泥臭いやりかたしてみた。

パット処理を入れながらCPU使用率がダイブ減ったから効果はありそう。


まずはレイアウト
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"
/>
<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>

画面にメッセージを表示するためのTextView(message)を追加した。
色とかサイズはSnakeのをそのまま使っているけど、実際には変えるかも

メッセージは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>
</resources>


さて、javaのほう。
まずはActivity
CrashBall.java


package org.firespeed.CrashBall;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.widget.TextView;
import android.view.MotionEvent;

public class CrashBall extends Activity {

/** Called when the activity is first created. */
private static String ICICLE_KEY = "CRASH_BALL";
private CrashBallView mView;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
requestWindowFeature(Window.FEATURE_NO_TITLE);

setContentView(R.layout.main);

mView = (CrashBallView)findViewById(R.id.ball);
mView.setText((TextView)findViewById(R.id.message));

if(icicle == null){
mView.setMode(CrashBallView.READY);
} else {
Bundle map = icicle.getBundle(ICICLE_KEY);
if (null != map) {
mView.restoreState(map);
} else {
mView.setMode(CrashBallView.READY);
}
}
}

@Override
protected void onPause(){
super.onPause();
mView.setMode(CrashBallView.PAUSE);
}

@Override
public boolean onTouchEvent(MotionEvent event){
super.onTouchEvent(event);
return(mView.onTouchEvent(event));
}


@Override
public void onSaveInstanceState(Bundle outState) {
//Store the game state
Bundle icicle = new Bundle();
outState.putBundle(ICICLE_KEY, mView.saveState(icicle));
}
}



前回よりずいぶん処理が増えたので、部分ごとに分けて説明
onCreateメソッドは処理開始時に呼ばれて、オブジェクトの初期化などを行う。

requestWindowFeature(Window.FEATURE_NO_TITLE);

まず、タイトルバーを非表示にして、画面一杯使えるようにする。

setContentView(R.layout.main);

mView = (CrashBallView)findViewById(R.id.ball);
mView.setText((TextView)findViewById(R.id.message));

次にxmlからウィンドウとテキストを呼び出してViewに設定している。
注意点は、setTextするまでテキストオブジェクトは作られていないから、コンストラクタでテキストオブジェクトは使えない。

if(icicle == null){
mView.setMode(CrashBallView.READY);
} else {
Bundle map = icicle.getBundle(ICICLE_KEY);
if (null != map) {
mView.restoreState(map);
} else {
mView.setMode(CrashBallView.READY);
}

引数で与えられるBundle icicleは状態を保存させるためのオブジェクトで、新規の時はNULLがくる。
NULLだったら、何もせずにREADY(ゲーム開始画面)へ
NULL以外だったらgetBundleで保存値を取得し
ViewのrestoreStateメソッドへ渡す

onPauseメソッドは画面の電源ボタンが押された時に呼ばれる。

protected void onPause(){
super.onPause();
mView.setMode(CrashBallView.PAUSE);
}

モードをポーズにするだけ。

onTouchEventメソッドは画面上を操作したときに発生するイベントで

public boolean onTouchEvent(MotionEvent event){
super.onTouchEvent(event);
return(mView.onTouchEvent(event));
}

細かな処理はViewにやらせるので
Viewに引数をそのまま引き渡す
ちなみに、ここで言うVIEWはMVCでいうところのCだったりするから注意

onSaveInstanceStateメソッドは状態を保存する状況になったとき(状態が最前面以外に移るとき)に呼ばれる
Androidでは複数の画面を実行するために
画面が後ろに行くときはいったん処理状況を保存しておき、再度全面に来るときに保存された状況を呼び出すという方式がとられている。

@Override
public void onSaveInstanceState(Bundle outState) {
//Store the game state
Bundle icicle = new Bundle();
outState.putBundle(ICICLE_KEY, mView.saveState(icicle));
}

保存自体はViewのsaveStateメソッドを呼び出し行う。

次に、このプログラムの主要部分となる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; // ゲームオーバー

private int w = 100;
private int h = 100;

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

// 画面表示用のメッセージ
private TextView mMessage;
private RefreshHandler mFieldHandler = new RefreshHandler();
private Paint mPaint = new Paint();
private Pad mPad;
private ArrayList<Ball> mBalls = new ArrayList<Ball>();
private int mBallsCount = 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();
CrashBallView.this.invalidate();
}

};



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();
mBalls.add(new Ball(w/2,300,-0.2f,-5,w,h));
mBallsCount = 1;
}

public boolean onTouchEvent(MotionEvent event){
this.mPad.onTouchEvent(event);

if(event.getAction() == MotionEvent.ACTION_DOWN){
switch(mMode){
case READY:
setMode(RUNNING);
break;
case LOSE:
setMode(READY);
break;
}
}
return true;
}
public void setMode(int newMode){
int oldMode = mMode;
mMode = newMode;

if(newMode == RUNNING){
if (oldMode == READY ) {
newGame();
}
if(oldMode != RUNNING){
mMessage.setVisibility(View.INVISIBLE);
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;
}
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);
}

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

public void update(){
if(mMode == RUNNING){
mPad.update();
for (int i = 0; i <this.mBallsCount; i++){
Ball ball = this.mBalls.get(i);
ball.update();
if(mPad.y <= ball.getly() && mPad.getly() >= ball.y && mPad.x <= ball.getlx() && mPad.getlx() >= ball.x){
int pos = mPad.colArea(ball.getcx());
float newXSpeed;
float newYSpeed;

switch(pos){
case 0:
newXSpeed = ball.xSpeed-6f;
newYSpeed = - Math.abs(ball.ySpeed) * 0.5f;
break;
case 1:
newXSpeed = ball.xSpeed-4f;
newYSpeed = - Math.abs(ball.ySpeed) * 0.8f;
break;
case 2:
newXSpeed = ball.xSpeed-2f;
newYSpeed = - Math.abs(ball.ySpeed);
break;
case 3:
newXSpeed = ball.xSpeed * 0.9f;
newYSpeed = - Math.abs(ball.ySpeed) * 1.2f;
break;
case 4:
newXSpeed = ball.xSpeed+2;
newYSpeed = - Math.abs(ball.ySpeed);
break;
case 5:
newXSpeed = ball.xSpeed+4;
newYSpeed = - Math.abs(ball.ySpeed) * 0.8f;
break;
default:
newXSpeed = ball.xSpeed+6;
newYSpeed = - Math.abs(ball.ySpeed) * 0.5f;
break;
}
if(newYSpeed > -10){
newYSpeed = -10;
}
ball.xSpeed=ball.setSpeed(newXSpeed);
ball.ySpeed = newYSpeed;
//ball.ySpeed=ball.setSpeed(newYSpeed);

}
else if(ball.y > this.h){
this.mBalls.remove(i);
mBallsCount--;
if(mBallsCount >= 0){
setMode(LOSE);
return;
}
}

}
mFieldHandler.sleep(DELAY_MILLIS);
}

}

public void setText(TextView message){
this.mMessage = message;
}


@Override
public void onDraw(Canvas canvas){
canvas.drawColor(Color.BLACK);
mPad.draw(canvas, mPaint);
for (int i = 0; i <this.mBallsCount; i++){
this.mBalls.get(i).draw(canvas, mPaint);
}

}



@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);
}

return super.onKeyDown(keyCode, event);

}

//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;
}

}

これも前回から大きく変わった部分
やはり部分毎に説明していく。

まずは定数、変数などの定義

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; // ゲームオーバー

画面に4つのモードを持たせる事にした。
それぞれ整数型の値で状況を分ける。


private int w = 100;
private int h = 100;

画面の解像度を記憶する変数。
初期値は100だけど、後述のonSizeChangedイベントで値が設定される。

// 画面表示用のメッセージ
private TextView mMessage;
private RefreshHandler mFieldHandler = new RefreshHandler();
private Paint mPaint = new Paint();
private Pad mPad;
private ArrayList<Ball> mBalls = new ArrayList<Ball>();
private int mBallsCount = 0;

mMessageは画面に表示するためのTextView、前のmain.xmlで作った奴で、内容はString.xmlから持ってきた値を入れる。
初期化してないから、setMessageするまで使っちゃだめ
mFieldHandler は画面を更新するためのハンドラ
前回は別のファイルにしていたけど、まどろっこしいんで内部クラスにした。

// 一定時間待機後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();
}

};

その中身がコレ、内部クラスになったのでViewを保持しなくなったりしているけど、基本的にやることは同じ、
sleepメソッドが呼ばれると
一定時間後にメッセージが飛び、
handleMessageでメッセージを受信したらViewのupdate(状態更新)とinvalidate(画面更新)を呼ぶ。
updateの中で再度sleepを呼べば、また一定時間後にメッセージが飛び、handleMessageが実行され、updateが呼ばれ・・・ と繰り返す。

mPaintは描画用のオブジェクト、一回作ってずっと使い続ける
mPadはボールをはじき返すパッド
mBallsはボールを格納する配列。
ボールを複数使えるように配列に格納することにした。
mBallsCountはボールの数を格納する変数。
配列の要素数を取得しても良いんだけど
頻繁に使うのでパフォーマンスを考え変数にしている。

newGameメソッドは新しいゲームを始めるときに呼ぶ処理

mBalls.clear();
mBalls.add(new Ball(w/2,300,-0.2f,-5,w,h));
mBallsCount = 1;

ボール配列をクリアした後に、新しいボールを一つ作ってる。
ここで、ボールを複数作れば複数のボールが画面上を飛び回る。

onTouchEventメソッドは画面を操作されたときに呼ばれる処理、

this.mPad.onTouchEvent(event);

まずは指に追従するようにパッドに処理を行わせる、


if(event.getAction() == MotionEvent.ACTION_DOWN){
switch(mMode){
case READY:
setMode(RUNNING);
break;
case LOSE:
setMode(READY);
break;
}
}
return true;

つぎにゲーム開始状態ならゲーム実行状態へ、
ゲームオーバー状態ならゲーム開始状態へ移動する。

setModeメソッドは状態遷移時に呼ぶメソッド
newModeには新しい状態が入ってくる
それに対してmModeには今の状態が入っている

int oldMode = mMode;
mMode = newMode;

あらかじめoldModeに今の状態を待避させモードをセットする。


if(newMode == RUNNING){
if (oldMode == READY ) {
newGame();
}
if(oldMode != RUNNING){
mMessage.setVisibility(View.INVISIBLE);
update();
}
return;
}

新しく実行画面に遷移する場合、
今が準備完了画面なら新しいゲームを開始する。
今が実行中以外ならメッセージを非表示にしてUpdateを呼ぶ
※Updateは一定時間毎に呼ばれ続けるのでゲームが開始される
今が実行中のときは何もしない。


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;
}
mMessage.setText(newMessage);
mMessage.setVisibility(View.VISIBLE);

一時停止、ゲーム開始画面、ゲームオーバー画面に遷移する場合は、それに応じたメッセージを表示する。

onSizeChangedメソッドは解像度が変わったときに呼ばれる。
実際には処理開始時にも呼ばれる。
ここは未完成、とりあえず解像度をセットして準備完了画面にしておく

this.w=w;
this.h=h;
mPad = new Pad(w,h);
setMode(READY);


updateメソッドは今回のメイン処理、
ゲーム実行中に一定時間毎(1フレームごと)に呼ばれてゲームの動きを制御する。

if(mMode == RUNNING){

UPDATE処理は実行中状態でしか実行しない。
一定時間後に再処理を行う、mFieldHandler.sleep(DELAY_MILLIS)も呼ばれなくなるから
言い換えれば、RUNNINGがUPDATEの繰り返し条件にもなる


mPad.update();

まずは、padの状態を更新する。
ここはシンプル


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

次にボールの配列から要素を取り出して処理を行う

ball.update();

まずは単体の処理


if(mPad.y <= ball.getly() && mPad.getly() >= ball.y && mPad.x <= ball.getlx() && mPad.getlx() >= ball.x){

次にpadとの衝突判定を行っている。
&&を使っているので、左辺の式が一つでもfalseなら以降の処理は判定しない。
ボールは殆どの場合padより上にあるから、まずはpad以下であるかでふるいにかければたいていの場合Falseになるので、後の判定を省ける。


int pos = mPad.colArea(ball.getcx());
float newXSpeed;
float newYSpeed;

colAreaは衝突した位置を7段階で返す。
中央を3に左に行くほど小さく、右に行くほど大きい

switch(pos){
case 0:
newXSpeed = ball.xSpeed-6f;
newYSpeed = - Math.abs(ball.ySpeed) * 0.5f;
break;
case 1:
newXSpeed = ball.xSpeed-4f;
newYSpeed = - Math.abs(ball.ySpeed) * 0.8f;
break;
case 2:
newXSpeed = ball.xSpeed-2f;
newYSpeed = - Math.abs(ball.ySpeed);
break;
case 3:
newXSpeed = ball.xSpeed * 0.9f;
newYSpeed = - Math.abs(ball.ySpeed) * 1.2f;
break;
case 4:
newXSpeed = ball.xSpeed+2;
newYSpeed = - Math.abs(ball.ySpeed);
break;
case 5:
newXSpeed = ball.xSpeed+4;
newYSpeed = - Math.abs(ball.ySpeed) * 0.8f;
break;
default:
newXSpeed = ball.xSpeed+6;
newYSpeed = - Math.abs(ball.ySpeed) * 0.5f;
break;
}
if(newYSpeed > -10){
newYSpeed = -10;
}

接触した位置に応じてスピードに変化を持たせる、


ball.xSpeed=ball.setSpeed(newXSpeed);
ball.ySpeed = newYSpeed;

setSpeedは最高速を制御して居るんだけど、これでは処理が汚いので、
setYSpeedとsetXSpeedメソッドを作って引数で値をセットするようにしようとおもう。


else if(ball.y > this.h){
this.mBalls.remove(i);
mBallsCount--;
if(mBallsCount >= 0){
setMode(LOSE);
return;
}
}

奈落に落ちたボールは失われる。
全てのボールが無くなったらゲームオーバー

}
mFieldHandler.sleep(DELAY_MILLIS);
}

一定時間後にまたupdateを呼ぶためのコール。
上でも書いたけど実行状態の時にしか呼ばないのに注意。

setTextメソッドはActivityからTextViewをセットするためのメソッド
setTextViewに名前変えよう・・・

this.mMessage = message;


onDrawメソッドは画面描画のためのメソッド
最初は配列に全てのオブジェクトを入れて描画させようと思ったけど、
パフォーマンスを考慮して、配列は分けて呼ぶことにした。

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



onKeyDownはキーを押されたときに呼ぶメソッド

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);
}

return super.onKeyDown(keyCode, event);

上ボタンを押したとき、開始画面ならゲームを開始、
ゲーム中なら一時停止、一時停止ならゲームを再開させる。

restoreStateメソッドは状態を復元するときに呼ぶメソッド

setMode(PAUSE);
mMode = icicle.getInt("mode");
mBalls = flaotsToBalls(icicle.getFloatArray("balls"));

icicle.getInt("mode");でモードの位置を
icicle.getFloatArray("balls")でボールの情報を取得している。

flaotsToBallsメソッドはfloatの配列をball配列に変えるメソッド


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;
}

配列には
ボール1の横座標、縦座標、横速度、縦速度
ボール2の横座標、縦座標、横速度、縦速度
といった順番で値が入っているので、
ループでコレを分解して、ボールの配列にしていく。

saveStateメソッドは状態を保存するときに呼ぶメソッド

icicle.putInt("mode", mMode);
icicle.putFloatArray("balls",ballsToFloats(mBalls));
return icicle;

やることはrestoreStateの逆

ballsToFloatsメソッドは
flaotsToBallsメソッドの逆、ball配列をfloatの配列にする。

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;


ボール1の横座標、縦座標、横速度、縦速度
ボール2の横座標、縦座標、横速度、縦速度
と値を続けて配列にしていき、ボールの配列を一つの配列にする
複数のボールを続けていく。
ボールが一つなら配列の要素数は4
ボールが二つなら配列の要素数は8になる。

次に、ボールとパッド
そして、まだ作ってないけどブロックが実装するインターフェイス
ActiveObject.javaを弄る。

package org.firespeed.CrashBall;

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

public interface ActiveObject{
void update();
void draw(Canvas canvas, Paint paint);
}

画面を描画するためのdrawを追加


次にボールの
Ball.java


package org.firespeed.CrashBall;

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

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

// Ball Size
private static final int HALF_SIZE = 10;
private static final int SIZE = 20;

// Ball Speed
public float xSpeed;
public float ySpeed;
public float maxSpeed = 10f;
// Ball related variables.
public float x;
public float y;

public float g = 0.3f;

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

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;

}



public float setSpeed(float oldSpeed){
if(maxSpeed < Math.abs(oldSpeed)){
if(oldSpeed > 0){
return maxSpeed;
}else{
return -maxSpeed;
}
}
return oldSpeed;
}


public void update(){
ySpeed += g;
if(x + xSpeed <= 0 ){
x = - (x + xSpeed);
xSpeed *= -1.01f;
xSpeed = setSpeed(xSpeed);
}else{
float lx = getlx();
if(lx + xSpeed >= mScreenWide){
x = (mScreenWide * 2) - lx - xSpeed - SIZE;
xSpeed *= -1.01f;
xSpeed = setSpeed(xSpeed);
}else{
x += xSpeed;
}
}
if(y + ySpeed <= 0){
y = - (y + ySpeed);
ySpeed *= -0.9f;
ySpeed = setSpeed(ySpeed);
}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);
}

}

前回から変わったところを説明
まず、前回はXとYが円の中心位置だったけど、
長方形型のオブジェクトとあわせるためこれを円の左上に変更。
オブジェクトの右下はgetlxとgetlyで取得する。
インターフェイスに乗っけても良かったけど、xとyは直接アクセスって事もあるし一貫性を考えてあえてハズした。
便利ではあるのでブロック実装時に復活するかも

setSpeedメソッドは最高速度を抑止するために作ったんだけど。。。
ここいらは作り直す

if(maxSpeed < Math.abs(oldSpeed)){
if(oldSpeed > 0){
return maxSpeed;
}else{
return -maxSpeed;
}
}
return oldSpeed;
}



updateメソッド

ySpeed += g;
if(x + xSpeed <= 0 ){
x = - x - xSpeed;
xSpeed *= -1.01f;
xSpeed = setSpeed(xSpeed);
}else{
float lx = getlx();
if(lx + xSpeed >= mScreenWide){
x = (mScreenWide * 2) - lx - xSpeed - SIZE;
xSpeed *= -1.01f;
xSpeed = setSpeed(xSpeed);
}else{
x += xSpeed;
}
}
if(y + ySpeed <= 0){
y = - (y + ySpeed);
ySpeed *= -0.9f;
ySpeed = setSpeed(ySpeed);
}else{
y += ySpeed;
}

は壁との衝突判定をメソッド内でやるようにした
ロジックが冗長になるけど、左の壁にぶつかったら右の壁は判定しないようにしているので
パフォーマンスは上がると思う。
加えて、壁に衝突時に単純に速度を逆転するんじゃなくて、反転後の位置を正しい位置に置くようにした。

x = - (x + xSpeed);

例えば壁の位置が0で今のボールの左端が30、速度-40の場合、単純に速度を逆転すると

今のボールの右端30+反転した速度40で新しいボールの右端が70になってしまうけど

本当は速度30を壁までに消費し、反転して残った速度10を消費するので
新しいボールの左端は10になるのが正しい。

そこで、新しいボールの左端は壁の位置-はみ出し分としている。
下図参照


はみ出し分は左の壁の位置から(今のボールの左端+速度)を引いた値で求まる。
左の壁の位置は0なので
0-(今のボールの左端+速度)
0を省略すれば
-(今のボールの左端+速度)がはみ出し分。
上の場合を式に当てはめれば
-(今のボールの左端30+(速度-40))
=-(-10)
=10
となる。

左の壁は(今のところ)座標0だから簡単だけど

右の壁は壁の位置を入力してやらないと行けないのでちょっと複雑。

x = (mScreenWide * 2) - lx - xSpeed - SIZE;


横位置が2倍されていたりして妙なことになっているけど、

左の壁同様
新しいボール右端は壁の位置 - はみ出した分
に置けばいい。

はみ出し分は
今のボールの右端 + 速度 - 壁の位置で求まるので

新しいボールの右端は
壁の位置 - (今のボールの右端 + 速度 - 壁の位置)で求まる。

マイナスを括弧内に当てはめれば
壁の位置 - 今のボールの右端 - 速度 + 壁の位置

つまり(壁の位置 * 2) - ボールの右端 - 速度で求まる。

xはボールの左端を指定することになっているので
新しいボールの右端からボールの直径を引いて新しいボールの左端の座標を求めている。
新しいボールの左端は(壁の位置 * 2) - 今のボールの右端 - 速度 - ボールの直径
と言うのが↓

x = (mScreenWide * 2) - lx - xSpeed - SIZE;

壁に当たると1.01しているのはゲーム要素で
壁に当たる毎に速度が増していく仕組み

drawメソッドで円を描く
x,yは右上の座標なので半径分足したところを中心に円を描く

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);
}


Ballは面倒だったけど
Padは簡単


package org.firespeed.CrashBall;

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

public class Pad implements ActiveObject{

private int mScreenWide = 100;

// Pad Size
public static final int HEIGHT = 10;
public static final int WIDE = 80;
public static final int WIDE_BLOCK = WIDE /5;
public static final int HALF_WIDE = WIDE/2;

// Pad related variables.
public float x;
public float y;

private float mTouchX =100;

public float getlx(){
return(x + WIDE);
}
public float getly(){
return(y + HEIGHT);
}
public int colArea(float pointX){
if(pointX < x){
return 0;
}else{
return (int)( pointX - x)/WIDE_BLOCK + 1;
}
}

public void onTouchEvent(MotionEvent event){
if(event.getAction() == MotionEvent.ACTION_MOVE){
mTouchX = event.getX();
}
}

public Pad(int w, int h){
mScreenWide = w;
y = 400;
}

public void update(){
x = mTouchX - HALF_WIDE;
if(x < 0){
x = 0;
}else if(getlx() > m ScreenWide){
x = mScreenWide-WIDE;
}
}

public void setSize(int w, int h){
mScreenWide = w;
}

public void draw(Canvas canvas, Paint paint){
paint.setColor(Color.YELLOW);
canvas.drawRect(x, y, x + WIDE, y + HEIGHT, paint);
}


}


定数の宣言

public static final int HEIGHT = 10;
public static final int WIDE = 80;
public static final int WIDE_BLOCK = WIDE /5;
public static final int HALF_WIDE = WIDE/2;

上から高さ、幅、ボールを接触した位置の区切り幅、幅の半分

colAreaメソッドは指定された座標がパッドのどの辺か
中央が3で、左端が0、右端が6の7段階で評価する。

if(pointX < x){
return 0;
}else{
return (int)( pointX - x)/WIDE_BLOCK + 1;
}

右端より右だったら0で、
そこからパッドを5分割したサイズで評価していく

onTouchEventメソッドは画面を操作した時に呼ばれる

if(event.getAction() == MotionEvent.ACTION_MOVE){
mTouchX = event.getX();
}

指を動かしたら、それに合わせてカーソルを移動する・・・ んだけど
いったんmTouchXに移して実際の移動はupdateメソッドで行う

updateメソッドで位置を動かすんだけど・・・

x = mTouchX - HALF_WIDE;
if(x < 0){
x = 0;
}else if(getlx() > mScreenWide){
x = mScreenWide-WIDE;
}

画面外にはみ出しそうなときは動かさないのと、
xは左端なので指の位置よりサイズ半分だけ左側に描画する。
ゲーム開始画面
メッセージとパッドが出てる。


ゲーム画面、放物線を描くけど今回はGを控えめにしている。
パットに当たる位置でボールの動きが変わる。


ボールが全部落ちてしまうとゲームオーバー。


さて、次はいよいよブロックを作らないと、
パフォーマンスを考慮した衝突判定をやります。

前:新型プリウス試乗してきたよ 次:2009春の新型MacBook出ました

関連キーワード

[Android][Java][IT]

コメントを投稿する