JavaとJavaEEプログラマのブログ

JavaEEを中心にしたをソフトウェア開発についてのブログ

ハエ取りゲーム完成

全ソース

EsGameActivityクラス。
RedrawHandlerを使って、一定時間ごとに再描画するように修正。
iPhone版ではゲームオーバー時にリスタートボタンを表示するようになっていましたが、EsGameRenderの内部変化でViewのレイアウトを変化させる方法が見つからなかったのでMENUボタンにリスタートボタンを追加。
Viewのイベントでレイアウトを動的に変化させたりダイアログを表示させるのは簡単なのですが…。

package sample.game;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.TableLayout;


public class EsGameActivity extends Activity {

	//OpenGLESビュー
	private  GLSurfaceView gLSurfaceView;

	private EsGameRender esGameRender;

	private TableLayout layout;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        //GLサーフェイスビューを作成して設定
        gLSurfaceView = new GLSurfaceView(this);

        esGameRender = new EsGameRender(this);

        gLSurfaceView.setRenderer(esGameRender);

        gLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);//任意のタイミングで再描画する設定

        layout = new TableLayout(this);
        layout.addView(gLSurfaceView);

        setContentView(layout);

        RedrawHandler handler = new RedrawHandler(100);
        handler.start();


    }

    private int touchState;
    private float touchX, touchY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {

    	if(esGameRender == null){
    		return true;
    	}

    	touchX = event.getX();
    	touchY = event.getY();

    	touchState = event.getAction();

    	esGameRender.setTouchParam(touchX, touchY, touchState);

    	return true;
    }


    @Override
    protected void onResume()
    {
        super.onResume();
        gLSurfaceView.onResume();
    }

    @Override
    protected void onPause()
    {
        super.onPause();
        gLSurfaceView.onPause();
    }

    private final static String RESTART_BUTTON = "RESTART";

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    	//メニューボタンの設定
    	menu.add(RESTART_BUTTON);
    	return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    	//RESTARTボタンが選択された。
    	if(RESTART_BUTTON.equals( item.getTitle()) ){
    		esGameRender.startNewGame();
    		return true;
    	}
    	return false;
    }



    /** 画面を再描画するハンドラー */
    class RedrawHandler extends Handler {
        private int delayTime;
        private int frameRate;
        public RedrawHandler(int frameRate) {
            this.frameRate = frameRate;
        }
        public void start() {
            this.delayTime = 1000 / frameRate;
            this.sendMessageDelayed(obtainMessage(0), delayTime);
        }
        public void stop() {
            delayTime = 0;
        }
        @Override
        public void handleMessage(Message msg) {

        	gLSurfaceView.requestRender();//再描画

            if (delayTime == 0) return; // stop
            sendMessageDelayed(obtainMessage(0), delayTime);
        }
    }

}

EsGameRenderクラス。
ゲームの処理はこのクラスで行っています。
MediaPlayerクラスを使ってSEを鳴らしています。サンプルのBGMはcaf形式だったのでネットからひろった無料のwavファイルを使用しています。

package sample.game;


import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import sample.game.utils.TextureLoader;

import android.content.Context;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.opengl.GLSurfaceView;

import static sample.game.utils.GraphicUtils.*;
import static java.lang.Math.*;

public class EsGameRender implements GLSurfaceView.Renderer , OnCompletionListener{

	private static final int one = 0x10000;

	private static final int TARGET_NUM = 20;

	private static final long GAME_INTERVAL = 60 * 1000;

	private static Target [] targets  = new Target[TARGET_NUM];

	private Context context;

	private double randf(){
		return (random()  % 1001) * 0.001f;
	}

	public EsGameRender(Context context){
		this.context = context;
		startNewGame();

	}

	public void startNewGame(){

		score = 0;
		startTime = System.currentTimeMillis();
		isGameOver = false;

		for (int i = 0; i < TARGET_NUM; i++) {

			float x = (float) (randf()  * 2.0f - 1.0f);
			float y = (float) (randf()  * 2.0f - 1.0f);
			float angle = (float)(random() * 10.0  % 360 );
			float size = (float) (randf()  * 0.25f + 0.25f);
			float speed = (float)(randf()  * 0.01f + 0.01f ) * 10;
			float turnAngle = (float) (randf() * 4.0f - 2.0f);

			targets[i] = new Target(x,y,angle,size,speed, turnAngle);


		}

		// サウンドデータの初期化
		backgroundPlayer = MediaPlayer.create(context, R.raw.bazar);
		mediaPlayer = MediaPlayer.create(context, R.raw.explosion);
		mediaPlayer.setOnCompletionListener(this);
		backgroundPlayer.setOnCompletionListener(this);

		backgroundPlayer.setLooping(true);
		backgroundPlayer.start();

	}

	@Override
	public void onDrawFrame(GL10 gl10) {

		makeWorld(gl10);
		drawTextureRectangle(gl10, context, 0.0f, 0.0f, 2.0f, 3.0f, background, 0, 0, 0, one);//背景

		long passedTime = System.currentTimeMillis() - startTime;
		long remainTime = GAME_INTERVAL - passedTime;
		if(remainTime < 0){
			remainTime = 0;
			isGameOver = true;

		}

		for(Target t : targets){

			if(!isGameOver  &&  t.isPointInside(glX,glY) ){

				t.moveNextPosition();
				score = score + 100;

				glX = -3.0f;//初期化
				glY = -3.0f;//初期化

				mediaPlayer.start();

			}else{
				t.move();
			}


			t.draw(gl10,context,targetTexture);
		}

		//スコアを表示
		drawNumbers(gl10, context, -0.5f, 1.25f, 0.125f, 0.125f, numberTexture, score, 8, one, one, one, one);

		//残り時間を表示
		drawNumbers(gl10, context, 0.5f, 1.2f, 0.4f, 0.4f, numberTexture,(int)(remainTime / 1000) , 2, one, one, one, one);

		if(isGameOver){
			drawTextureRectangle(gl10, context, 0.0f, 0.0f, 2.0f, 0.5f, gameOverTexture, one, one, one, one);

			backgroundPlayer.stop();
		}

	}


	@Override
	public void onSurfaceChanged(GL10 gl10, int width, int height) {
		gl10.glViewport(0, 0, width, height);
	}

	@Override
	public void onSurfaceCreated(GL10 gl10, EGLConfig eglconfig) {

		  /*
         * ギザギザを目立たなくするGL_DITHERを無効にします。
         */
        gl10.glDisable(GL10.GL_DITHER);

        /*
         * カラーとテクスチャ座標の補間精度を最も効率的にします。
         */
         gl10.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
                 GL10.GL_FASTEST);

		background = TextureLoader.loadTexture(gl10, context, R.drawable.circuit);//リソース読み込み
		targetTexture = TextureLoader.loadTexture(gl10, context, R.drawable.fly);
		numberTexture = TextureLoader.loadTexture(gl10, context, R.drawable.number_texture);
		gameOverTexture = TextureLoader.loadTexture(gl10, context, R.drawable.game_over);

	}

	private static int background ;
	private static int targetTexture;
	private static int numberTexture;
	private static int gameOverTexture;

	public void setTouchParam(float touchX, float touchY, int touchState) {

		//OpenGL上の座標に変換する。
		glX = (touchX / 320.0f ) * 2.0f - 1.0f;
		glY = (touchY / 480.0f ) * -3.0f + 1.5f;
		this.touchState = touchState;

	}
	private static int touchState;
	private static float glX, glY;

	private static int score;
	private static long startTime ;
	public static boolean isGameOver;


	private static MediaPlayer mediaPlayer;
	private static MediaPlayer backgroundPlayer;

	@Override
	public void onCompletion(MediaPlayer mediaPlayer) {


	}

}


Targetクラス。
標的となるハエの動きはこのクラスで。

package sample.game;

import static sample.game.utils.GraphicUtils.*;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;

public class Target {

	public float angle;
	public float x,y;
	public float size;
	public float speed;
	public float turnAngle;

	public Target(float x, float y, float angle, float size, float speed, float turnAngle){

		this.x = x;
		this.y = y;
		this.angle = angle;
		this.size = size;
		this.speed = speed;
		this.turnAngle = turnAngle;

	}


	public boolean isPointInside(float gx, float gy){

		//標的とタッチ位置の距離を計算
		float dx = gx - this.x;
		float dy = gy - this.y;
		float distance = (float) Math.sqrt(dx * dx + dy * dy);

		//距離が標的の当たり範囲(円で近似する)より小さければ当たり。
		if(distance <= size * 0.5f){
			return true;
		}else{
			return false;
		}

	}


	public void move() {

//		Log.d(TAG, "state [x:y]="+ touchState + " [" + glX + ":" + glY +"]");

		//100回に1度の割合で方向を転換する。
		if((int)(Math.random() * (100+1)) % 100 == 0 ){

			//旋回角度を-2.0〜2.0の範囲で変化させる。
			turnAngle = (float) (Math.random()  * 4.0f - 2.0f );
		}


		angle = angle + turnAngle;
		double theta = angle / 180.0 * Math.PI;
		x = (float) (x + Math.cos(theta) * speed);
		y = (float) (y + Math.sin(theta) * speed);

		//画面外へ出たら反対の端へ移動させる
		if(x >= 2.0f){
			x -= 4.0f;
		}else if(x <= -2.0f){
			x += 4.0f;
		}

		if(y >= 2.5f){
			y -= 5.0f;
		}else if(y <= -2.5f){
			y += 5.0f;
		}

	}

	public void moveNextPosition(){

		//標的をランダムな位置に移動
		final float dist = 2.0f;//直径2.0fの円周上。
		float theta = (float) ((Math.random() * 10 % 360) / 180.0 * Math.PI);
		x = (float) (Math.cos(theta) * dist);
		y = (float) (Math.sin(theta) * dist);

	}

	public void draw(GL10 gl10, Context context, int targetTexture) {

		gl10.glPushMatrix();
		gl10.glTranslatef(x, y, 0.0f);
		gl10.glRotatef(angle, 0.0f, 0.0f, 1.0f);
		gl10.glScalef(size, size, 1.0f);
		drawTextureRectangle(gl10, context, 0.0f, 0.0f, 1.0f, 1.0f, targetTexture, 0, 0, 0, 0x1000);//標的
		gl10.glPopMatrix();

	}

}

GraphicUtilsクラス。
テクスチャーを描くクラスです。少しでもGCを減らそうと、固定パラメータをstaticにしています。

package sample.game.utils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;

import static java.lang.Math.*;

public final class GraphicUtils {

	public static  final int ONE_POINT_DATA_NUM = 3;//頂点を構成するデータの数(x,y,zの3つ)

	private static float getRadian(int divides, int n){
		return 2.0f / (float)divides * (float)n * (float)PI;
	}

	/**
	 * 指定した数字を、指定した桁数で表示する。
	 * @param x 表示位置のX座標
	 * @param y 表示位置のy座標
	 * @param width  数字の表示サイズ(幅)
	 * @param height 数字の表示サイズ(高さ)
	 * @param number 表示する数字
	 * @param figures 桁数
	 */
	public static void drawNumbers(GL10 gl, Context context, final float x, final float y,
			final float width, final float height,
			int textureId,
			final int number,final int figures,
			final int red, final int green, final int blue, final int alpha
			){

		float totalWidth = width * figures; //n文字分の横幅
		float rightX = x + totalWidth * 0.5f;
		float figlX = rightX - width * 0.5f;

		for(int i=0; i < figures; i++){
			float figNX = figlX - i * width;//数字の中心座標
			int numberToDraw = number / (int)Math.pow(10.0, (double)i) % 10; // i桁目の数字を取り出す。
			drawNumber(gl, context, figNX, y, width, height, textureId, numberToDraw, red, green, blue, alpha);
		}

	}


	/**
	 * 指定した数字のテクスチャを描画
	 */
	public static void drawNumber(GL10 gl, Context context, final float x, final float y,
			final float width, final float height,int textureId,
			final int number,
			final int red, final int green, final int blue, final int alpha
			){

		float u = (float)( number % 4 ) * 0.25f;
		float v = (float)( number / 4 ) * 0.25f;


		drawTextureRectangle(gl,context,x,y,width,height,textureId, u, v, 0.25f, 0.25f, red,green,blue,alpha);


	}

	/**
	 * テクスチャマッピングでテクスチャの一部分だけを四角形で描画
	 */
	public static void drawTextureRectangle(GL10 gl, Context context, final float x, final float y,
			final float width, final float height,int textureId,
			final float u, final float v, final float textureWidth, final float textureHeight,
			final int red, final int green, final int blue, final int alpha){

//		final float [] squares = new float[]{
//				-0.5f * width  + x, -0.5f * height + y, 0.0f,
//				 0.5f * width  + x, -0.5f * height + y, 0.0f,
//				-0.5f * width  + x,  0.5f * height + y, 0.0f,
//				 0.5f * width  + x,  0.5f * height + y, 0.0f,
//			};

		squares[0] = -0.5f * width  + x;
		squares[1] = -0.5f * height + y;
		squares[3] =  0.5f * width  + x;
		squares[4] = -0.5f * height + y;
		squares[6] = -0.5f * width  + x;
		squares[7] =  0.5f * height + y;
		squares[9] =  0.5f * width  + x;
		squares[10] = 0.5f * height + y;


		//マッピング座標
		final float [] textureCoords = new float[]{
				u, v + textureHeight,
				u + textureWidth, v + textureHeight,
				u, v,
				u + textureWidth, v,
		};

		drawTextureRectangle(gl,context,x,y,width,height,textureId,squares, textureCoords,red,green,blue,alpha);

	}

	final static float [] texCoords = new float[]{
			0.0f, 1.0f,
			1.0f, 1.0f,
			0.0f, 0.0f,
			1.0f, 0.0f,
	};

	final static float [] squares = new float[]{
			-0.5f , -0.5f , 0.0f,
			 0.5f , -0.5f , 0.0f,
			-0.5f ,  0.5f , 0.0f,
			 0.5f ,  0.5f , 0.0f,
	};




	/**
	 * テクスチャを四角形で描画
	 */
	public static void drawTextureRectangle(GL10 gl, Context context, final float x, final float y,
			final float width, final float height,int textureId,
			final int red, final int green, final int blue, final int alpha){

//		final float [] squares = new float[]{
//				-0.5f * width  + x, -0.5f * height + y, 0.0f,
//				 0.5f * width  + x, -0.5f * height + y, 0.0f,
//				-0.5f * width  + x,  0.5f * height + y, 0.0f,
//				 0.5f * width  + x,  0.5f * height + y, 0.0f,
//			};

		squares[0] = -0.5f * width  + x;
		squares[1] = -0.5f * height + y;
		squares[3] =  0.5f * width  + x;
		squares[4] = -0.5f * height + y;
		squares[6] = -0.5f * width  + x;
		squares[7] =  0.5f * height + y;
		squares[9] =  0.5f * width  + x;
		squares[10] = 0.5f * height + y;

		drawTextureRectangle(gl,context,x,y,width,height,textureId,squares, texCoords,red,green,blue,alpha);

	}

	/**
	 * テクスチャを描画
	 * @param gl
	 * @param context
	 * @param x
	 * @param y
	 * @param width
	 * @param height
	 * @param textureId
	 * @param squares
	 * @param textureCoords
	 * @param red
	 * @param green
	 * @param blue
	 * @param alpha
	 */
	public static void drawTextureRectangle(GL10 gl, Context context, final float x, final float y,
			final float width, final float height,int textureId,
			final float [] squares,
			final float[] textureCoords,
			final int red, final int green, final int blue, final int alpha){

		gl.glEnable(GL10.GL_TEXTURE_2D);//テクスチャ機能を有効化
		gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);//テクスチャオブジェクトをバインド


		//ポリゴン描画
		gl.glVertexPointer(ONE_POINT_DATA_NUM, GL10.GL_FLOAT, 0, getFloatBuffer(squares));//描画する頂点配列を設定
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);//頂点配列を有効にする。

		gl.glColor4x(red, green, blue,  alpha);

		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0,  getFloatBuffer(textureCoords));
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);


		gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, POLYGON_NUM);


		gl.glDisable(GL10.GL_TEXTURE_2D);//テクスチャ機能を無効化

	}
	static final int POLYGON_NUM = 4;

	/**
	 * 円を描画
	 */
	public static void drawCircle(GL10 gl,final float x, final float y,
			final int divides, final  float radius,
			final int red, final int green, final int blue, final int alpha){
		float [] vertices = new float[divides * 3 * 2];//頂点の数はn角形の場合はn*3*2になる。

		int vertexId = 0;

		for(int i=0; i < divides; i++){
			float theta1  = getRadian(divides, i);
			float theta2  = getRadian(divides, i+1);

			vertices[vertexId++] = x;
			vertices[vertexId++] = y;

			vertices[vertexId++] = (float) (cos(theta1) * radius + x);
			vertices[vertexId++] = (float) (sin(theta1) * radius + y);

			vertices[vertexId++] = (float) (cos(theta2) * radius + x);
			vertices[vertexId++] = (float) (sin(theta2) * radius + y);
		}

		//色指定
		gl.glColor4x(red, green, blue, alpha);
		gl.glDisableClientState(GL10.GL_COLOR_ARRAY);

		//ポリゴン描画
		gl.glVertexPointer(2, GL10.GL_FLOAT, 0, getFloatBuffer(vertices) );
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

		//3つの頂点を持つポリゴンn個で構成されている。
		final int polygonNum =  divides * 3;
		gl.glDrawArrays(GL10.GL_TRIANGLES, 0, polygonNum);

//		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

	}

	/**
	 * 長方形を描画
	 */
	public static void drawRectangle(GL10 gl,float x, float y, float width, float height, int red, int green, int blue, int alpha ){
		final float [] squares = new float[]{
				-0.5f * width  + x, -0.5f * height + y, 0.0f,
				 0.5f * width  + x, -0.5f * height + y, 0.0f,
				-0.5f * width  + x,  0.5f * height + y, 0.0f,
				 0.5f * width  + x,  0.5f * height + y, 0.0f,
			};

		drawSquare(gl,squares,red, green, blue,alpha);

	}



	/**
	 * 正方形を描画
	 */
	public static void drawSquare(GL10 gl,float x, float y, int red, int green, int blue, int alpha ){
		final float [] squares = new float[]{
			-0.5f + x, -0.5f + y, 0.0f,
			 0.5f + x, -0.5f + y, 0.0f,
			-0.5f + x,  0.5f + y, 0.0f,
			0.5f + x,   0.5f + y, 0.0f,

		};

		drawSquare(gl,squares,red, green, blue,alpha);
	}

	/**
	 * 正方形を描画
	 */
	public static void drawSquare(GL10 gl,float[] square, int red, int green, int blue, int alpha ){

		FloatBuffer squareBuffer = getFloatBuffer(square);

		gl.glVertexPointer(ONE_POINT_DATA_NUM, GL10.GL_FLOAT, 0, squareBuffer);//描画する頂点配列を設定
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);//頂点配列を有効にする。

		gl.glColor4x(red, green, blue,  alpha);
		final int POLYGON_NUM = 4;
		gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, POLYGON_NUM);

	}

	/**
	 * 初期化処理
	 * @param gl
	 */
	public static void makeWorld(GL10 gl){
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); //画面クリア

		gl.glMatrixMode(GL10.GL_PROJECTION);
		gl.glLoadIdentity();

		gl.glOrthof(-1.0f, 1.0f, -1.5f, 1.5f, 0.5f, -0.5f);//0.0を中心とする座標系

		gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f ); //画面クリアカラー設定(灰色)
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT); //画面クリア
	}

	public static FloatBuffer getFloatBuffer(float [] array){
		FloatBuffer floatBuffer;
		ByteBuffer bb = ByteBuffer.allocateDirect(array.length * 4);
		bb.order(ByteOrder.nativeOrder());
		floatBuffer = bb.asFloatBuffer();
		floatBuffer.put(array);
		floatBuffer.position(0);
		return floatBuffer;
	}



}

TextureLoaderクラス。
テクスチャを読み込みます。

package sample.game.utils;

import java.io.IOException;
import java.io.InputStream;

import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class TextureLoader {

	/**
	 * resouceIdで指定された画像をテクスチャとして読み込む。
	 * @param gl
	 * @param context
	 * @param resouceId
	 * @return
	 */
	public static int loadTexture(GL10 gl, Context context, int resouceId ){

		int[] textures = new int[1];

		gl.glGenTextures(1, textures, 0);//テクスチャオブジェクトを作成
		int textureId = textures[0];//作成したテクスチャのIDを設定

		gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);//テクスチャIDを2Dテクスチャとしてバインド。

		//バインドしたテクスチャのパラメータを設定
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);//テクスチャの縮小方法を設定
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);//テクスチャの拡大方法を設定

		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);//テクスチャの繰り返し方法を設定
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);


		gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE);//テクスチャの色で下地の色を置き換える


        InputStream is = context.getResources().openRawResource(resouceId);

        Bitmap bitmap;
        try {
            bitmap = BitmapFactory.decodeStream(is);
        } finally {
            try {
                is.close();
            } catch(IOException e) {
                // Ignore.
            	e.printStackTrace();
            }
        }

        GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
        bitmap.recycle();

        return textureId;
	}

}


ゲーム画面。
速度と再描画タイミングを調整したので、滑らかに動きます。2秒程の間隔で一瞬、動きが止まるのはフラッシュ攻撃です!…ではなくGCが走っているからです。
どうやらOpenGLのバグのようで、AndroidSDKに添付されているAPIデモのKUBE(Graphic->OpenGL ES->KUBE)でも同じ現象が確認できます。
現状での回避方法はあるのかな?