Beginning Android 2D Video Games

Android上で2Dゲーム作成を始めてみたい方へ

出力:Viewを使った画面表示

CanvasとPaintを使った描画

ViewやSurfaceViewによる描画では,CanvasというUIスレッドにあるオブジェクトに対して,Paintクラスに規定された各種の描画手段を用いて図形や画像を描画します.
このとき,CanvasオブジェクトはUIスレッドに対して1つなので,複数のスレッドから直接描画指示を出すことはできません.
そのため,SurfaceViewでは,Canvasオブジェクトに対して複数のスレッドが順番に書き込めるよう排他処理機能を提供し,これを利用するようになっています.

ViewやSurfaceViewのオブジェクトから見ると,Canvasオブジェクトは表面に乗った紙面(のレイヤー)であり,Paintオブジェクトはここに図形や画像を描く腕と考えることができます.

f:id:hidenaoA:20130325175558p:plain

Viewを継承したカスタムビューでの連続描画

Viewは,AndroidGUI部品の最も原始的なクラスのひとつであり,画面表示を行う○○ViewやLayoutなどは全てがこのクラスを継承しています.
このViewクラスを継承し,描画内容を全て記述することで,独自のカスタムビューを作成することが可能です.

Viewでの描画はonDraw()メソッドをオーバーライドする形で実装します.
View(あるいは,Viewを継承したクラス)による描画は,それほど高速にCanvasを書き換えるようにはできていません.
このため,滑らかなアニメーションには不向きですが,ある程度の間隔で画面を書き換えるボードゲームの画面作成のような用途には利用できます.

Viewによる連続描画は,invalidate()というメソッドを呼び出すことで,onDraw()を再実行して実現します.
次節に示す実装例のように,スレッドを用いない場合は,下図の様にonDraw()メソッド中でinvalidate()を呼び出すことで連続描画を行うことになります.

f:id:hidenaoA:20130325175600p:plain

さらに,スレッドを用いる場合は,Viewを継承したカスタムビューのクラスにRunnableインタフェースのrun()メソッドとスレッドの開始・停止処理を加えることで,マルチスレッドによる任意のタイミングでの連続描画が可能になります.

f:id:hidenaoA:20130325175601p:plain

実装例

以下の実装例では,スレッドを用いずにViewを継承したカスタムビューで連続描画を行っています.
特に,ゲームなどでカスタムビューを用いる場合は,アセット(assets/中の画像や音声ファイル等)の読み込みをコンストラクタで行うようにし,描画処理ではメンバ変数とした描画位置や画像を参照するだけにするよう気をつけます*1

package jp.ac.bunkyo.a2dgames;

/**
 * Viewによるカスタムビューでの描画をテスト表示するActivity
 * @author Hidenao Abe (hidenao@shonan.bunkyo.ac.jp)
 */

/* Copyright 2013 Hidenao Abe (hidenao@shonan.bunkyo.ac.jp)

本ソースコードは,Apache License Version 2.0(「本ライセンス」)に基づいてライセンスされます。あなたがこのファイルを使用するためには、本ライセンスに従わなければなりません。本ライセンスのコピーは下記の場所から入手できます。
http://www.apache.org/licenses/LICENSE-2.0
適用される法律または書面での同意によって命じられない限り、本ライセンスに基づいて頒布されるソフトウェアは、明示黙示を問わず、いかなる保証も条件もなしに「現状のまま」頒布されます。本ライセンスでの権利と制限を規定した文言については、本ライセンスを参照してください。
(この日本語訳は,http://sourceforge.jp/projects/opensource/wiki/licenses%2FApache_License_2.0によるものです)
*/

import java.io.IOException;
import java.io.InputStream;
import java.util.Random;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Typeface;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

public class RenderViewTest extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		//Activityをフルスクリーン表示にする
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
				WindowManager.LayoutParams.FLAG_FULLSCREEN);

		super.onCreate(savedInstanceState);
		//setContentView(R.layout.activity_render_view_test);

		setContentView(new MyRenderView(this));

	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.render_view_test, menu);
		return true;
	}

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        super.onOptionsItemSelected(item);
        switch(item.getItemId()){ //itemを表示するため,res/menu/*.xmlにItem要素を追加すること
        case R.id.menu_finishing :
            //強制的にActivityを終了する
        	finish();
            return true;
        }
        return false;
    }

    class MyRenderView extends View{

    	Paint paint; //Viewの持つCanvasへの描画機能(以下で紹介するのはごく一部)
    	Random rand; //乱数発生器
    	int screen_width, screen_height;

    	Bitmap bitmap;
    	Typeface font;


    	public MyRenderView(Context context){ //カスタムビュークラスのコンストラクタ(初期化を実行)
    		super(context);
    		paint = new Paint();
    		rand = new Random();

    		//screen_width = this.getWidth();
    		//screen_height = this.getHeight();

    		//Bitmapなどメモリを消費するオブジェクトはなるべく初期化時に読み込む
    		try{
    			AssetManager a_manager = context.getAssets();
    			InputStream is = a_manager.open("test-star_small.png");
    			bitmap = BitmapFactory.decodeStream(is);
    			is.close();
    		} catch (IOException e){

    		} finally {

    		}


    	}

    	@Override
    	protected void onDraw(Canvas canvas){//描画を担当するメソッド

    		canvas.drawRGB(255,255,255); //キャンバスをRGB(255,255,255)で塗りつぶす

    		screen_width=canvas.getWidth();
    		screen_height=canvas.getHeight();


    		//直線の描画((0,0)のスクリーンの左上隅から,ランダムに指定する座標まで,赤い直線をひく)
    		paint.setColor(Color.RED); //Colorクラスの別の色,あるいはRGB値でを指定可能
    		canvas.drawLine(0, 0, rand.nextInt(screen_width), rand.nextInt(screen_height), paint);

    		//円の描画
    		paint.setStyle(Style.STROKE); //輪郭を描画
    		paint.setColor(0xff00ff00); //RGB8888で色を指定
    		canvas.drawCircle(rand.nextFloat()*screen_width,rand.nextFloat()*screen_height,rand.nextInt(100)+1, //中心の座標(x, y)
    				paint);

    		//四角形を描画
    		paint.setStyle(Style.FILL); //塗りつぶしで描画に設定
    		paint.setColor(0x770000ff);
    		canvas.drawRect(rand.nextInt(screen_width-100), rand.nextInt(screen_height-100), //開始地点座標 (x, y)
    				rand.nextInt(screen_width)+rand.nextInt(100), rand.nextInt(screen_height)+rand.nextInt(100), //終了地点座標 (x, y)
    				paint);

    		//Bitmap(画像)を描画(drawBitmapメソッドについては,様々な指定方法がある)
    		canvas.drawBitmap(bitmap, rand.nextInt(screen_width-bitmap.getHeight()), rand.nextInt(screen_width-bitmap.getWidth()), paint);

    		//フォントを指定して文字列を表示
    		paint.setColor(Color.BLACK);
    		//paint.setTypeface(font);

    		try{
    			Thread.sleep(1000);//次の描画まで1000msecの間隔を空ける
    		}catch(InterruptedException e){

    		}

    		invalidate(); //描画を再実行(別スレッドからの呼び出しでもonDraw()の内容を実行する)
    	}
    }

}

実行画面は以下のようにランダムに図形や画像(ソースのみを写す場合は,画像ファイルを用意することが必要)が表示されます.

f:id:hidenaoA:20130325175559p:plain

*1:コンストラクタでの読み込みや大域変数的なメンバ変数の使用は,メモリ領域の管理(ガベッジコレクション)の割り込みが入ることを避けるため.