
前回 は、画面に表示した●をキーパッドで動かしてみました。今回は SDK のサンプルゲームに倣って、文字だけですがタイトルやゲームオーバなどのメッセージを画面に表示します。また、Activity の状態遷移を調べて、アプリケーションの一時停止や復帰に対応したいと思います。
http://jp.youtube.com/watch?v=zUWC2CCqY0M に動画をアップロードしてあります。メッセージ表示の様子が分かると思います。
前回 作成した ball プロジェクトのソースコードをベースとして使います。追加したファイルを含めたプロジェクトのディレクトリ構造は、以下の通りです。
SDK のドキュメントを読むと、Activity には
の4つの状態がある様です。active と running がどう違うのかまだよく分かっていませんが…。active/running は、アプリケーションがフォーカスを持ち、ユーザが操作している状態です。paused は、フォーカスが外れていてユーザの操作対象ではないが、画面上に表示されている状態です。表示サイズが小さいアプリケーションの裏に表示されている感じでしょうか?stopped は、非表示となり完全にバックグラウンドに移動した状態です。
状態の遷移に伴って実行されるコールバックメソッドが幾つかありますが、単純なアプリケーションであれば、以下を実装しておけば良さそうです(ドキュメントおよびサンプルを見た感じ)。
これまでにも既に実装していますが、Activity 生成時に必ず実行されます。View を作成したり、様々な初期化を行う必要があります。
paused 状態(あるいは一気に stopped まで)に移行する際に実行されます。次の onPause() メソッドの前に実行されます(onFreeze() の次には必ず onPause() が実行される)。ここで状態を待避しておくと、新しく Activity が起動した際に onCreate() にその状態が引き渡されるため、状態を復帰することができます。
他の Activity が active/running に復帰する際に実行されます。本メソッドでは、未保存のデータを保存する/アニメーションの様に CPU を消費する処理を停止する、などを行います。本メソッドが終了しないと、他の Activity の復帰が行われないため、時間がかかる処理を行わない様にしないといけない様です。筆者は、onFreeze() との住み分けが いまいちよく分かりません…
SDK のサンプルゲーム(Snake/LunarLander)では、ゲーム開始やゲームオーバ時に画面上に ”Game Over" などの文字を表示してゲームっぽく見せています。文字を描画しているだけで画像を用意する必要も無さそうに見えたので、あ、これは簡単そうだと思い調べてみました。
これらのサンプルゲームでは、以下の様なレイアウトを構成して最前面に表示される TextView に文字を描いています。ちなみに、***Layout とは View を画面上にレイアウトするために用意されているクラスです。
FrameLayout(android.widget.FrameLayout) は、子として指定した View をそのまま描画します。複数の View を子として指定した場合は、後に指定された View が前に指定された View を上書きする様に描画します。
RelativeLayout(android.widget.RelativeLayout) は、親に対して、あるいは他の子に対して相対位置指定を行うことで View をレイアウトします(xxxx の右、yyyy の下など)。
今回作成するサンプルコードでは、文字を表示する部分を上下に分けて、スコアを表示する View とメッセージを表示する View を用意することにしました(↓参照)
↑で使っている LinearLayout(android.widget.LinearLayout) は、子を水平/垂直いずれかの方向に整列してレイアウトするクラスです。具体的な使い方については、サンプルコードの説明の時に説明します。
以下、ソースコードの説明を書きます。Updatable.java と UpdateHandler.java は、前回 から変更無しなので省略します。
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="score_prefix">Score:</string> <string name="score_suffix">00000</string> <string name="score_initial">Score:00000</string> <string name="mode_ready">\n*** Balls ***\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>
projectName/res/values/strings.xml というファイルには、アプリケーションで使う文字列をリソースとして定義できます。プログラム中では、R.strings.xxxx という形で id を指定してリソースを取り出します。xxxx の部分は、strings.xml 内の定義で name 属性に指定した値です。
<?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"
/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
id="@+id/score"
android:text="@string/score_initial"
android:visibility="visible"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAlign="end"
android:textColor="#ffffffff"
android:textSize="12sp"/>
<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="#ffffffff"
android:textSize="24sp"
android:layout_weight="1"/>
</LinearLayout>
</FrameLayout>
レイアウトを決定する xml ファイルは、projectName/res/layout 以下に格納します。ファイル名称は決められているわけではなく、自由に付けることができます。プログラム中でファイル名を id としてレイアウトを適用します。
前述した様に、本サンプルでのレイアウトは
とします。main.xml において、LinearLayout の orientation 属性を "vertical" にすることで、垂直方向に部品をレイアウトする様にしています。
LinearLayout の子として指定している2つの TextView ですが、LinearLayout の orientation 属性が垂直の場合、先に記述している部品が上に配置されます。TextView の属性についての説明は以下の通りです。
| text | 表示内容の初期値。@string/xxxx の様に指定すると、前述の strings.xml にて定義した xxxx という名称の文字列リソースを使用するという意味になります。 |
| visibility | 初期の表示状態。"visible" とすると表示状態、"invisible" とすると非表示状態で開始します。 |
| layout_width | 幅指定。"fill_parent" とすると親要素いっぱいに表示することになります。"wrap_content" は、表示内容に合わせることになります。 |
| layout_height | 高さ指定。layout_width と同様 |
| textAlign | 文字列の表示位置。"start" で左寄せ、"center" で真ん中、"end" で右寄せとなります。 |
| textColor | 文字列の表示色。数種類の形式で指定できますが、本サンプルでは #aarrggbb(aはアルファ値)で指定しています。 |
| textSize | 文字列のフォントサイズを指定します。 |
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 scoreView = (TextView)findViewById(R.id.score);
TextView messageView = (TextView)findViewById(R.id.message);
ballView = (BallView)findViewById(R.id.ball);
ballView.setTextView(scoreView, 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);
}
}
requestWindowFeature(Window.FEATURE_NO_TITLE) を実行し、Activity 名がタイトルとして表示されない様にしました。また、新たに用意した2つの TextView を取得して BallView に設定する様にしました。
その後、引数の有無によって新規の起動なのか以前の状態を渡されているのかを判定しています。引数の icicle が null 以外の場合は、onFreeze() で待避した情報を BallView#restore(Bundle) によって復帰します。icicle が null の場合、あるいは null ではないが待避した情報を取得できなかった場合は、BallView#setMode() によって初期状態に設定します。
なお、本サンプルでは状態に関わらずフレームレートを表示させるため、BallView#update() を onCreate() で実行することで常時アニメーション処理を行っています。onStop() を実装していないため、非表示のバックグラウンド状態でも無意味に CPU を消費しますので、ご注意ください。
BallView#save() によって保存したい情報を設定した android.os.Bundle クラスのインスタンスを取得し、引数の outState に対して Bundle#putBundle() を実行して情報を待避しています。
BallView#setMode() によりフレームレート以外の表示内容更新を止めています(実際には、同じ内容で再描画を行っています。無駄ですが…)。
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 score/messages.
private TextView scoreView;
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.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();
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 scoreView, TextView messageView)
{
this.scoreView = scoreView;
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)
{
long now = System.currentTimeMillis();
long fps = (1000 / (now - lastTime));
lastTime = now;
CharSequence str = res.getText(R.string.score_initial);
scoreView.setText(Float.toString(fps) + "fps, " + str);
canvas.drawColor(Color.DKGRAY);
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();
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);
}
}
Activity の状態とは別に、ゲームを意識して View 自体の状態を導入しました(MODE_XXXX)
状態変更を受け付けます。
| MODE_READY 指定時 | メッセージ表示用の TextView を表示します。この TextView は レイアウト用の xml ファイル で "message" という id が付いている TextView です。strings.xml の "mode_ready" という名称の文字列リソースが設定されているため、本 TextView を表示状態にするだけで、画面上に "\n*** Balls ***\nPress Space To Play"(\n は改行) と表示されます。 |
| MODE_PAUSE 指定時 | メッセージ表示用の TextView を表示しますが、表示内容を strings.xml の "mode_pause" という名称の文字列リソースに変更しています。コード中で文字列リソースを取得するために、Resources#getText() を使っています。 |
| MODE_RUNNING 指定時 | メッセージ表示用の TextView を非表示にします。 |
| MODE_OVER 指定時 | メッセージ表示用の TextView を表示しますが、表示内容を strings.xml の "mode_end" という名称の文字列リソースに変更しています。 |
MODE_RUNNING の場合のみ Ball#update() を実行して Ball を動かします。Ball が画面下端に到達する(新たに実装した Ball#isAlive() が false を返す)と、View の状態を MODE_OVER に変更してゲームオーバとなる様にしました。状態を更新した後に invalidate() で画面を再描画します。
BallActivity#onFreeze() で情報を待避するために android.os.Bundle のインスタンスを生成して、待避が必要な情報を設定します(ほとんどの情報は Ball#save() で設定)。Bundle#putXXXX() により、キーと関連付けて変数を格納することができます。restore() では逆に、Bundle#getXXXX() を使って格納済みの変数を取得して状態を復帰します。
前回は画面サイズを静的に扱いましたが、今回は onSizeChanged() を実装することで動的に取得する様にしました。
前回実行時からの経過時間を基にフレームレートを計算し、スコア表示用の TextView に表示します。ちなみに本サンプルでは希望フレームレートを前回までの 60fps から 30fps にしました。その後、背景を塗りつぶして Ball を描画します。
スペースキーの入力に応じて View の状態を変更する様にしました。また、View の状態が MODE_RUNNING の時に限ってキーパッド入力により Ball を動かす様に変更しました。キーパッドの中央のキーは使用しない様にしました。
package rio1218.ball;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
/**
* This is the class represents a single ball.
*/
public class Ball
{
// Some initial parameters.
private static final int INIT_X = 160;
private static final int INIT_Y = 130;
// Ball related constants.
private static final int RADIUS = 5;
private static final int VX = 5;
private static final int VY = 5;
// 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;
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 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) {
y = screenHeight + RADIUS;
}
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)
{
paint.setColor(Color.YELLOW);
paint.setAntiAlias(true);
canvas.drawCircle(x, y, 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");
}
}
希望フレームレートを半分にしたので、フレーム毎の速度を増やしました。また、alive というメンバを導入して、画面下端に到達すると false を返すメソッド isAlive() を追加実装しました。更に、Activity の状態遷移への対応のため save()/restore() を実装し、状態の待避/復帰を行える様にしました。
サンプルコードの説明終わり。次回は、背景と Ball に画像を表示してみたいと思います。
FC2 Blog Ranking に参加してます。クリックよろしくお願いします!
<<[Google Android SDK]背景とボールを画像にしてみました。 | ホーム | [Google Android SDK]ゲームへの第一歩。キーパッドでボールを動かしてみます。>>
Author:いちのせ りょう
1974年北海道生まれ、育ちは沖縄/宮崎、で現在は東京在住のSEです。社会人10年目、未だにばりばり作ってます!Rio's Laboratory もよろしく…
性別:男性
車:MAZDA DEMIO
好きな音楽:B'z、The Yellow Monkey、Bon Jovi
好きなゲーム:MGS、Bio Hazard、Zeldaとか
やりたい事:F1観に行きたい、沖縄の海に潜りたい、空を飛びたい