Androidでゲーム作成(1)

Androidを使ってゲームでも作りたいな、という事でAPIとか調べならが試してみました。AndroidではSwingは使えないのですが、比較的に似たインターフェイスAPIが用意されているため、Swingで簡単なゲームを作ったことがあればすぐになれるのではないでしょうか。
とりあえず、AndroidでHelloWorldくらいは作れる能力がある前提です。まったく試したことない人は、日本Androidの会のチュートリアルがオススメです。

D

自由に描画できるCanvasが欲しい - SurfaceView

ゲームでは、テキストエリアなどのGUIコンポーネントを配置していくのではなく、背景やキャラクターなどを独自に描画しなければなりません。Swingであれば、paintComponentをオーバーライドして…となるわけですが、Androidの場合はSurfaceViewという専用のViewを使います。このSurfaceViewのサブクラスとしてViewを定義し、キーイベントや描画ロジックを実装していきます。

/**
 * Game盤.
 * 
 * @author shuji
 */
public class GameBoadView extends SurfaceView {
// 中略
}

SurfaceViewの起動時にメインループスレッドなどを起動する - SurfaceHolder.Callback

SurfaceHolder.Callbackインターフェイスには、SurfaceViewの起動時や破棄時に呼び出されるコールバックメソッドを定義できます。このコールバックオブジェクトをSurfaceViewにaddCallbackメソッドを使い、登録します。
SurfaceHolder.Callbackインターフェイスは簡単に作るのであれば、Viewに実装してしまっても良いでしょう。ここでは内部クラスとして定義しました。

	/**
	 * SurfaceViewのCallbackクラス.
	 * <p>
	 * SurffaceViewの生成、破棄時などにコールバックされる.
	 * </p>
	 * 
	 * @author shuji
	 * 
	 */
	private class SurfaceCallback implements SurfaceHolder.Callback {
		public void surfaceChanged(SurfaceHolder holder, int format, int width,
				int height) {
		}

		public void surfaceCreated(SurfaceHolder holder) {
			Log.d("Yukkuri", "surfaceCreated");
		}

		public void surfaceDestroyed(SurfaceHolder arg0) {
			Log.d("Yukkuri", "surfaceDestroyed");
		}

SurfaceViewが起動しない… - setFocusable/requestFocus

微妙にはまるところが、フォーカスです。Androidでは、フォーカスが当たることで初めてそのアプリが活性化するという特性があります。従って、SurfaceViewをメインとしたゲームを実装する場合は、コンストラクタ等でフォーカスを当てるようにします。

	public GameBoadView(Context context) {
		super(context);
		getHolder().addCallback(new SurfaceCallback());
		setFocusable(true);
		requestFocus();
	}

描画する(1) - SurfaceHolder#lockCanvas #unlockCanvasAndPost

SurfaceViewにはdrawというメソッドがありますが、このメソッドをオーバーライドしただけでは何も描画されません。というのも、Androidではリソースを節約するためなのか、アプリケーションから描画を求めなければ描画されないようです。その為、キーイベントやメインループなどをトリガーとして、描画メソッドを呼び出します。
描画メソッドはdrawをオーバーライドしなくても構いませんが、描画に必要なのはandroid.graphics.Canvasインスタンスになります。このCanvasインスタンスを取得するためには、SurfaceHolderのlockCanvas メソッドを使います。lockCanvas 名前から推測できるように、マルチスレッド環境を想定した作りになっています。あるスレッドが描画処理を行っている間、他のスレッドからの描画は排他するわけです。したがって、描画後に必ずunlockCanvasAndPostメソッドを呼び出す必要があります。尚、SurfaceHolderはSurfaceViewのgetHolderメソッドから参照できます。

	void repaint() {
		Log.d("Yukkuri", "repaint");
		Canvas canvas = null;
		SurfaceHolder surfaceHolder = getHolder();
		try {
			canvas = surfaceHolder.lockCanvas();
			if (canvas == null) return;
			synchronized (surfaceHolder) {
				draw(canvas);
			}
		} finally {
			if (canvas != null) surfaceHolder.unlockCanvasAndPost(canvas);
		}
	}

描画する(2) - Drawable

描画は線分、矩形、円、ビットマップ画像などを扱うことができますが、それらはDrawableクラスのサブクラスとして定義されています。詳しくはJavaDocを参照してもらえれば解ると思うので、箱を描画するサンプルを提示しますので、雰囲気はつかんでください。

		ShapeDrawable rect = new ShapeDrawable(new RectShape());
		Paint paint = rect.getPaint();
		paint.setColor(0xFF000000);
		rect.setBounds(0, 0, getWidth(), getHeight());
		rect.draw(canvas);

キャラクターを描画する - BitmapDrawable

ゲームのキャラクターやカードは、BitmapDrawableを使い描画することができます。画像の形式はjpgやpngが使用できますが、gifには対応していないので注意してください。読み込むパッケージと同じ場所に画像を配置し、getClass().getResourceAsStreamを使って読み込むのが一番簡単な方法です。

		BitmapDrawable drawable = new BitmapDrawable(getClass().getResourceAsStream("youmu.png"));

描画時は、ShapeDrawable と同様にsetBoundsメソッドを使用して、描画エリアを指定してください。この時、左上の座標だけでなく、右下の座標も必要になります。言い換えれば、元画像のサイズよりも大きく設定すると自動的に拡大されて表示されます。尚、元画像のサイズを取得するには、getIntrinsicWidth/getIntrinsicHeightが使用できます。
色々とゲームを作るのであれば、次のようなモデルクラスに移動やアクションを含めて定義するのが便利です。

public class Component {

	BitmapDrawable drawable;
	int a = 5; // 移動量
	int width;
	int height;
	Rect bounds;

	public Component(InputStream input) {
		this.drawable = new BitmapDrawable(input);
		width = drawable.getIntrinsicWidth();
		height = drawable.getIntrinsicHeight();
		bounds = new Rect(0, 0, width, height);
	}

	public void left() {
		this.bounds.left -= a;
		this.bounds.right -= a;
		this.drawable.setBounds(bounds);
	}

	public void right() {
		this.bounds.left += a;
		this.bounds.right += a;
		this.drawable.setBounds(bounds);
	}

	public void down() {
		this.bounds.top += a;
		this.bounds.bottom += a;
		this.drawable.setBounds(bounds);
	}

	public void up() {
		this.bounds.top -= a;
		this.bounds.bottom -= a;
		this.drawable.setBounds(bounds);
	}
	
	public void scaleUp() {
		width = (int) (width * 1.2);
		this.bounds.right = this.bounds.left + width;
		height = (int) (height * 1.2);
		this.bounds.bottom = this.bounds.top + height;
		this.drawable.setBounds(bounds);
	}

	public void setPosition(int x, int y) {
		bounds.left = x;
		bounds.right = x + width;
		bounds.top = y;
		bounds.bottom = y + height;
	}

}

キーイベントを拾いたい - onKeyDown

SurfaceViewではリスナーを登録しなくとも、デフォルトでキーイベントを受け取るメソッドが定義されています。感覚としてはAppletに近いですね。メソッドではキーコードを引数として受け取るので、これを判断材料にキャラクターの移動などを行えばよいでしょう。

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		Log.d("Yukkuri", "onKeyDown" + event);
		switch (keyCode) {
		case KeyEvent.KEYCODE_DPAD_UP:
			youmu.up();
			break;
		case KeyEvent.KEYCODE_DPAD_RIGHT:
			youmu.right();
			break;
		case KeyEvent.KEYCODE_DPAD_DOWN:
			youmu.down();
			break;
		case KeyEvent.KEYCODE_DPAD_LEFT:
			youmu.left();
			break;
		case KeyEvent.KEYCODE_DPAD_CENTER:
			youmu.scaleUp();
			break;
		default:
			break;
		}
		repaint();
		return true;
	}

次回は定期的に処理を行う、メインループの作り方を説明します。尚、ソースコード一式はこちらから。