Beginning Android 2D Video Games

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

平面への衝突とその後の運動

ものとものが衝突する運動を考える場合,ぶつかる場所の状況によって,その時点より後の2つの物体の運動は大きな影響を受けます.

一方の物体が他方に比べ非常に大きく,動かず,平らな面を持っている場合は,下図のように,衝突してきた物体のみが運動してきた方向と鉛直方向には逆向きに運動をすることになります.

f:id:hidenaoA:20130330233648p:plain

このとき,運動に影響を与える力について,大きく以下の2つの係数をパラメータとして設定し,衝突後の運動を表現します.

  • 反発係数 e(0\le{e}\le{1}):物体や平面の変形による熱や音としてエネルギーが消費される度合い
  • 摩擦係数 \mu(0\le{\mu}\le{1}):平面と物体の間に摩擦力が生じ,エネルギーが消費される度合い

平面が平らで滑らかな場合は, \mu=1であり,平面と水平な成分への速さに変化は生じません.
同様に,反発係数eが1であれば,「完全弾性衝突」と呼ばれる運動となり,物体は衝突後に全く反対向きで速さを保って跳ね返ることになります.

平面への衝突の実装例

以下の実装例では,放物運動を示したコードを再利用し,画面境界を壁面として,
衝突の判定と衝突後の速度の計算を行っている箇所(動作に関するロジック)は,run()メソッドの以下の部分です.
今回は,外側の境界から半径の分を考慮して,衝突したかどうかを判定しています.

					for(int i=0; i<n; i++){
						if(isVisible[i]){
							long now = System.currentTimeMillis();
							//y[i] = y[i] + velocity[i]/((int)(now-start)*10+1);
							velocity_y[i] = velocity_y[i] + G*(float)(now - start)/100.0f/2.0f;
							y[i] = y[i] + (int)(velocity_y[i] * (float)(now - start)/100.0f); //Y軸方向には加速度に従って加速する運動(100.0fはフレームレート)
							x[i] = x[i] + (int)(velocity_x[i] * (float)(now - start)/100.0f); //X軸方向には等速運動

							//衝突検知&反発(以下の例では球同士は衝突しないが,球同士の衝突も検知し反発させるとより自然に見える)
							if(x[i] < 0+radial[i] || x[i] > screen_width-radial[i]){
								velocity_x[i] = -e[i]*velocity_x[i];
							}
							if(y[i] < 30+radial[i] || y[i] > screen_height-100-radial[i]){
								velocity_y[i] = -e[i]*velocity_y[i];
							}

							//ほぼ静止してしまったら,表示対象から外す(消す) ※消し方は検討の余地あり
							if(Math.abs(velocity_x[i]) <= 0.1f && Math.abs(velocity_y[i]) <= 0.1f){ isVisible[i] = false; }
						}
					}

全体のコードは以下の通りです.
初速度を与える際に,円の大きさを設定しているため,これに合わせて反発係数を小さいものほど大きく(1に近く)してありますが,重さを設定している訳ではありません.

package jp.ac.bunkyo.a2dgames;

/**
 * 任意の反発係数をもつオブジェクトが画面境界を壁として反発する運動の説明
 * @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.util.Random;

import jp.ac.bunkyo.a2dgames.TouchOnFire.ToFView;
import android.os.Bundle;
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.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;

public class CollisionMotion extends Activity {

	ColMView m_view;

	@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_collision_motion);

		m_view = new ColMView(this);
		setContentView(m_view);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.collision_motion, 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;
    }

    @Override
    protected void onPause(){
    	super.onPause();
    	m_view.pause();

    	if(isFinishing()){
    		m_view.finish();
    	}
    }

    @Override
    protected void onResume(){ //Activityの開始・再開処理
    	super.onResume();
    	m_view.resume(); //スレッドの開始処理はActivityの開始・再開に合わせて実行する
    }

    @Override
    protected void onStop(){
    	super.onStop();
    	m_view.finish();
    	Thread.interrupted();
    }


    //高速なアニメーションなどに対応したSurfaceViewを継承したカスタムViewを作成
    class ColMView extends SurfaceView implements Runnable, SurfaceHolder.Callback{

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

    	int bgColor=Color.WHITE;
    	Canvas canvas;
    	SurfaceHolder s_holder;

    	Thread renderThread;

    	Context pContext;

    	volatile boolean isRunning=false; //スレッドの実行制御を行うための変数

    	// 描画対象を管理するメンバ変数
    	int n=10; //任意数の描画対象を管理するには,配列ではなくListなどを利用する(ただし,アクセサ(set/get)は利用しないほうが良いとされる)
       	boolean[] isVisible = new boolean[n]; //描画範囲にあるかどうかかを判定する
    	int[] x = new int[n];       //描画位置のX座標
    	int[] y = new int[n];       //描画位置のY座標
    	int[] radial = new int[n];  //描画する円の半径
    	float[] velocity_x = new float[n];//描画する円の移動速度(任意数のピクセル/frameを速度とする)
    	float[] velocity_y = new float[n];//描画する円の移動速度(任意数のピクセル/frameを速度とする)
    	float[] e = new float[n]; //反発係数e (0<e<=1)
    	float start_x, end_x; //タッチの開始と終了から初速度を計算
    	float start_y, end_y;
    	long touch_down, touch_up;

    	private final float G = 0.98f; //加速度を設定(g=0.098px/frame^2で重力加速度を想定)

		public ColMView(Context context) {
			super(context);

			pContext=context;

    		paint = new Paint();
    		rand = new Random();

    		for(int i=0; i<n; i++){
    			isVisible[i] = false;
    		}

       		s_holder = getHolder();

		}

		public void run(){ //ゲームロジック,描画位置を別スレッドで計算

			long start = System.currentTimeMillis();
			long duration = 0;

			while(isRunning){

				if(! s_holder.getSurface().isValid()){ continue; }

				canvas = s_holder.lockCanvas();

				if(canvas != null){
					long loop_start = System.currentTimeMillis();
					//座標の更新(ゲームなら,ここでゲームロジック(のメソッド群)を実装)
					for(int i=0; i<n; i++){
						if(isVisible[i]){
							long now = System.currentTimeMillis();
							//y[i] = y[i] + velocity[i]/((int)(now-start)*10+1);
							velocity_y[i] = velocity_y[i] + G*(float)(now - start)/100.0f/2.0f;
							y[i] = y[i] + (int)(velocity_y[i] * (float)(now - start)/100.0f); //Y軸方向には加速度に従って加速する運動(100.0fはフレームレート)
							x[i] = x[i] + (int)(velocity_x[i] * (float)(now - start)/100.0f); //X軸方向には等速運動

							//衝突検知&反発(以下の例では球同士は衝突しないが,球同士の衝突も検知し反発させるとより自然に見える)
							if(x[i] < 0+radial[i] || x[i] > screen_width-radial[i]){
								velocity_x[i] = -e[i]*velocity_x[i];
							}
							if(y[i] < 30+radial[i] || y[i] > screen_height-100-radial[i]){
								velocity_y[i] = -e[i]*velocity_y[i];
							}

							//ほぼ静止してしまったら,表示対象から外す(消す) ※消し方は検討の余地あり
							if(Math.abs(velocity_x[i]) <= 0.1f && Math.abs(velocity_y[i]) <= 0.1f){ isVisible[i] = false; }
						}
					}

					drawContents(); //描画を実行

					s_holder.unlockCanvasAndPost(canvas);

					duration = loop_start - System.currentTimeMillis();
					start = System.currentTimeMillis();

				}

	    		try{
	    			Thread.sleep(100 - duration);//次の描画まで100msecの間隔を空ける
	    			                  //(フレームレートを厳守するのであれば,sleepさせるのではなく,描画処理を一定間隔で呼び出すようにする)
	    		}catch(InterruptedException e){

	    		}

			}

		}

    	public void drawContents(){//描画を担当するメソッド

    		canvas.drawColor(bgColor); //キャンバスをbgColorで指定した色で塗りつぶす
    		paint.setColor(Color.BLACK);
    		paint.setTextSize(15);
    		canvas.drawText("画面を斜め上(左右どちらでも)へなぞってください", 10, 20, paint);

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

    		//円の描画
    		paint.setStyle(Style.FILL); //塗りつぶし
    		paint.setColor(Color.MAGENTA); //色を指定
    		for(int i=0; i<n; i++){
    			if(isVisible[i]){
    				canvas.drawCircle(x[i],y[i],radial[i], //中心の座標(x, y),半径
    	    				paint);
    			}
    		}

    	}

		public void pause(){ //中断処理
	        isRunning = false;

	        //必要であれば,ゲームデータなどの一時保存を行う

	        while(true){
	        	try {
	        		renderThread.join();
	        		break;
	        	} catch (InterruptedException e) {
	        		Thread.currentThread().interrupt();
	        	}
	        }

		}

		public void finish(){ //終了処理メソッド
			synchronized (renderThread) {
	        	isRunning = false;
	        }

	        try {
	            renderThread.join();
	        } catch (InterruptedException e) {
	            Thread.currentThread().interrupt();
	        }

	        ((Activity)pContext).finish(); //Context経由でActivityを終了

		}

		public void resume(){
			isRunning = true;

			renderThread = new Thread(this);
			renderThread.start();
		}

		public boolean onTouchEvent(MotionEvent event) { //SurfaceViewにおいて画面タッチイベントを処理
			switch(event.getAction()){
			case MotionEvent.ACTION_DOWN: //指が画面に触れたとき
				start_x = event.getX(); //タッチ開始時の座標を取得
				start_y = event.getY();
				touch_down = System.currentTimeMillis();
				break;
			case MotionEvent.ACTION_UP: //指が画面から離れたとき
			case MotionEvent.ACTION_CANCEL:
				end_x = event.getX(); //タッチ終了時(または,追跡不能になった時点)の座標を取得
				end_y = event.getY();
				touch_up = System.currentTimeMillis();
				for(int i=0; i<n; i++){
					if(isVisible[i] == false){
						isVisible[i] = true;
						x[i] = (int)start_x;
						y[i] = screen_height - 100;
						radial[i] = (int)(touch_up - touch_down)/20; //タッチの長さに合わせて大きさが変わる(間隔が短いほど小さい)

						velocity_x[i] = (end_x - start_x)/((touch_up - touch_down)/10); //タッチの長さに合わせて速さが変わる(間隔が短いほど速い)

						velocity_y[i] = (end_y - start_y)/((touch_up - touch_down)/10); //タッチの長さに合わせて速さが変わる(間隔が短いほど速い)
						if(velocity_y[i] > -5.0f){
							velocity_y[i] = -5.0f;
						}
						else{

						}
						e[i] = 1.0f - rand.nextFloat()*radial[i]/100; //反発係数をランダムに設定(小さいものほどよく跳ねるように計算)

						break;
					}
				}
				break;

			}
			return true;
		}

		@Override
		public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
				int arg3) {
			// TODO 自動生成されたメソッド・スタブ

		}

		@Override
		public void surfaceCreated(SurfaceHolder arg0) {

		}

		@Override
		public void surfaceDestroyed(SurfaceHolder arg0) {
			// TODO 自動生成されたメソッド・スタブ
			finish();

		}

    }

}

画面での動作は,短い時間で大きくなぞった場合ほど大きな初速度と小さな大きさ,高い反発係数が与えられるようになります.
これにより,小さなものほど速く,よく跳ねるように表現されます.
ただし,物体同士の衝突を表す,円同士の衝突については計算していません.
また,より物理に忠実に再現するには,重さを与え,加速度に対して忠実に衝突後のエネルギーを計算し,各成分の速さを更新する必要があります.

f:id:hidenaoA:20130330233649p:plain