Beginning Android 2D Video Games

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

目次

本ブログでは,Androidアプリとしてゲームを作成することに関する技術的な事項を紹介・解説しています.
また,本資料で扱っていないゲーム作成やゲーム企画やコンテンツ作成に関する内容を含めて,体系的に学ぶことに興味を持たれた方は,是非,文教大学情報学部情報システム学科のカリキュラムをご覧下さい.
(リンクの無い記事については,後日,追加していきます)

3 2Dゲームのトリック

3.3 物体の衝突

3.4 入力と出力の連動

簡単な2Dゲームの作成

ボールアクションゲームの作成(1)

  • 開発環境の準備とゲームの構想
  • 要素技術の習得と実装
  • ゲームアプリとしての統合とテスト

参考書籍・サイト

免責事項:
資料中の説明,その他記載などにより,損害が生じた場合の補償には,一切応じられません.
各記事中のソースコードについては,明記してある通りApache License 2.0に準拠してご利用ください.

参照:Intentを利用したデータ受け渡し

Activityによる画面遷移を行うIntentの仕組みでは,ファイルやデータベース(SQLite)などを介さず,データを受け渡す仕組みが用意されています.

画面遷移時に渡すデータは,Intentクラスのオブジェクトを作成した後,setData()やsetExtra()を用いて,別画面に送信するデータをセットします.
setData()メソッドは,主に暗黙的インテントで用いられることが想定されているため,Uri型の変数を引数とします.
setExtra()メソッドでは,任意のデータ(文字列や数値,それらの配列)を明示的インテントで別の画面(またはサービス)に送付することが想定されます.
setExtra()のString型の第一引数は,第二引数の値を識別するためのキーにあたります.
なお,setData(),setExtra()で送付できるデータ型に関しては,Intentクラスのドキュメントを参照して下さい.


別の画面(Activity)に遷移する場合は,別画面を呼び出すstartActivity()メソッドを呼ぶ前に上記のsetData()やsetExtra()を実行する処理を記述します.

呼び出された側の画面やサービスでは,getInten()によって得たIntent型のオブジェクトからgetXXXXExtra()メソッドを呼び出すことで,XXXXにあたるデータ型の値をsetExtra()で設定したキーを指定することで得ます.
このキーの値でどのようなデータ型を送受信するのかは,アプリの作成者間で決めなければなりません.

データの送受信のサンプル

Intentを利用したデータの受け渡しと画面遷移は,以下の図に示すような動作を行うサンプルにて示します.

f:id:hidenaoA:20131018002942p:plain

まず,インテントを実行して別の画面を呼び出すActivityですが,上のボタンでテキストボックスに入力した文字列を別のActivityに送信します.
下のボタンとテキストボックス(電話番号入力をレイアウトXMLで指定)では,URIに電話アプリを立ち上げるようオブジェクトを生成してsetData()を実行しています.
URIの種類によって,暗黙的インテントを受けとるアプリケーションを指定することが可能です.

なお,各ボタンのクリックイベントを処理するメソッド(onActivityIntentButtonClick()とonTelIntentButtonClick())は,レイアウトXMLでボタンにonClick属性を加えて指定しています.

package jp.ac.bunkyo.a2dgames;

import android.net.Uri;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.text.SpannableStringBuilder;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;

public class IntentSendTest extends Activity {

	EditText text_input,tel_input;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_intent_send_test);

		text_input = (EditText)findViewById(R.id.intent_extra_string);
		tel_input = (EditText)findViewById(R.id.intent_tel_number);

	}

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.intent_send_test, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {//Menuボタンを押して出たメニューが選ばれたときの動作を記述
        super.onOptionsItemSelected(item);
        switch(item.getItemId()){ //それぞれのitemの表示はres/menu/*.xmlで行う
        case R.id.menu_finishing :
            //強制的にActivityを終了する
        	finish();
            return true;
        }
        return false;
    }

    public void onActivityIntentButtonClick(View v){
		//明示的インテントでextra部分に文字列を格納して送信
		Intent intent = new Intent(getApplicationContext(),IntentRecieveTest.class);

		//Extraとして入力された文字列をsend_stringというキーを付けて保存
		SpannableStringBuilder sb = (SpannableStringBuilder)text_input.getText();
		intent.putExtra("send_string", sb.toString());

		startActivity(intent);
    }

    public void onTelIntentButtonClick(View v){
    	//暗黙的インテントで電話機能を呼び出す
    	Intent intent = new Intent();
    	intent.setAction(Intent.ACTION_VIEW);
    	SpannableStringBuilder sb = (SpannableStringBuilder)tel_input.getText();
    	String tel_uri_str = "tel:"+sb.toString();
    	intent.setData(Uri.parse(tel_uri_str));
    	startActivity(intent);
    }


}

明示的インテントを受信するActivityは,以下のように文字列を受けとる動作をIntentオブジェクトのgetStringExtra()メソッドで行っています.

package jp.ac.bunkyo.a2dgames;

import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.TextView;

public class IntentRecieveTest extends Activity {

	TextView text_view;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_intent_recieve_test);

		text_view = (TextView)findViewById(R.id.intet_recieve_text);
		Intent intent =getIntent();
		String sent_string = intent.getStringExtra("send_string");
		text_view.setText(sent_string);
	}


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.intent_recieve_test, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {//Menuボタンを押して出たメニューが選ばれたときの動作を記述
        super.onOptionsItemSelected(item);
        switch(item.getItemId()){ //それぞれのitemの表示はres/menu/*.xmlで行う
        case R.id.menu_finishing :
            //強制的にActivityを終了する
        	finish();
            return true;
        }
        return false;
    }


}

AndroidHttpdClientを利用したWebアクセス(POST/GET)

Androidアプリに何らかの配信機能やソーシャル機能,データの共有・リモート保存の機能を持たせる場合,最も合理的な1つの解決方法はWeb上に配置したアプリケーションを介すること,となります*1

Web(HTTPやHTTPS)を介したアプリケーションは,CGIによるPerlPHP, Ruby(独自のフレームワークもあり),JSPServletといったものがあります.
これらは,HTTPを解釈するWebサーバ(ApacheTomcatなど)上で動作するプログラムとデータを保管するファイルやデータベース(MySQLPostgreSQLなど)から成ります.

通常,Webアプリの利用は,HTMLを介してブラウザから行うことが圧倒的に多いのですが,HTTPを発行できるクライアントであれば,ブラウザに限ることはありません.
AndroidでもHTTPクライアントとして,AndroidHttpClientというクラスが提供されています.
以下にWebを介したデータのやりとりの模式図を示します.

f:id:hidenaoA:20131013003025p:plain

以下の例では,単純にHTMLで以下のように作成した

をresponse.phpという送信された文字列を表示するWebアプリに送信するものとして説明します.

<form action="http://localhost/work/response.php" method="POST">
  <input type ="text" name="send_str" size="30">
  <input type="submit" value="Send">
</form>
<form action="http://localhost/work/response.php" method="GET">
  <input type="submit" value="Reload">
</form>

Manifest.xmlの設定

AndroidHttpClientを使ってWebアクセスを行うためには,ネットワーク通信が必須のため,以下のpermissionを受ける必要があります.

<uses-permission android:name="android.permission.INTERNET" />

AndroidHttpClientによるPOSTとGETによる要求発行

まず,画面レイアウトですが,EditTextとButtonを適当に配置した下にTextViewが配置してあります.
TextViewについては,WebViewとすることで,WebアプリケーションからのHTMLレスポンスを整形して表示することも可能となります.

それぞれのボタンについては,クリック時のイベント処理として,要求の発行を実行してあります.
ここでDialogを表示していますが,必要であれば,キャンセル処理を実装して下さい.
注意しておきたいことは,AsyncTaskによる実行は別スレッドのため,UIスレッドへのアクセスは,このボタン押下イベント処理内で行わないといけない,ということです.

AsyncTaskの利用

Webアクセスのように処理の時間が予め予想できない処理については,AndroidアプリではAsyncTaskを用いることが勧められています.
AsyncTaskは,doInBackgroundメソッドに実装された内容とこの実行後に呼ばれるonPostExecuteメソッドの内容がexecuteメソッドを呼ぶことで実行されます.
なお,doInBackgroundの返値(String)はonPostExecuteの引数として受け取れます.

詳しいAsyncTaskの説明については,文献や以下の解説を参照してください.
非同期処理の基礎 - Android 開発入門

実装

AndroidHttpClientを利用したWebアプリケーションへのアクセス実行コードを以下に示します.
POSTによって送信するときの変数名と値のペアは,ここではBasicNameValuePairといNameValuePairのサブクラスを利用しています.
このNameValuePairを複数送る場合は,Listにして,UrlEncodedEntityに変換することで複数の変数と値のペアを送付することが可能となります.

GETの場合は,HttpGetのオブジェクトを生成(new)する際に渡すURL文字列によってデータ送信することも可能です.
その場合,URLエンコードによって文字列を変換する必要があります.

なお,これらのデータ送信を受けとるWebアプリケーションは,以下の例では冒頭にあるresponse.phpとしています.

package jp.ac.bunkyo.a2dgames;

/**
 * HTTP POSTを利用してレスポンス(ボディー文字列)を表示するActivity
 * @author Hidenao Abe (hidenao@shonan.bunkyo.ac.jp)
 *
 * POSTを受けとるPHPコード(このサンプルではhttp://localhost/work/response.phpに設置)は以下の通り
 * <?php
 *   //send_strは「送信」ボタンを押してPOST送信されてきた文字列
 *   $posted = mb_convert_encoding($_REQUEST["send_str"],"UTF-8","auto");
 *
 *   //必要であれば,ここで保存処理などを行う
 *
 *   //レスポンス本体となる応答出力部分(ここから)
 *   echo "送信された文字列:$posted\n";
 *   //レスポンス本体となる応答出力部分(ここまで)
 * ?>
 *
 */

/* 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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import android.os.AsyncTask;
import android.os.Bundle;
import android.app.Activity;
import android.app.ProgressDialog;
import android.text.SpannableStringBuilder;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import android.net.http.AndroidHttpClient;

public class HTTPClientTest extends Activity {

	TextView text_view;
	EditText text_input;

	ProgressDialog dialog;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_httpclient_test);
		text_view = (TextView)findViewById(R.id.textView1);
		text_input = (EditText)findViewById(R.id.editText1);

		Button btnUpload = (Button) findViewById(R.id.button1);
		btnUpload.setOnClickListener(new View.OnClickListener() {
			public void onClick(View view) {
				//ダイアログを表示
				dialog = new ProgressDialog(HTTPClientTest.this);
				dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
				dialog.setMessage("送信中");
				dialog.show();
				//アップロード(POST)実行
				httpPost();
				dialog.dismiss();
			}
		});

		Button btnReload = (Button) findViewById(R.id.button2);
		btnReload.setOnClickListener(new View.OnClickListener() {
			public void onClick(View view) {
				//ダイアログを表示
				dialog = new ProgressDialog(HTTPClientTest.this);
				dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
				dialog.setMessage("更新中");
				dialog.show();
				//GET実行
				httpGet();
				dialog.dismiss();
			}
		});

	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.httpclient_test, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {//Menuボタンを押して出たメニューが選ばれたときの動作を記述
		super.onOptionsItemSelected(item);
		switch(item.getItemId()){ //それぞれのitemの表示はres/menu/*.xmlで行う
		case R.id.menu_finishing :
			//強制的にActivityを終了する
			finish();
			return true;
		}
		return false;
	}

	private void httpPost(){ //HTTP POSTを使って送信を行う(Bottonのアクションリスナから呼ばれる)
		new AsyncTask<Void, Void, String>() {

			@Override
			protected String doInBackground(Void... params) {
				// HTTPリクエストの構築
				HttpPost request = new HttpPost("http://localhost/work/response.php");
				List<NameValuePair> sendParams = new ArrayList<NameValuePair>();
				SpannableStringBuilder sb = (SpannableStringBuilder)text_input.getText();
				sendParams.add(new BasicNameValuePair("send_str", sb.toString()));
				try {
					request.setEntity(new UrlEncodedFormEntity(sendParams, "UTF-8"));
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
				}

				// HTTPリクエスト発行
				AndroidHttpClient httpClient = AndroidHttpClient.newInstance("Android HTTP Client Test");
				HttpResponse response = null;
				String response_str = "NG";
				try {
					response = httpClient.execute(request);
					// HttpResponseのEntityデータをStringへ変換
					BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
					StringBuilder builder = new StringBuilder();
					String line = null;
					while ((line = reader.readLine()) != null) {
						builder.append(line + "\n");
					}
					response_str = builder.toString();
				} catch (IOException e) {
					e.printStackTrace();
					response_str = e.toString();
				}

				if(httpClient != null){
					httpClient.close();
				}

				return response_str; //返値はonPostExecuteに渡される
 			}


			@Override
			protected void onPostExecute(String result) { //引数はdoInBackgroundの結果
				// 画面に文字列を表示
				text_view.setText(result);
			}
    	 }.execute();

    }

    private void httpGet(){ //HTTP GETを使ってリクエストする
   	 new AsyncTask<Void, Void, String>() {

			@Override
			protected String doInBackground(Void... params) {
				// HTTPリクエストの構築
				HttpGet request = new HttpGet("http://localhost/work/response.php");

				// HTTPリクエスト発行
				AndroidHttpClient httpClient = AndroidHttpClient.newInstance("Android HTTP Client Test");
				HttpResponse response = null;
				String response_str = "GET NG";
				try {
					response = httpClient.execute(request);
					// HttpResponseのEntityデータをStringへ変換
					BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
					StringBuilder builder = new StringBuilder();
					String line = null;
					while ((line = reader.readLine()) != null) {
						builder.append(line + "\n");
					}
					response_str = builder.toString();
				} catch (IOException e) {
					e.printStackTrace();
					response_str = e.toString();
				}

				if(httpClient != null){
					httpClient.close();
				}

				return response_str; //返値はonPostExecuteに渡される
			}


			@Override
			protected void onPostExecute(String result) { //引数はdoInBackgroundの結果
				// 画面に文字列を表示
				text_view.setText(result);
			}
 	 }.execute();

    }

}

以上の例ではテキストを送信しましたが,通常のタグと同様にPOSTを用いてファイルを送信することも可能です.
以下のサイトにファイルを送信する方法が掲載されています(サーバ側はファイル保存が可能であれば,Servletである必要はありません.).

Android HttpClientでサーバーにファイルをアップロードする

*1:独自サーバプログラムをネットワーク上のサーバに立ち上げてもいいのですが,セキュリティ対策やメンテナンス,汎用性の面で不利になことが多いでしょう.

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

ものとものが衝突する運動を考える場合,ぶつかる場所の状況によって,その時点より後の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

放物(弾道)運動の表現

放物(ものを放り投げたような)運動は,初期状態の速度の方向と加速度の方向が異なり運動の開始後に物体に外から力が加わらないこと,を想定しています*1

2Dのゲームでは,1つの平面上における放物線を描くような運動を下図のように想定します.
この運動は,初速度の鉛直成分に並行かつ逆向きの加速度を想定し,加速度と垂直な方向を水平方向として,説明されます.
図にあるように,鉛直方向上向きを負の数で表すと,上向きの速さは徐々に低下していき,やがて0となったあと,下向きの速さが加わっていきます.
水平方向の運動は,外から力が加わらない(空気の抵抗も無いと仮定する)ことから,等速運動となります.

f:id:hidenaoA:20130330233647p:plain

放物運動の実装例

以下の実装例では,画面タッチによる初速度をACTION_DOWNからACTION_UPの間で計算しています.
この初速度の大きさは,X方向(水平)とY方向(鉛直)に分解され,それぞれの添字で表している物体のそれぞれの成分の速さとして,与えられます.

		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{

						}


						break;
					}
				}
				break;

			}
			return true;
		}

以上のonTouchEvent()メソッドをSurfaceViewを継承したカスタムビューに組み込んで,全体を表示するアクティビティは,以下のようになります.

package jp.ac.bunkyo.a2dgames;

/**
 * 2Dゲームで物体の放物運動(同一平面上)の描画を説明する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.util.Random;

import jp.ac.bunkyo.a2dgames.MotionFall.MFView;
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 MotionParabolic extends Activity {

	MPView 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_motion_parabolic);

		m_view = new MPView(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.motion_parabolic, 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 MPView 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 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 MPView(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(y[i] > screen_height){ 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{

						}


						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:20130330233646p:plain

*1:Eniacなど初期の電子計算機が弾道計算のために開発されたのは有名ですが,正確に弾道を計算しようとすると,弾自身の回転による慣性力,弾の大きさや突起物による空気の抵抗,風による外力などが無視できなくなります.

物体の運動の表現をタッチによって開始する

単一の指による画面タッチで説明したように,画面のタッチイベントをonTouchEvent()で処理するとき,以下のようなアクションをひろっていきます.

  • ACTION_DOWN:画面に触れたとき
  • ACTION_MOVE:画面上を指でなぞっているとき
  • ACTION_UP:画面から指が離れたとき
  • ACTION_CANCEL:指の位置を追えなくなったとき

そのほか,ジェスチャーを感知することもできます.

これらのタッチイベントの種類とタッチを感知している位置(getX()とgetY()メソッドで取得可能)で動作の開始・終了・変更を定義します.
以下の例で,画面に触れると同時に描画の開始を行うような処理をしていますが,これは動作の種類と合わせて(例えば,case ACTION_UP:として,”画面から指が離れたとき”へ)変更することができます.

画面タッチと物体の運動の連動の実装例

以下のコードでは,決まった領域を画面タッチして,弾の様に見える上方向に移動する円を表示開始の指示を与えるようにしてあります.

この決まった領域を変形し,それらしい場所に配置して,Bitmapオブジェクトで読み込んだボタン画像などを配置しておけば,画面上のボタンを押しているような視覚効果を与えることができます.
また,弾のように見える円を線にしたり,運動の方向を変えることで,より豊かな表現が可能です.
横方向にすれば,横方向のシューティングにもなります.

SurfaceViewを継承したゲーム用ビューのコンストラクタでは,アセットにある効果音をSoundPoolのオブジェクトに読み込み,終了時にこのSoundPoolのオブジェクトを解放しています.
アセットをゲーム用ビューのコンストラクタで読み込む→終了時に解放,という流れは,Androidアプリとしてのゲーム作成の基本として憶えておきましょう.

package jp.ac.bunkyo.a2dgames;

/**
 * 2Dゲーム画面中の決められた位置をタッチすることで他の動くオブジェクトを描画する動作の説明
 * @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.util.Random;

import jp.ac.bunkyo.a2dgames.MotionParabolic.MPView;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
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 TouchOnFire extends Activity {

	ToFView 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_touch_on_fire);

		m_view = new ToFView(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.touch_on_fire, 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 ToFView extends SurfaceView implements Runnable, SurfaceHolder.Callback{

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

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

    	Thread renderThread;

    	Context pContext;

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

    	// 描画対象を管理するメンバ変数(描画対象はクラスとしてもよいが,アクセサ(set/get)は利用しないほうが良いとされる)
    	int n=10; //任意数の描画対象を管理するには,配列ではなくListなどを利用する
       	boolean[] isVisible = new boolean[n]; //描画範囲にあるかどうかかを判定する
    	int[] x = new int[n];       //描画位置のX座標
    	int[] y = new int[n];       //描画位置のY座標
    	int[] radial = new int[n];  //描画する円の半径
    	float velocity = 20;//描画する円の移動速度(任意数のピクセル/frameを速度とする)

    	SoundPool se_pool;      //SE(サウンドエフェクト)を保持して再生するため
    	int se_id = -1;         //SEを識別するためのハンドル(複数の場合は配列にする)


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

			pContext=context;

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

    		for(int i=0; i<n; i++){
    			isVisible[i] = false;
    		}
    		velocity = rand.nextFloat()*20 + 10;

    		//SE再生のための準備
    		se_pool = new SoundPool(20,AudioManager.STREAM_MUSIC, 0); //引数:最大同時再生数,オーディオ出力先,デフォルト値

    		try{
    			AssetManager asset_mng = getAssets();
    			AssetFileDescriptor desc = asset_mng.openFd("b_se11.wav");
    			se_id = se_pool.load(desc, 1); //loadの引数:assetsからの読み取りを行う機能,再生音声の優先度
    		} catch(IOException e){
    			//エラーメッセージの出力が必要であれば,表示用文字列を作成
    		}

       		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);
							y[i] = y[i] - (int)(velocity * (float)(now - start)/100.0f); //Y軸方向を上方向に移動

							if(y[i] < 30 + radial[i]){ 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.WHITE);
    		paint.setTextSize(15);
    		canvas.drawText("画面下の矩形部分をタッチしてください.", 10, 20, paint);

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

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

    		//矩形の描画(発射ボタンなどBitmapオブジェクトの代わり)→この矩形範囲は必ずタッチイベント処理でも用いること
    		paint.setColor(Color.BLUE);
    		canvas.drawRect(100,screen_height - 100, screen_width-100, screen_height,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();
	        }

			if(se_pool != null){
				se_pool.unload(se_id);
			}

	        ((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: //指が画面に触れたとき

				//指定の位置をタッチしたかどうかの判定
				int touch_x = (int)event.getX();
				int touch_y = (int)event.getY();

				if(touch_x < 100 || touch_x > screen_width - 100){ break; }
				if(touch_y < screen_height - 100){ break; }

				//上記の条件をクリアした場合,円の描画を指示
				for(int i=0; i<n; i++){
					if(isVisible[i] == false){
						isVisible[i] = true;
						x[i] = screen_width/2;
						y[i] = screen_height - 100;
						radial[i] = rand.nextInt(20)+1;

						if(se_id != -1){
							se_pool.play(se_id, 1, 1, 0, 0, 1);
							//play()の引数:SEのid,左バランス,右バランス,優先度,ループの有無,再生速度のレート
						}
						break; //forループを抜ける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:20130330233645p:plain

落下運動の表現

等速運動の表現では,画面上をそれぞれに与えられた速さで動く物体(描画対象を)を表現しました.
ところが,一般に,物体には加速度が加わっています.
運動する物体(例えば,乗物)上にいると,運動の方向と加速度の方向が一致しているときは,加速/減速として,感じられます.
運動の方向と加速度の方向が異なると,横Gあるいは上下動として,感じられます.
下の図は,加速度と運動の方向が一致する場合ですが,水平方向だけではなく,

f:id:hidenaoA:20130330233642p:plain

さて,加速度の方向と異なる方向に初速度の成分があった場合,どうなるでしょう.
下の図は,鉛直にかかる加速度(一般に「重力(加速度)」と呼ばれる)に対して,水平方向に初速度を与えた場合の運動を模式的に表しています.
初速度の鉛直方向の速さを0とした場合でも,時間経過と共に速度が増していくことが分かります.

f:id:hidenaoA:20130330233643p:plain

落下運動の実装例

以下に,水平方向にのみ初速度を与えた落下運動を表現するためのコードをしまします.
このプログラムでは,画面を左から右になぞった距離に応じてX軸方向の初速度が与えられ,円が飛び出し,落下していきます.

package jp.ac.bunkyo.a2dgames;

/**
 * 2Dゲームで落下する物体の描画を説明する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.util.Random;

import jp.ac.bunkyo.a2dgames.MotionUni.MUView;
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 MotionFall extends Activity {

	MFView 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_motion_fall);

		m_view = new MFView(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.motion_fall, 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 MFView 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];//描画する円の移動速度(任意数のピクセル/10msecを速度とする)
    	float[] velocity_y = new float[n];//描画する円の移動速度(任意数のピクセル/10msecを速度とする)
    	float start_x, end_x;
    	long touch_down, touch_up;

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


		public MFView(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(y[i] > screen_height){ 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(20);
    		canvas.drawText("初速を決めるため画面を左から右へなぞってください", 10, 20, paint);

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

    		//円の描画
    		paint.setStyle(Style.FILL); //塗りつぶし
    		paint.setColor(Color.RED); //色を指定
    		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();
				break;
			case MotionEvent.ACTION_UP: //指が画面から離れたとき
			case MotionEvent.ACTION_CANCEL:
				end_x = event.getX();
				for(int i=0; i<n; i++){
					if(isVisible[i] == false){
						isVisible[i] = true;
						x[i] = 0;
						y[i] = 100;
						radial[i] = rand.nextInt(20)+1;
						if(end_x - start_x < 50){
							velocity_x[i] = 5.0f;
						}
						else{
							velocity_x[i] = (end_x - start_x)/10.0f;
						}
						velocity_y[i]=0.0f;

						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:20130330233644p:plain