Androidでのゲームの作り方
すっかり更新を忘れてしまった。
気付いたらAndroidのSDKは1.0r1となっていた。RC1じゃないんだ。
米国でAndroidの年内の発売が発表された。
WSJは大嘘つき。
0.9から1.0r1になってまたしても複数の自作プログラムが動かなくなった。
自分だけならまだしも公式ブログにて公開された複数の"Apps for Andorid"も動かなくなったまま放置されている。
GlobalTimeだけでも直してください。
お願いします。
Android勉強会にてピカイチに輝いていた村上さんがご自身のブログにてまた輝いていた。
最高です。
Androidネタもぜひお願いいたします。
本日、Androidユーザー会の勉強会に参加してきました。
楽しかったです。
暑かったです。
空調の予約だけはどうぞ忘れないでください。
お願いします。
SDK WGのリーダーをされていらっしゃる江川さんがとても参考になるお話をされていた。
特に興味深かったのはセンサーまわり。
江川さんの公開資料にはなかったと思ったけど、講演にて使用されていたセンサーシミュレーターはこちら。
http://www.openintents.org/en/node/23
http://code.google.com/p/openintents/wiki/SensorSimulator
これはゲームを作りたくなりますね。
でも以下はセンサーは使ってません。
すみません。
Androidでのゲームの作り方
SDKに付属してくるLunar Landerのソースを読まれるのが一番てっとりばやいのです。
最近公開されたDivideAndConquerはまたちょっと違った作り方、かつタッチパネルを用いたゲームになっているのでそちらを参照するのも良いでしょう。
ここではLunar Landerについて極簡単に説明します。
長くなるのでソースは添付しません。
お手元にソースを用意して見ながら読んでみてください。
画面の設計
基本はSurfaceViewです。
こちらは画面の描画を直接行うことができる奇特なViewです。
画面更新が一番速いとGoogle認定スペシャリストの安生さんのお墨付きです。
Lunar Landerでは主に2つのViewを用いてゲームを作成しています。
1つはゲーム画面、もう1つはポーズやゲームオーバーのメッセージを表示するTextViewです。
まずFrameLayoutを用意します。
これは最も基本なLayoutで複数のViewを重ねるものです。
SurfaceViewをゲーム画面用に用意してFrameLayoutに追加します。
次にRelativeLayoutを用意します。
これはテキストの表示位置をど真ん中にレイアウトするために必要としています。
RelativeLayoutにTextViewを追加してそれを画面の真ん中に設定します。
ButtonのGravityにhorizontal_centerを設定。
そして重要なのがcenter_in_parentの設定です。
なぜなのかよくわかりませんが、gravityにCENTERを設定する方法では画面の真ん中になりません。
必ずcenter_in_parentを用いる必要があります。
通常のViewではHandlerを用いて画面更新を依頼することになるのですが、SurfaceViewでは自分で用意したThreadの中でCanvasを取得して画面領域をロックして、描画を終わったら一気に更新ということが可能です。
JavaAppletにてゲームを作られた方ならダブルバッファのテクニックを覚えていらっしゃると思いますが、Androidではプラットフォームがダブルバッファを行ってくれていますので自分でちらつき対策を行う必要がありません。
ゲームのメインループ
この画面の更新のループには自作のThreadを用意します。
Threadは名無しではなく、名前付きのThreadの内部クラスを用います。
なぜThreadが名無しではいけないかといいますと、名無しでThreadを作成すると純粋にThreadとしてのメソッドしか外側から呼び出すことができないため面倒なのです。
Threadには定番のrun()がありますのでそのなかのループでゲームのループを行います。
ループで行う基礎はCanvasの取得、ゲームのロジックの実行、Canvasへの描画、Canvasの開放とそれにともなう画面の更新です。
Canvasへの描画は通常は画面全体を対象としますが、画面のうち更新する必要がある領域がわかっている場合には引数にRectを用いて領域指定ができるようです。
これで速くなるかもしれませんが、筆者は測っていないのでチューニング命な方は試してみてください。
Canbasへの描画ですが、resのdrawableを用いてBitmapを描画しています。
Androidは回転・拡大縮小・変形などライブラリにて自由自在にできますのでオーダインとか頑張れば作れてしまうかもしれません。
キー入力
キー入力にはViewのonKeyDownとonKeyUpのイベント関数を用います。
これはJavaAppletと同じですね。
JavaAppletと同じく、キーの同時押しを実現するにはkeyDownの情報を一度ステータス変数に保存しておき、後からそれらをまとめて処理する方法が良いでしょう。
このとき、キー入力はViewのメソッドであることに注意してください。
メインループはThreadです。
Javaでは内部クラスのメソッドは一定の条件下で外部のメソッドや変数にアクセスできることを利用します。
メッセージの表示
アプリ実行時やポーズ、ゲームオーバー時にメッセージを表示するにはTextViewを更新しなければなりません。
この時に注意が必要なのはAndroidではViewへの更新はほとんどの場合、Handlerを経由してリクエストしなければならないことです。
Handlerを用いずにThreadの中からViewへ直接更新を実行すると動的例外が発生しますのでこれも経験と思って御自身で試してみてください。
タイトル画面でもデモ画面でも全部Canvasに書くのが当たり前だという硬派なゲームプログラマの方でしたらTextViewなんか必要ないですね。
その場合にはThreadのrunでloopを複数作ってゲーム上のステータス毎に画面表示を行えば大丈夫です。
Threadのライフタイムはrunの実行が全てですのでrunはいくつでもループを持ってかまいませんし、名前付きタグをうまく用いればアセンブラやC言語で書くプログラムと同じような状態遷移を書くことも可能でしょう。
[Android] Androidのゲームの作り方・実践編
上に書いたことだけわかればAndroidでゲームを作るのは難しくないと思います。
そんなわけで実践編です。
先日移植したアレを修正しました。
先日移植したアレはSurfaceViewを用いていながらThreadも使わず全てHandlerにて処理しておりました。
今回はLunar Landerに習ってThreadとlockCanvasによるループを用いています。
変更点は次のとおりです。
- 画面のカラー化
- 影の追加
- 回転軸の変更
- 回転補正の追加
- Nextミノの表示
- 重力の採用
- 50ライン毎に重力アップ
- ゲームオーバーの追加(笑
操作はパッドとセンターボタン、エミュレータではZとXも使用可です。
Core2Duo2.4GHzのノートPCにて53fps出ています。
かなり速いですね。
AMDの結構古いAtholon 64 X2 3800+ 2GHzですと45fps程度になります。
DalvikVMにJITのない純粋なインタプリタ、かつエミュレータな環境ではCPU性能が一番重要かと思われます。
恐らく実機ではこのような無茶な性能はでないと思いますが実機を入手する予定のある幸運な方にはぜひ実行してみてfpsの値をご報告して頂ければと思います。
ソースは以下。
今回もはてなに大迷惑なソースの直接公開。
リソースは一切使っていません。XMLもなし。
プロジェクトを作成してJavaのsrcだけ置き換えれば動きます。
遊んでみてください。
package minghai.practice4; import java.util.LinkedList; import java.util.Random; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.view.Gravity; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.RelativeLayout; import android.widget.TextView; public class Droris extends Activity { private enum KeyBits { LEFT (1<<0), RIGHT (1<<1), UP (1<<2), DOWN (1<<3), ROTATE_LEFT (1<<4), ROTATE_RIGHT (1<<5), HOLD (1<<6); int bit; KeyBits(int b) { bit = b; } public int setBit(int b) { return bit | b; } public int resetBit(int b) { return bit & ~b; } public boolean isOn(int b) { return (bit & b) > 0; } } private class FieldView extends SurfaceView implements SurfaceHolder.Callback { final private class Matrix { public int[][] m; public Matrix(int[][] arg) { this.m = arg; } } final private class Block { public Matrix[] rotation; public int color; private int direction = 0; public int initX; public int initY; private Block() { super(); } public Block(int[][][] rotation, int color, int initX, int initY) { int c = rotation.length; this.rotation = new Matrix[c]; for (int i = 0; i < c; i++) { this.rotation[i] = new Matrix(rotation[i]); } this.color = color; this.initX = initX; this.initY = initY; } public Block clone() { Block b = new Block(); b.rotation = this.rotation; b.color = this.color; b.initX = this.initX; b.initY = this.initY; b.direction = 0; return b; } public Matrix getMatrix() { return rotation[direction]; } public void rotateLeft() { if (++direction >= rotation.length) { direction = 0; } } public void rotateRight() { if (--direction < 0) { direction = rotation.length - 1; } } } int[][][] orange = { { {0,0,0}, {1,1,1}, {1,0,0} }, { {0,1,0}, {0,1,0}, {0,1,1} }, { {0,0,0}, {0,0,1}, {1,1,1} }, { {1,1,0}, {0,1,0}, {0,1,0} } }; Block orangeBlock = new Block(orange, 0xFFFFB000, 3, -1); int[][][] blue = { { {0,0,0}, {2,2,2}, {0,0,2} }, { {0,2,2}, {0,2,0}, {0,2,0} }, { {0,0,0}, {2,0,0}, {2,2,2} }, { {0,2,0}, {0,2,0}, {2,2,0} } }; Block blueBlock = new Block(blue, Color.BLUE, 3, -1); int[][][] yellow = { { {3,3}, {3,3} } }; Block yellowBlock = new Block(yellow, Color.YELLOW, 4, 0); int[][][] cyan = { { {0,0,0}, {4,4,4}, {0,4,0} }, { {0,4,0}, {0,4,4}, {0,4,0} }, { {0,0,0}, {0,4,0}, {4,4,4} }, { {0,4,0}, {4,4,0}, {0,4,0} } }; Block cyanBlock = new Block(cyan, Color.CYAN, 3, -1); int[][][] green = { { {0,0,0}, {5,5,0}, {0,5,5} }, { {0,0,5}, {0,5,5}, {0,5,0} } }; Block greenBlock = new Block(green, Color.GREEN, 3, -1); int[][][] magenta = { { {0,0,0}, {0,6,6}, {6,6,0} }, { {6,0,0}, {6,6,0}, {0,6,0} } }; Block magentaBlock = new Block(magenta, Color.MAGENTA, 3, -1); int[][][] red = { { {0,0,0,0}, {7,7,7,7}, {0,0,0,0}, {0,0,0,0} }, { {0,0,7,0}, {0,0,7,0}, {0,0,7,0}, {0,0,7,0} } }; Block redBlock = new Block(red, Color.RED, 3, -1); private final Random mRand = new Random(System.currentTimeMillis()); // 使用される全てのブロック // ここからランダムに選択し、cloneにてコピーした新しいブロックインスタンスを用いる LinkedList<Block> blocks; // 現在画面上のブロック Block block; int posx, posy; static final private int FRAME_SIZE = 5; static final private int BLOCK_SIZE = 16; static final private int mapWidth = 10; static final private int mapHeight = 20; int[][] map = new int[mapHeight][]; private SurfaceHolder mSurfaceHolder; private int keyState; private int lineCounter = 0; private int gravity = 1; private long lastDropTime; private long lastMoveTime; class MyThread extends Thread { private boolean mRun = true; private int frameCount = 0; private final long startTime = System.currentTimeMillis(); private int fps; @Override public void run() { lastDropTime = System.currentTimeMillis(); lastMoveTime = lastDropTime; while (mRun) { Canvas c = null; long currentTime = System.currentTimeMillis(); frameCount++; if (currentTime - lastMoveTime > 100) { doMove(); lastMoveTime = currentTime; } fps = (int)(frameCount * 1000 / (currentTime - startTime)); if (currentTime - lastDropTime > 500) { doDrop(); lastDropTime = currentTime; //lastMoveTime = currentTime; } try { c = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) { doDraw(c); } } finally { // do this in a finally so that if an exception is thrown // during the above, we don't leave the Surface in an // inconsistent state if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } } } public void quit() { mRun = false; } public int getFPS() { return fps; } }; final private MyThread mThread = new MyThread(); private void doDrop() { if (!check(block.getMatrix().m, posx, posy + 1)) { mergeMatrix(block.getMatrix().m, posx, posy); clearRows(); Block tmp = blocks.remove(mRand.nextInt(blocks.size())); futureBlocks.add(tmp); blocks.add(block.clone()); block = futureBlocks.removeFirst(); posx = block.initX; posy = block.initY; if (map[0][5] != 0) { Message m = mHandler.obtainMessage(); mHandler.sendMessage(m); } return; // End if merged } int tg = gravity - 1; posy++; while (tg-- > 0) { if (check(block.getMatrix().m, posx, posy + 1)) { posy++; } else { return; } } } private LinkedList<Block> futureBlocks; public FieldView(Context context) { super(context); mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); mSurfaceHolder.setSizeFromLayout(); setFocusable(true); setFocusableInTouchMode(true); requestFocus(); } public void initGame() { for (int y = 0; y < mapHeight; y++) { map[y] = new int[mapWidth]; for (int x = 0; x < mapWidth; x++) { map[y][x] = 0; } } blocks = new LinkedList<Block>(); blocks.add(orangeBlock); blocks.add(blueBlock); blocks.add(yellowBlock); blocks.add(cyanBlock); blocks.add(greenBlock); blocks.add(magentaBlock); blocks.add(redBlock); Block tmp = blocks.remove(mRand.nextInt(blocks.size())); block = tmp.clone(); posx = block.initX; posy = block.initY; futureBlocks = new LinkedList<Block>(); for (int i = 0; i < 3; i++) { Block tmp2 = blocks.remove(mRand.nextInt(blocks.size())); futureBlocks.add(tmp2); } lineCounter = 0; gravity = 1; } private void commonPaint(Canvas canvas, ShapeDrawable rect, int[][] matrix, int offsetx, int offsety) { int h = matrix.length; int w = matrix[0].length; for (int y = 0; y < h; y ++) { for (int x = 0; x < w; x ++) { int k = matrix[y][x]; if (k != 0) { int color; switch (k & 0x07) { case 1: color = orangeBlock.color; break; case 2: color = blueBlock.color; break; case 3: color = yellowBlock.color; break; case 4: color = cyanBlock.color; break; case 5: color = greenBlock.color; break; case 6: color = magentaBlock.color; break; case 7: color = redBlock.color; break; default: color = 0; break; } if ((k & 0x10) != 0) { int red = Color.red(color); int blue = Color.blue(color); int green = Color.green(color); color = Color.argb(0x80, red, green, blue); } rect.getPaint().setColor(color); int px = (x + offsetx) * BLOCK_SIZE; int py = (y + offsety) * BLOCK_SIZE; rect.setBounds(px, py, px + BLOCK_SIZE, py + BLOCK_SIZE); rect.draw(canvas); } } } } private void paintMatrix(Canvas canvas, int[][] matrix, int offsetx, int offsety, int color) { ShapeDrawable rect = new ShapeDrawable(new RectShape()); rect.getPaint().setColor(color); commonPaint(canvas, rect, matrix, offsetx, offsety); } private void paintShadow(Canvas canvas, int[][] matrix, int offsetx, int offsety, int color) { int y = posy; while (check(matrix, posx, y)) { y++; } if (y > 0) y = y - 1; if (y == posy) return; // ブロックと影が重なったら描かない ShapeDrawable rect = new ShapeDrawable(new RectShape()); rect.getPaint().setStyle(Style.STROKE); rect.getPaint().setStrokeWidth(5); int c = color & 0x7FFFFFFF; rect.getPaint().setColor(c); commonPaint(canvas, rect, matrix, offsetx, y); } // ブロックがマップ上のその位置に存在できるかどうかの確認 // できるならtrue, できないならfalse boolean check(int[][] block, int offsetx, int offsety) { int blockWidth = block[0].length; int blockHeight = block.length; // ブロックの全ての点を舐める for (int y = 0; y < blockHeight; y ++) { int ry = y + offsety; // ブロックは上だけ画面外でも良い if (ry < 0) continue; for (int x = 0; x < blockWidth; x ++) { int b = block[y][x]; if (b == 0) continue; // ここから下はブロックに点有り // ブロックの点が画面外にあるならアウト if (ry >= mapHeight) return false; int rx = x + offsetx; if (rx < 0 || rx >= mapWidth) { return false; } // ブロックの点が画面内の瓦礫と重なるならアウト int m = map[ry][rx]; if (m != 0) { return false; } } } return true; } void mergeMatrix(int[][] block, int offsetx, int offsety) { for (int y = 0; y < block.length; y ++) { int ry = y + offsety; if (ry < 0) continue; for (int x = 0; x < block[0].length; x ++) { if (block[y][x] != 0) { map[ry][offsetx + x] = block[y][x] | 0x10; } } } } void clearRows() { // 埋まった行は消す。nullで一旦マーキング for (int y = 0; y < mapHeight; y ++) { boolean full = true; for (int x = 0; x < mapWidth; x ++) { if (map[y][x] == 0) { full = false; break; } } if (full) map[y] = null; } // 新しいmapにnull以外の行を詰めてコピーする int[][] newMap = new int[mapHeight][]; int y2 = mapHeight - 1; for (int y = mapHeight - 1; y >= 0; y--) { if (map[y] == null) { continue; } else { newMap[y2--] = map[y]; } } // 消去した行が無い場合 if (y2 == -1) { map = newMap; return; } //重力は50行毎に強くなる lineCounter += y2 + 1; gravity = (int)(lineCounter / 50) + 1; // 消えた行数分新しい行を追加する for (int i = 0; i <= y2; i++) { int[] newRow = new int[mapWidth]; for (int j = 0; j < mapWidth; j++) { newRow[j] = 0; } newMap[i] = newRow; } map = newMap; } /** * Draws the 2D layer. */ void doDraw(Canvas canvas) { // future blocks canvas.drawColor(0xEFFFFFFF); Paint t = new Paint(); canvas.drawText("FPS: " + mThread.getFPS(), 250, 20, t); canvas.drawText("Line: " + lineCounter, 250, 300, t); canvas.drawText("Gravity: " + gravity, 234, 310, t); canvas.save(); canvas.translate(FRAME_SIZE, BLOCK_SIZE * 2); paintNextBlock(canvas); canvas.restore(); // field canvas.translate(0, 70); ShapeDrawable rect = new ShapeDrawable(new RectShape()); Paint p = rect.getPaint(); final int field_width = mapWidth * BLOCK_SIZE; final int field_height = mapHeight * BLOCK_SIZE; rect.setBounds(0, 0, field_width + FRAME_SIZE * 2, field_height + FRAME_SIZE * 2); p.setColor(0xFF000000); rect.draw(canvas); canvas.translate(FRAME_SIZE, FRAME_SIZE); rect.setBounds(0, 0, field_width, field_height); p.setColor(0xFFCCCCCC); rect.draw(canvas); int[][] tetra = block.getMatrix().m; paintMatrix(canvas, tetra, posx, posy, block.color); paintMatrix(canvas, map, 0, 0, 0xFF808080); paintShadow(canvas, tetra, posx, posy, block.color); } private void paintNextBlock(Canvas canvas) { Block b = futureBlocks.getFirst(); int[][] m = b.getMatrix().m; int last_width = m[0].length; paintMatrix(canvas, m, b.initX, b.initY, b.color); canvas.translate(BLOCK_SIZE * (b.initX + last_width + 0.5f), BLOCK_SIZE); canvas.scale(0.5f, 0.5f); for (int i = 1; i < futureBlocks.size(); i++) { b = futureBlocks.get(i); paintMatrix(canvas, b.getMatrix().m, 0, b.initY, b.color); m = b.getMatrix().m; last_width = m[0].length + 1; // 空白を1個空ける if (last_width == 2) last_width++; canvas.translate(BLOCK_SIZE * last_width, 0); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_Z: keyState = KeyBits.ROTATE_LEFT.setBit(keyState); break; case KeyEvent.KEYCODE_X: keyState = KeyBits.ROTATE_RIGHT.setBit(keyState); case KeyEvent.KEYCODE_DPAD_RIGHT: keyState = KeyBits.RIGHT.setBit(keyState); break; case KeyEvent.KEYCODE_DPAD_LEFT: keyState = KeyBits.LEFT.setBit(keyState); break; case KeyEvent.KEYCODE_DPAD_UP: keyState = KeyBits.UP.setBit(keyState); break; case KeyEvent.KEYCODE_DPAD_DOWN: keyState = KeyBits.DOWN.setBit(keyState); break; default: return false; } return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent msg) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_Z: //keyState = KeyBits.ROTATE_LEFT.resetBit(keyState); break; case KeyEvent.KEYCODE_X: //keyState = KeyBits.ROTATE_RIGHT.resetBit(keyState); break; case KeyEvent.KEYCODE_DPAD_RIGHT: keyState = KeyBits.RIGHT.resetBit(keyState); break; case KeyEvent.KEYCODE_DPAD_LEFT: keyState = KeyBits.LEFT.resetBit(keyState); break; case KeyEvent.KEYCODE_DPAD_UP: //keyState = KeyBits.UP.resetBit(keyState); break; case KeyEvent.KEYCODE_DPAD_DOWN: keyState = KeyBits.DOWN.resetBit(keyState); break; default: return false; } return true; } public void doMove() { // 回転補正: 回転後に±1の範囲で補正を行う。壁蹴り。回し入れ。 // 右優先、陰謀有り if (KeyBits.ROTATE_LEFT.isOn(keyState)) { block.rotateLeft(); if (!check(block.getMatrix().m, posx, posy)) { if (check(block.getMatrix().m, posx + 1, posy)) { // 陰謀 posx++; } else if (check(block.getMatrix().m, posx - 1, posy)) { posx--; } else { block.rotateRight(); } } keyState = KeyBits.ROTATE_LEFT.resetBit(keyState); } if (KeyBits.ROTATE_RIGHT.isOn(keyState)) { block.rotateRight(); if (!check(block.getMatrix().m, posx, posy)) { if (check(block.getMatrix().m, posx + 1, posy)) { // 陰謀 posx++; } else if (check(block.getMatrix().m, posx - 1, posy)) { posx--; } else { block.rotateLeft(); } } keyState = KeyBits.ROTATE_RIGHT.resetBit(keyState); } if (KeyBits.RIGHT.isOn(keyState)) { if (check(block.getMatrix().m, posx + 1, posy)) { posx = posx + 1; } } if (KeyBits.LEFT.isOn(keyState)) { if (check(block.getMatrix().m, posx - 1, posy)) { posx = posx - 1; } } if (KeyBits.UP.isOn(keyState)) { int y = posy; while (check(block.getMatrix().m, posx, y)) { y++; } if (y > 0) posy = y - 1; keyState = KeyBits.UP.resetBit(keyState); doDrop(); } if (KeyBits.DOWN.isOn(keyState)) { if (check(block.getMatrix().m, posx, posy + 1)) { posy++; } } } @Override public void surfaceChanged(SurfaceHolder surfaceholder, int i, int j, int k) { surfaceholder.setFixedSize(j,k); } @Override public void surfaceCreated(SurfaceHolder surfaceholder) { // start the thread here so that we don't busy-wait in run() // waiting for the surface to be created mThread.start(); } @Override public void surfaceDestroyed(SurfaceHolder surfaceholder) { // we have to tell thread to shut down & wait for it to finish, or else // it might touch the Surface after we return and explode boolean retry = true; mThread.quit(); while (retry) { try { mThread.join(); retry = false; } catch (InterruptedException e) { } } } } Handler mHandler = new Handler() { @Override public void handleMessage(Message m) { //mStatusText.setVisibility(m.getData().getInt("viz")); mStatusText.setVisibility(View.VISIBLE); //mStatusText.setText(m.getData().getString("text")); } }; FieldView mFieldView; TextView mStatusText; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); FrameLayout fl = new FrameLayout(this); mFieldView = new FieldView(this); fl.addView(mFieldView); RelativeLayout rl = new RelativeLayout(this); //rl.setLayoutParams(new ViewGroup.LayoutParams( // ViewGroup.LayoutParams.FILL_PARENT, // ViewGroup.LayoutParams.FILL_PARENT)); mStatusText = new TextView(this); mStatusText.setId(1); RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp.addRule(RelativeLayout.CENTER_IN_PARENT, 1); mStatusText.setGravity(Gravity.CENTER_HORIZONTAL); mStatusText.setTextSize(40); mStatusText.setText("GAME OVER"); mStatusText.setTextColor(Color.BLACK); mStatusText.setVisibility(View.INVISIBLE); rl.addView(mStatusText, lp); fl.addView(rl); setContentView(fl); } @Override protected void onResume() { super.onResume(); mFieldView.initGame(); Looper.myQueue().addIdleHandler(new Idler()); } @Override protected void onPause() { super.onPause(); mFieldView.mThread.quit(); } @Override protected void onStop() { super.onStop(); mFieldView.mThread.quit(); } // Allow the activity to go idle before its animation starts class Idler implements MessageQueue.IdleHandler { public Idler() { super(); } public final boolean queueIdle() { return false; } } }