風邪引いてすっかり作業が滞ってしまいました…みなさん体調にはご注意下さい。まだ治っていませんが頑張ってみます^^;
前回 のサンプルプログラムは、黒画面上を●が動くだけで見た目につまらなかったので、今回は背景とボールを画像にしてみることにします。
http://jp.youtube.com/watch?v=72tQzMT7FB0 に動画をアップロードしてあります。動作としてやっていることは変わりませんが、画像を使うだけで見た目が全然変わります!…と思うのですが、いかがでしょうか…?
前回 のソースコードをベースとして使います。zip ファイルをここに置いておきます。参考までに
です。
環境依存の build.xml を同梱するなど問題があったので、12/10 にファイルを変更しました。既にダウンロードした方でビルドできない方がいらっしゃいましたら、新しいファイルに入っている BallActivity.apk を使ってみて下さい。build.xml は入っていませんので、各自でプロジェクトを作成していただく等してご用意下さい。
↓zip ファイルの中身です。
今回追加したファイルは、res/drawable 配下の ball.png と wallpaper.png です。
文字通り背景として使います。Windows のペイントで描いてみました!全く絵心のない筆者ですが、それなりに見え…ませんかね?
●の代わりに動くボールとして使います。
以下、変更点があるソースコードの説明を書きます。
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="mode_ready">\n*** A Ball ***\nPress Space To Play</string> <string name="mode_pause">\nPause\nPress Space To Go Back</string> <string name="mode_end">\nGame Over\nPress Space To Play Again</string> </resources>
"score_xxxx" という名称の文字列リソースを削除しました。というのも、前回は画面の構成を
としていたのですが、今回描画速度の関係で
の様に変更したため、元々用意していたスコア表示領域がなくなったからです。まあ、今後の進み次第で文字列リソースは復活するかもしれません。
<?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"> <rio1218.ball.BallView id="@+id/ball" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/wallpaper" /> <TextView id="@+id/message" android:text="@string/mode_ready" android:visibility="visible" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textAlign="center" android:textColor="#ff444444" android:textSize="24sp" android:layout_weight="1" /> </FrameLayout>
前述の通り、前回と異なり今回の画面構成は
としています。スコア表示領域をなくしてメッセージ表示領域のみ残しました。描画速度の関係でこの様にしたのですが、詳細は BallView.java の説明に書きます。
また、BallView の背景に画像を指定しています(android:background="@drawable/wallpaper")。"@drawable/画像ファイル名称(".拡張子"を除く)"として画像リソースを指定します。
package rio1218.ball; import android.app.Activity; import android.os.Bundle; import android.view.Window; import android.widget.TextView; public class BallActivity extends Activity { private static final String BUNDLE_KEY = "ballView"; private BallView ballView; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.main); TextView messageView = (TextView)findViewById(R.id.message); ballView = (BallView)findViewById(R.id.ball); ballView.setTextView(messageView); if (null != icicle) { Bundle map = icicle.getBundle(BUNDLE_KEY); if (null != map) { ballView.restore(map); ballView.update(); return; } } ballView.setMode(BallView.MODE_READY); ballView.update(); } @Override protected void onFreeze(Bundle outState) { super.onFreeze(outState); outState.putBundle(BUNDLE_KEY, ballView.save()); } @Override protected void onPause() { super.onPause(); ballView.setMode(BallView.MODE_PAUSE); } }
前回は ballView.setTextView(scoreView, messageView) の様にスコア表示領域とメッセージ表示領域の 2 つの TextView を BallView に設定しましたが、今回は ballView.setTextView(messageView) とメッセージ表示領域のみを設定しています。
package rio1218.ball; import java.util.Map; import android.content.Context; import android.content.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; import android.widget.TextView; /** * Main view. */ public class BallView extends View implements Updatable { // Modes of this view. public static final int MODE_READY = 0; public static final int MODE_RUNNING = 1; public static final int MODE_PAUSE = 2; public static final int MODE_OVER = 3; // Frames per second, and the time between frames. private static final int FPS = 30; private static final long TIME_TO_SLEEP = (long)(1000.0/FPS); // Handler for periodic update of screen. private UpdateHandler updateHandler = new UpdateHandler(this); // Resource manager. private Resources res = getContext().getResources(); // Text area to show messages. private TextView messageView; // Player ball. private Ball ball = new Ball(); // Current mode of this view. private int mode = BallView.MODE_READY; /** * Constructor of this view class. */ public BallView(Context context, AttributeSet attrs, Map inflateParams) { super(context, attrs, inflateParams); init(); } public BallView(Context context, AttributeSet attrs, Map inflateParams, int defStyle) { super(context, attrs, inflateParams, defStyle); init(); } private void init() { setFocusable(true); ball.setDrawable(res.getDrawable(R.drawable.ball)); ball.init(); } /** * Sets view mode to the specified one. */ public void setMode(int newMode) { mode = newMode; CharSequence str = null; switch (mode) { case BallView.MODE_READY: messageView.setVisibility(View.VISIBLE); break; case BallView.MODE_PAUSE: str = res.getText(R.string.mode_pause); messageView.setText(str); messageView.setVisibility(View.VISIBLE); break; case BallView.MODE_RUNNING: messageView.setVisibility(View.INVISIBLE); break; case BallView.MODE_OVER: str = res.getText(R.string.mode_end); messageView.setText(str); messageView.setVisibility(View.VISIBLE); break; } } public void update() { if (mode == BallView.MODE_RUNNING) { ball.update(); if (! ball.isAlive()) { setMode(BallView.MODE_OVER); } invalidate(ball.getDirtyRect()); } updateHandler.sleep(TIME_TO_SLEEP); } public Bundle save() { Bundle map = new Bundle(); map.putInteger("mode", mode); ball.save(map); return map; } public void restore(Bundle map) { setMode(BallView.MODE_PAUSE); mode = map.getInteger("mode"); ball.restore(map); } public void setTextView(TextView messageView) { this.messageView = messageView; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { ball.setScreenSize(w, h); } private long lastTime; @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); ball.draw(canvas); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_SPACE) { switch (mode) { case BallView.MODE_READY: setMode(BallView.MODE_RUNNING); init(); update(); break; case BallView.MODE_RUNNING: setMode(BallView.MODE_PAUSE); update(); break; case BallView.MODE_PAUSE: setMode(BallView.MODE_RUNNING); update(); break; case BallView.MODE_OVER: setMode(BallView.MODE_RUNNING); init(); invalidate(); update(); break; } return true; } if (mode == BallView.MODE_RUNNING) { if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { ball.setDirection(Ball.UP); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { ball.setDirection(Ball.RIGHT); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { ball.setDirection(Ball.DOWN); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { ball.setDirection(Ball.LEFT); return true; } } return super.onKeyDown(keyCode, event); } }
スコア表示領域用の TextView を削除しました。理由については、onDraw() の説明を見てください。
今回はボールとして画像を表示する関係で、Ball#init() を実行する前に Ball#setDrawable() によって画像を設定します。android.content.Resources#getDrawable() によって、res/drawable 配下に格納した画像を android.graphics.drawable.Drawable として取得できます。Resources#getDrawable() に渡す引数は、R.drawable.画像ファイル名称(".拡張子" を除く) です。
前回までは、引数無しの invalidate() を実行することで画面全体を再描画していました。しかし今回背景に画像を使用する関係で、画面全体の再描画を行うと非常に遅くなります。よって、引数を渡して invalidate() を実行することで再描画領域を指定します。これにより、指定領域の背景のみが再描画された上で BallView#onDraw() が実行されます。
引数有りの invalidate() は 2 種類ありますが、ここでは android.graphics.Rect で描画領域を指定する方を使います。新たに Ball クラスに実装した Ball#getDirtyRect() によって Ball 周辺領域を Rect として取得し、それを描画領域として指定します。今回は 1 つの Ball だけが動くので、Ball 周辺を再描画すれば十分です。なお、MODE_RUNNING 以外の状態で再描画する必要も無いため、invalidate() の位置を変えました。
レイアウトファイルで予め画像を背景として指定しているので、canvas.drawColor() による背景塗りつぶしをやめました。その他、fps 描画を止めました。
fps 描画の取りやめですが、最初はそのまま残してサンプルプログラムを作ってみました。動かしてみたところ、Ball が画面上部にある(fps 描画領域に近い) 時はそこそこの速度で描画されるのですが、Ball が画面下部に移動する(fps 描画領域から遠い) につれてどんどん動きが遅くなるという現象に遭遇しました。おそらく仕様上、Ball が画面下部にある場合は画面上部のスコア表示領域と画面下部の Ball 領域を含む領域、つまりほぼ画面全体が再描画されてしまっているのではないかと思います。
これにより、今回はスコア表示領域の表示をやめました。おそらく TextView に描画を任せず自分で描画すれば良いのではないかと思っているのですが、その辺については今後調べてみます。
package rio1218.ball; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; /** * This is the class represents a single ball. */ public class Ball { // Ball related constants. public static final int RADIUS = 12; public static final int SIZE = (Ball.RADIUS * 2); public static final int VX = 5; public static final int VY = 5; // Some initial parameters. private static final int INIT_X = 160; private static final int INIT_Y = 130; // Ball direction. public static final int UP = 1; public static final int RIGHT = 2; public static final int DOWN = 3; public static final int LEFT = 4; public static final int STOP = 5; private int screenWidth; private int screenHeight; private int direction = Ball.STOP; private int alive = 1; private int x = INIT_X; private int y = INIT_Y; private Bitmap bitmap; public void setDrawable(Drawable drawable) { bitmap = Bitmap.createBitmap(Ball.SIZE, Ball.SIZE, true); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, Ball.SIZE, Ball.SIZE); drawable.draw(canvas); } public void setDirection(int direction) { this.direction = direction; } public void setPosition(int x, int y) { this.x = x; this.y = y; } public void setScreenSize(int w, int h) { screenWidth = w; screenHeight = h; } public Rect getDirtyRect() { return new Rect( x - Ball.RADIUS - Ball.VX, y - Ball.RADIUS - Ball.VY, x + Ball.RADIUS + Ball.VX, y + Ball.RADIUS + Ball.VY ); } public void init() { setDirection(Ball.STOP); setPosition(Ball.INIT_X, Ball.INIT_Y); alive = 1; } public boolean isAlive() { return (alive == 1); } public void update() { switch (direction) { case UP: y -= VY; if (y < RADIUS) { alive = 0; } break; case RIGHT: x += VX; if (x > screenWidth - RADIUS) { x = screenWidth - RADIUS; direction = LEFT; } break; case DOWN: y += VY; if (y > screenHeight - RADIUS) { alive = 0; } break; case LEFT: x -= VX; if (x < RADIUS) { x = RADIUS; direction = RIGHT; } break; } } private final Paint paint = new Paint(); public void draw(Canvas canvas) { canvas.drawBitmap(bitmap, x - Ball.RADIUS, y - Ball.RADIUS, paint); } public void save(Bundle map) { map.putInteger("screenWidth", screenWidth); map.putInteger("screenHeight", screenHeight); map.putInteger("direction", direction); map.putInteger("x", x); map.putInteger("y", y); map.putInteger("alive", alive); } public void restore(Bundle map) { screenWidth = map.getInteger("screenWidth"); screenHeight = map.getInteger("screenHeight"); direction = map.getInteger("direction"); x = map.getInteger("x"); y = map.getInteger("y"); alive = map.getInteger("alive"); } }
新たに追加したメソッドです。画面描画時に実行される draw(Canvas) では Canvas クラスのメソッドを使って描画しますが、Canvas クラスには Drawable を描画するメソッドがありません。そのため、引数の Drawable を android.graphics.Bitmap として保存しておき、draw(Canvas) 実行時に Canvas#drawBitmap() にて画像を描画します。ちなみに Drawable#setBounds() にて描画対象領域を設定しておかないと Drawable#draw() にて描画されない様です。
新たに追加したメソッドです。Ball 周辺領域を Rect に格納して返します。移動前および移動後の領域が再描画される様に値を設定しています。
これまでと違い、画面上端に到達した場合も isAlive() が 0 を返す様にしました。
円を描く代わりに、Canvas#drawBitmap() を使って保存しておいた Bitmap 内容を描画します。
サンプルコードの説明終わり。画像も表示できたことだし、次回こそゲーム性を持たせたいと思います。
FC2 Blog Ranking に参加してます。クリックよろしくお願いします!
<<B'z のニューアルバム ACTION ようやく購入 | ホーム | [Google Android SDK]タイトルやゲームオーバのメッセージを表示します。Activity の状態遷移にも対応してみます。>>
Author:いちのせ りょう
1974年北海道生まれ、育ちは沖縄/宮崎、で現在は東京在住のSEです。社会人10年目、未だにばりばり作ってます!Rio's Laboratory もよろしく…
性別:男性
車:MAZDA DEMIO
好きな音楽:B'z、The Yellow Monkey、Bon Jovi
好きなゲーム:MGS、Bio Hazard、Zeldaとか
やりたい事:F1観に行きたい、沖縄の海に潜りたい、空を飛びたい