2014年8月30日土曜日

2-3. Unity で砲台ゲームを作る - 改良


ソースはこちらで公開しています。
改良版: https://github.com/matsushima-terunao/unity_toride

実際のゲームはこちら
PC 改良版: http://ter.dip.jp/blog/unity_toride/build/build.html
Android 改良版: https://github.com/matsushima-terunao/unity_toride/raw/master/build/toride.apk

爆破エフェクトの変更

爆破エフェクトは Particle System から作成しましたが、用意されている素材を利用するように修正します。
 Project タブ内の Assets を右クリック -> Import New Package -> Particles
 Importing package ダイアログで Import をクリック
 Assets -> Standard Assets -> Particles -> Fire -> Fire1 を Hierarchy にドロップ、さらに Project -> Assets にドロップして Prefab 化、Hierarchy -> Fire1 は削除
 Fire1 配下の OuterCore, InnerCore, smoke に対して、Inspector -> Ellipsoid Particle Emitter に Min Size, Max Size, Ellipsoid の値を3倍にし、大きくする



爆破効果音の追加

爆破エフェクト発生時に効果音を鳴らすようにします。
 wav ファイルを用意し、Project -> Assets にドロップ
 Fire1 に Inspector 上で Add ComponentAudio Source, Audio Listener を追加
 Audio Source -> Audio Clip に Assets に追加した効果音をドロップ



地形の自動生成

スクリプトで、ゲームが始まるたびに地形を自動生成するようにします。
ソースはこちら
https://github.com/matsushima-terunao/unity_toride/blob/master/Assets/terrainScript.js

スクリプトは Hierarchy -> Terrain に関連付けます。Inspector タブ上に表示されるスクリプト内変数のうち以下のものを関連付けます。
 Particle Prefab: Project -> Assets -> Fire1
 Player: Hierarchy -> cannon(自分側)
 Other: Hierarchy -> cannon(相手側)

スクリプトで地形の高さデータを変更するには、高さデータの配列を作成し、TerrainData.SetHeights() に渡します。
高さデータの配列は Start() で TerrainData.GetHeights() て取得しています。
地形データはフラクタルアルゴリズムで作成しています。プログラムは Oh!X 1995年5月号の記事を参考にしました。
/** 変更前の高さマップ */
var heightsOrg: float[,];
/** 高さマップ */
var heights: float[,]; // 高さマップ

function Start () {
	var terrain: Terrain = gameObject.GetComponent(Terrain);
	var w: int = terrain.terrainData.heightmapWidth;
	var h: int = terrain.terrainData.heightmapHeight;
	Debug.Log("w=" + w + ",h=" + h);
	heightsOrg = terrain.terrainData.GetHeights(0, 0, w, h); // 変更前の高さマップ
	heights = terrain.terrainData.GetHeights(0, 0, w, h); // 高さマップ
}

/**
 * 初期化。
 */
function Init() {
	var terrain: Terrain = gameObject.GetComponent(Terrain);
	var w: int = terrain.terrainData.heightmapWidth;
	var h: int = terrain.terrainData.heightmapHeight;
	Debug.Log("w=" + w + ",h="  + h);
	fractal(512, 1.5, 0.3, heights);
	terrain.terrainData.SetHeights(0, 0, heights);
}

地形を削る

砲弾が地形に当たったときに地形を削るようにします。
衝突イベント内で、衝突地点を中心に高さ配列のデータを変更します。なお、Inspector 上で Pixel Error を 1 にしておかないと、周辺の地形にも影響が出てしまいます。
また、範囲内に砲台があったときにヒットするようにします。
function OnCollisionEnter(collision: Collision) {
	MainScript.Log("terrain collision", "terrain collision: " + collision.gameObject.name + "," + collision.contacts[0].point);
	if ("ball(Clone)" == collision.gameObject.name) {
		Destroy(collision.gameObject);
		// 破壊エフェクト。
		var particle: GameObject = Instantiate(particlePrefab, collision.transform.position - Vector3(0, 5, 0), Quaternion.identity); // インスタンスの生成
		particle.tag = "Respawn";
		Destroy(particle, 3);
		// 穴を掘る
		var terrain: Terrain = gameObject.GetComponent(Terrain);
		var R: int = 5; // 穴の半径
		var D: float = 0.002; // 穴の深さ
		var w: int = terrain.terrainData.heightmapWidth;
		var h: int = terrain.terrainData.heightmapHeight;
		var mapX: int = collision.contacts[0].point.z * w / terrain.terrainData.size.x; // heightmap 上の座標
		var mapZ: int = collision.contacts[0].point.x * h / terrain.terrainData.size.z; // heightmap 上の座標
		var mapR: int = R * w / terrain.terrainData.size.z; // heightmap 上の座標
		// z -> x
		var z1: int = Mathf.Max(-mapR, -mapZ);
		var z2: int = Mathf.Min(mapR, -mapZ + h - 1);
		for (var z: int = z1; z <= z2; ++ z) {
			// x -> y
			var mapW: int = Mathf.Sqrt(mapR * mapR - z * z);
			var x1: int = Mathf.Max(-mapW, -mapX);
			var x2: int = Mathf.Min(mapW, -mapX + w - 1);
			for (var x: int = x1; x <= x2; ++ x) {
				heights[x + mapX, z + mapZ] = Mathf.Max(0, heights[x + mapX, z + mapZ] - D * Mathf.Sqrt(mapW * mapW - x * x));
			}
		}
		terrain.terrainData.SetHeights(0, 0, heights);
		// ヒット。
		for (var cannon: GameObject in [player, other]) {
			if ((cannon.transform.position - collision.transform.position).magnitude
					< cannon.transform.FindChild("Sphere").transform.localScale.x / 2 + R) {
				cannon.GetComponent(cannonScript).Hit();
			}
		}
	}
}

視点の変更

マウス、タッチ操作で視点を変えられるようにします。
赤枠内で、クリック、タッチ位置が Player 砲台上かを判定します。砲台上のときは従来の砲台操作、それ以外の場合は視点操作にします。
青枠内で、ホイール、ピンチ量をズーム値とします。
緑枠内で実際の視点の変更を行います。
 地形の中心(50, 0, 50)からカメラへのベクトルを求める
 横方向の角度、縦方向の角度、長さの極座標に変換
 横移動量、縦移動量、ズーム値をそれぞれ加算
 再度直交座標に変換し、カメラの位置を更新
 カメラを地形の中心(50, 0, 50)に向ける
	var inputResolution: float = 90.0 / Mathf.Min(Screen.width, Screen.height); // 操作回転量: 画面短辺 -> 90度
	var inputDistance: Vector3 = Vector3.zero; // 入力移動量
	var fire: boolean = false; // 発射
	if (gameObject.CompareTag("Player")) {
		// 自分
		// mouse drag: 移動量を求める
		var mousePosPrev: Vector3 = mousePos;
		mousePos = Input.mousePosition;
		if (mouseDown) {
			inputDistance = mousePos - mousePosPrev;
		}
// mouse down if (Input.GetMouseButtonDown(0)) { mouseDown = true; // player 上判定 var hit: RaycastHit; var ray: Ray = Camera.main.ScreenPointToRay(mousePos); mouseOnPlayer = (Physics.Raycast(ray, hit, 100) && (gameObject == hit.collider.gameObject || hit.collider.transform.IsChildOf(transform))); }
// mouse up: 発射 if (Input.GetMouseButtonUp(0)) { fire = mouseOnPlayer; mouseDown = false; }
// mouse wheel: ズーム inputDistance.z = Input.GetAxis("Mouse ScrollWheel") * 10;
// touch var touchPosPrev: Vector3 = touchPos; if (1 == Input.touchCount) { var touch:Touch = Input.GetTouch(0); switch (touch.phase) {
// touch down case TouchPhase.Began: touchDown = true; touchPos = touch.position; // player 上判定 ray = Camera.main.ScreenPointToRay(mousePos); touchOnPlayer = (Physics.Raycast(ray, hit, 100) && (gameObject == hit.collider.gameObject || hit.collider.transform.IsChildOf(transform))); break;
// touch swipe: 移動量を求める case TouchPhase.Moved: touchPos = touch.position; inputDistance = touchPos - touchPosPrev; break; // touch up: 発射 case TouchPhase.Ended: fire = touchDown; touchDown = false; touchPos = touch.position; if (fire) { inputDistance = touchPos - touchPosPrev; } break; }
} else if (Input.touchCount >= 2) { // touch pinch: ズーム touchDown = false; var touch0:Touch = Input.GetTouch(0); var touch1:Touch = Input.GetTouch(1); if (TouchPhase.Began == touch1.phase) { touchPos.z = Vector2.Distance(touch0.position, touch1.position); } else if (TouchPhase.Moved == touch0.phase || TouchPhase.Moved == touch1.phase) { touchPos.z = Vector2.Distance(touch0.position, touch1.position); inputDistance = Vector3(0, 0, -(touchPos.z - touchPosPrev.z) / 10); } }
// mouse or touch 移動量 -> 回転 // 縦方向: x軸回転 // 横方向: y軸回転 if (Vector3.zero != inputDistance) { if (mouseOnPlayer || touchOnPlayer) { var angle: Vector3 = transform.eulerAngles - Vector3(inputDistance.y, inputDistance.x, 0) * inputResolution; angle.x = Mathf.Max(0, Mathf.Min(80, angle.x)); angle.y = Mathf.Max(80, Mathf.Min(190, (angle.y + 180) % 360)); // -100..10 angle.y = (angle.y + 180) % 360; transform.eulerAngles = angle; MainScript.Log("eulerAngles", "eulerAngles=" + angle + "," + transform.eulerAngles); } else {
// 地形の中心からカメラへのベクトル var terrainPos: Vector3 = Vector3(terrain.terrainData.size.x / 2, 0, terrain.terrainData.size.z / 2); var pos: Vector3 = Camera.main.transform.position - terrainPos; // 極座標に変換 var a: float = Mathf.Atan2(pos.z, pos.x); var b: float = Mathf.Atan2(pos.y, Mathf.Sqrt(pos.x * pos.x + pos.z * pos.z)); var c: float = Mathf.Sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z); // 回転、ズーム a -= inputDistance.x * inputResolution / 30; b = Mathf.Max(0, Mathf.Min(Mathf.PI / 2.1, b - inputDistance.y * inputResolution / 30)); c = Mathf.Max(terrainPos.x, c + inputDistance.z); // 直交座標に変換 pos.x = c * Mathf.Cos(b) * Mathf.Cos(a); pos.z = c * Mathf.Cos(b) * Mathf.Sin(a); pos.y = c * Mathf.Sin(b); Camera.main.transform.position = pos + terrainPos; Camera.main.transform.LookAt(terrainPos);
} }

→ 3-1. Unity でユニティちゃんアクションゲームを作る - キャラクターの配置
← 2-2. Unity で砲台ゲームを作る - スクリプトの作成
↑ 一覧

2014年8月22日金曜日

2-2. Unity で砲台ゲームを作る - スクリプトの作成

一通り画面を作成したら、スクリプトを作成します。

ソースはこちらで公開しています。
本ページ: https://github.com/matsushima-terunao/unity_toride_old
改良版: https://github.com/matsushima-terunao/unity_toride

実際のゲームはこちら
PC 本ページ: http://ter.dip.jp/blog/unity_toride_old/build/web/web.html
PC 改良版: http://ter.dip.jp/blog/unity_toride/build/build.html
Android 本ページ: https://github.com/matsushima-terunao/unity_toride_old/raw/master/build/toride.apk
Android 改良版: https://github.com/matsushima-terunao/unity_toride/raw/master/build/toride.apk

MainScript

ゲーム全体を管理するスクリプトです。MainScript は特にオブジェクトに関連したスクリプトではありませんが、オブジェクトに関連付けないとスクリプトが動作しないので、ここでは Main Camera に関連付けます。
 Project タブ -> Create -> Javascript -> 名前を MainScript に変更
MainScript をダブルクリック -> MonoDevelop が起動するのでスクリプトを編集
MainScript を Hierarchy タブ -> Main Camera へドロップして関連付ける
スクリプト内に宣言した変数が Inspector タブ -> Game Main (Script) パネル内に表示されます。これをオブジェクトに関連付けます。(スクリプト中の青枠の部分)
 Hierarchy タブ -> canon(Player)Player の右の枠 へドロップ
 Hierarchy タブ -> canon(相手側)Other の右の枠 へドロップ
 Hierarchy タブ -> GUI TextLabel の右の枠 へドロップ



Update() 内で、ゲーム中以外ではラベルを表示し、マウスクリックまたは画面タッチでゲームをスタートさせます。

/**
 * ゲームメイン。
 * 
 * @author 2014/08 matsushima
 */

#pragma strict

/** ゲーム状態: 開始 */
static var STATE_START:int = 0;
/** ゲーム状態: ゲーム中 */
static var STATE_RUNNING:int = 1;
/** ゲーム状態: 勝ち */
static var STATE_WIN:int = 2;
/** ゲーム状態: 負け */
static var STATE_LOSE:int = 3;

/** ゲーム状態 */
static var state:int = STATE_START; // ゲーム状態: 開始
/** デバッグテキスト */
static var debugTexts:Hashtable = new Hashtable();
/** プレーヤー */ var player:Transform; /** 対戦相手 */ var other:Transform; /** テキストラベル */ var label:Transform;
function Start () { } function Update () { switch (state) { case STATE_START: // ゲーム状態: 開始 label.guiText.text = "START"; label.guiText.enabled = true; break; case STATE_RUNNING: // ゲーム状態: ゲーム中 break; case STATE_WIN: // ゲーム状態: 勝ち label.guiText.text = "YOU WIN"; label.guiText.enabled = true; break; case STATE_LOSE: // ゲーム状態: 負け label.guiText.text = "WIN LOSE"; label.guiText.enabled = true; break; } // click or touch start if (label.guiText.enabled) { if (Input.GetMouseButtonUp(0) || (Input.touchCount > 0) && (TouchPhase.Ended == Input.GetTouch(0).phase)) { state = STATE_RUNNING; // ゲーム状態: ゲーム中 label.guiText.enabled = false; player.GetComponent(cannonScript).Init(); other.GetComponent(cannonScript).Init(); } } } function OnGUI() { // デバッグテキスト表示 var y:int = 0; for (var k:Object in debugTexts.Keys) { GUI.Label(Rect(0, y * 20, 400, 20), debugTexts[k].ToString()); ++ y; } } static function Log(key:String, text:Object) { // // デバッグテキスト表示登録 // debugTexts[key] = text; // // デバッグテキストログ出力 // print(text); }

cannonScript

砲台を制御するスクリプトです。MainScript と同様の方法でスクリプトを Hierarchy タブの cannon(Player, 対戦相手両方)に関連付けます。同様に Inspector タブ上に表示されるスクリプト内変数のうち以下のものを関連付けます。
 Ball Prefab: Project -> Assets -> ball
 Particle Prefab: Project -> Assets -> Particle System
 Terrain: Hierarchy -> Terrain
Update() はフレームごとに定期的に呼び出されますが、Player と相手砲台それぞれで呼ばれます。それで、Inspector タブの Tag に Player が設定されているかを
	if (gameObject.CompareTag("Player")) {
で識別しています。
赤枠内は Player の入力判定処理です。前回押下状態なら移動量を求め、今回離されたなら発射フラグを立てます。
緑枠内の処理で移動量から Player の砲台の回転量を決め、回転させます。
青枠内は相手側砲台の処理です。相手側砲台と前回砲弾着点との角度と、Player 砲台と前回砲弾着地点との角度の差から、Y軸方向の回転量を決めます。また、相手側砲台と前回砲弾着点との距離と、Player 砲台と前回砲弾着地点との距離の差から、X軸方向の回転量を決めます。
オレンジ枠内は砲弾の発射処理です。まず、砲台の角度から砲弾の発射角度を求めておきます。そして、Instantiate() で砲弾の Prefab から砲弾を実体化します。
Object.Instantiate

static function Instantiate(original: Object, position: Vector3, rotation: Quaternion): Object;
static function Instantiate(original: Object): Object;
パラメータ
 original: 複製したい既存オブジェクト
 position: 新規オブジェクトの位置
 rotation: 新規オブジェクトの回転
戻り値
 original のオブジェクトをクローンしたうえで返り値とします

オブジェクトをエディタ上の Duplicate コマンドと同様の方法で複製します。
AddForce() で砲弾に発射角度の力を加えて移動させます。
Rigidbody.AddForce

AddForce(force: Vector3, mode: ForceMode = ForceMode.Force): void;
パラメータ
 force: 加える力のベクトル
 mode: どのように力を適用するか

AddForce(x: float, y: float, z: float, mode: ForceMode = ForceMode.Force): void;
パラメータ
 x: 加える力のベクトルの X 成分
 y: 加える力のベクトルの Y 成分
 z: 加える力のベクトルの Z 成分
 mode: どのように力を適用するか

Rigidbodyオブジェクトに力を加える。加えることでオブジェクトを動かすことができる。
数フレームに渡って力を加えるには Update でなく FixedUpdate の中で適用すべきです。
砲弾側のスクリプトの変数に砲台を代入します。「オブジェクト.GetComponent(スクリプト名).変数名または関数名」で別スクリプトの変数・関数にアクセスできます。
GameObject.GetComponent

GetComponent(type: Type): Component;
パラメータ
 コンポーネントの型

GetComponent(type: string): Component;
パラメータ
 コンポーネントの型の名前

コンポーネントを取得します。コンポーネントを取得することで、Rigidbodyクラスやスクリプトなどを参照したりすることが可能になります。
OnCollisionEnter() 内は砲台がほかのオブジェクトと衝突したときの処理です。砲弾と衝突した場合、破壊エフェクトを生成し、砲台が Player か相手かによって勝ち負けを判断します。
MonoBehaviour.OnCollisionEnter(Collision)

OnCollisionEnter(collision : Collision)
パラメータ
 collision: 接触地点、衝突強さなどの情報

collider/rigidbody がほかの collider/rigidbody に接触し始めたときに呼ばれる。
Init() は対戦開始時に MainScript から呼ばれる砲台初期化ルーチンです。

/**
 * 砲台。
 * 
 * @author 2014/08 matsushima
 */

#pragma strict

var BALL_FORCE:float = 1800;

/** 砲弾 */
var ballPrefab:Transform;
/** 爆発 */
var particlePrefab:ParticleSystem;
/** 爆発 */
var particle:ParticleSystem = null;
/** 地形 */
var terrain:Terrain;

/** 砲弾存在 */
var ballExists:boolean = false;
/** 砲弾位置 */
var ballPosition:Vector3;

/** マウスボタン */
var mouseDown:boolean = false;
/** マウス位置 */
var mousePos:Vector3;
/** タッチ */
var touchDown:boolean = false;
/** タッチ位置 */
var touchPos:Vector3;

function Start () {

}

function Update () {
	if (MainScript.STATE_RUNNING != MainScript.state) { // not ゲーム状態: ゲーム中
		return;
	}
	var angleResolution:float = 180.0 / Mathf.Min(Screen.width, Screen.height); // 操作回転量: 画面短辺 -> 180度
	var distance:Vector3 = Vector3.zero; // 入力移動量
	var fire:boolean = false; // 発射
	if (gameObject.CompareTag("Player")) {
		// 自分
// mouse drag: 移動量を求める var mousePosPrev:Vector3 = mousePos; mousePos = Input.mousePosition; if (mouseDown) { distance = mousePos - mousePosPrev; } // mouse down if (Input.GetMouseButtonDown(0)) { mouseDown = true; } // mouse up: 発射 if (Input.GetMouseButtonUp(0)) { fire = mouseDown; mouseDown = false; } // touch swipe var touchPosPrev:Vector3 = touchPos; if (Input.touchCount > 0) { var touch = Input.GetTouch(0); switch (touch.phase) { // touch down case TouchPhase.Began: touchDown = true; touchPos = touch.position; break; // touch swipe: 移動量を求める case TouchPhase.Moved: touchPos = touch.position; distance = touchPos - touchPosPrev; break; // touch up: 発射 case TouchPhase.Ended: fire = touchDown; touchDown = false; touchPos = touch.position; distance = touchPos - touchPosPrev; break; } }
// mouse or touch 移動量 -> 回転 // 縦方向: x軸回転 // 横方向: y軸回転 if (Vector3.zero != distance) { var angle:Vector3 = transform.eulerAngles - Vector3(distance.y, distance.x, 0) * angleResolution; angle.x = Mathf.Max(0, Mathf.Min(80, angle.x)); angle.y = Mathf.Max(80, Mathf.Min(190, (angle.y + 180) % 360)); // -100..10 angle.y = (angle.y + 180) % 360; transform.eulerAngles = angle; MainScript.Log("eulerAngles", "eulerAngles=" + angle + "," + transform.eulerAngles); }
} else {
// 相手 if (!ballExists) { // not 砲弾存在 var player:GameObject = GameObject.FindGameObjectWithTag("Player"); // 横回転量 = (プレイヤーとの角度 - 砲弾との角度) / 2 + ランダム var playerAngle = Mathf.Atan2( player.transform.position.z - transform.position.z, player.transform.position.x - transform.position.x) * 180 / Mathf.PI; var ballAngle = Mathf.Atan2( ballPosition.z - transform.position.z, ballPosition.x - transform.position.x) * 180 / Mathf.PI; distance.y -= (playerAngle - ballAngle) / 2 + Random.value * 2 - 1; // 縦回転量 = (プレイヤーとの距離 - 砲弾との距離) / 10 + ランダム var magnitude:float = (player.transform.position - transform.position).magnitude - (ballPosition - transform.position).magnitude; distance.x += magnitude / 10 + Random.value * 2 - 1; // 回転 transform.eulerAngles += Vector3(distance.x, distance.y, 0); // 発射。 fire = true; }
}
// 発射。 if (fire && !ballExists) { // 発射角度 var y:float = Mathf.Cos(2 * Mathf.PI * transform.eulerAngles.x / 360); var r:float = Mathf.Sin(2 * Mathf.PI * transform.eulerAngles.x / 360); var x:float = r * Mathf.Sin(2 * Mathf.PI * transform.eulerAngles.y / 360); var z:float = r * Mathf.Cos(2 * Mathf.PI * transform.eulerAngles.y / 360); angle = Vector3(x, y, z); // インスタンスの生成 var ball:Transform = Instantiate(ballPrefab, transform.FindChild("Cylinder").position + angle * 5, Quaternion.identity); ballExists = true; // 力を加える ball.rigidbody.AddForce(angle * BALL_FORCE); MainScript.Log("AddForce", "AddForce=" + angle * BALL_FORCE + "," + (angle * BALL_FORCE).magnitude); // 発射元の砲台 ball.GetComponent(ballScript).cannon = transform; }
} function OnCollisionEnter(collision: Collision) { MainScript.Log("cannon collision", "cannon collision: " + collision.gameObject.name + "," + collision.contacts[0].point); if ("ball(Clone)" == collision.gameObject.name) { // 砲台非表示 gameObject.SetActive(false); // 破壊エフェクト。 // インスタンスの生成 particle = Instantiate(particlePrefab, transform.position, Quaternion.identity); // 勝ち負け if (MainScript.STATE_RUNNING == MainScript.state) { MainScript.state = (gameObject.CompareTag("Player") ? MainScript.STATE_LOSE : MainScript.STATE_WIN); } } } /** * 初期化。 */ function Init() { ballExists = false; mouseDown = false; touchDown = false; // 破壊エフェクト破棄 if (null != particle) { Destroy(particle); particle = null; } // 砲台角度・位置 if (gameObject.CompareTag("Player")) { // プレイヤー transform.eulerAngles = Vector3(45, 315, 0); transform.position.x = 100 - 20 * Random.value; transform.position.z = 20 * Random.value; } else { // 対戦相手 transform.eulerAngles = Vector3(45, 135, 0); ballPosition = Vector3(100 * Random.value, 0, 100 * Random.value); transform.position.x = 20 * Random.value; transform.position.z = 100 - 20 * Random.value; } // 砲台の地形の高さ transform.position.y = terrain.terrainData.GetHeight( transform.position.x * terrain.terrainData.heightmapWidth / terrain.terrainData.size.x, transform.position.z * terrain.terrainData.heightmapHeight / terrain.terrainData.size.z) - transform.localScale.x; MainScript.Log("height", "height=" + transform.position.y); // 砲台表示 gameObject.SetActive(true); }

ballScript

砲弾を制御するスクリプトです。MainScript と同様の方法でスクリプトを Project タブの Assets 内の ball に関連付けます。
/**
 * 砲弾。
 * 
 * @author 2014/08 matsushima
 */

#pragma strict

/** 発射元の砲台 */
var cannon:Transform;

function Start () {

}

function Update () {
	// 高さ < 0 なら破棄
	cannon.GetComponent(cannonScript).ballPosition = transform.position;
	if (transform.position.y < 0) {
		Destroy(gameObject);
	}
}

function OnCollisionEnter(collision: Collision) {
	MainScript.Log("ball collision", "ball collision: " + collision.gameObject.name + "," + collision.contacts[0].point);
	// 地形なら破棄
	if ("Terrain" == collision.gameObject.name) {
		Destroy(gameObject, 3); // 3秒後に破棄
	}
}

function OnDestroy() {
	// 破棄されたら砲台側の砲弾生存フラグをクリア
	if (null != cannon) {
		cannon.GetComponent(cannonScript).ballExists = false; // 砲弾存在
	}
}

→ 2-3. Unity で砲台ゲームを作る - 改良
← 2-1. Unity で砲台ゲームを作る
↑ 一覧

2014年8月21日木曜日

2-1. Unity で砲台ゲームを作る

昔、ベーマガを読んでいた人には懐かしい、砦の攻防という砲台ゲームを作ってみます。

ソースはこちらで公開しています。
本ページ: https://github.com/matsushima-terunao/unity_toride_old
改良版: https://github.com/matsushima-terunao/unity_toride

実際のゲームはこちら
PC 本ページ: http://ter.dip.jp/blog/unity_toride_old/build/web/web.html
PC 改良版: http://ter.dip.jp/blog/unity_toride/build/build.html
Android 本ページ: https://github.com/matsushima-terunao/unity_toride_old/raw/master/build/toride.apk
Android 改良版: https://github.com/matsushima-terunao/unity_toride/raw/master/build/toride.apk

プロジェクトの作成

Unity 起動後、Unity - Project Wizard のダイアログ
(表示されない場合は、メニューの File -> New Project...)
Create New Project タブ -> Browse...
 Choose location for new project のダイアログで、
 プロジェクトを保存したいフォルダーに移動
 新しいフォルダ(ここでは toride)を作成
 このフォルダーを選択
 フォルダーの選択をクリック
Create をクリック
ここで Scene を保存しておきます。
 メニューの File -> Save Scene
 Save Scene ダイアログで、ファイル名(ここでは toride)を入力し、保存

地形を作る

Terrain 追加

Hierarchy タブ -> Create -> Terrain で Scene に地形が追加されます。
これだと地形が大きすぎるのでサイズを小さくします。
 Inspector タブ内の Terrain (Script) パネルの中のボタンが並んでいるツールバーの右端の歯車ボタンをクリック
Terrain Width, Length, Height をそれぞれ 100 に変更

視点変更

編集しやすいように、Scnene 内でマウスホィールや、手のひらボタン+ドラッグで Scene 内に収まるよう調整します。
ここで一度再生してみます。すると、横から見た視点になっているので、Main Camera の位置と方向を変更します。
このとき、Hierarchy や Scnene 上で Main Camera を選択した状態で、メニューの GameObject -> Align With View を選んでみてください。そうすると Scene で表示されている視点になるように、Main Camera が自動的に設定されます。
再度再生して確認してみてください。



ライトの追加

Hierarchy タブ -> Create -> Directional light
Scene にライトが追加されます。左側から光が当たるようにドラッグして移動します。



山を作る

 Hierarchy や Scnene 上で Terrain を選択
 Inspector タブ内の Terrain (Script) パネルの中のボタンが並んでいるツールバーの左端の山ボタンをクリック
Brushes から好きなブラシを選択
こうして Scene 内の Terrain 上でドラッグすると地形を盛り上げることができます。



Shift を押しながらドラッグすると逆に盛り下げることができます。しかし 0 の高さ未満には盛り下げることはできません。そのときは、あらかじめ
 Inspector タブ内の Terrain (Script) パネルの中のボタンが並んでいるツールバーの左から2番目の山ボタンをクリック
Height に好きな数値(ここでは 10)を入力して、Flatten をクリック
こうすることで地形全体をかさ上げすることができます。



Texture を貼る

あらかじめ素材をインポートしておきます。
 Project タブ内の Assets を右クリック -> Import New Package -> Terrain Assets
 Importing package ダイアログで Import をクリック
実際に地形に Texture を貼ります。
 Inspector タブ内の Terrain (Script) パネルの中のボタンが並んでいるツールバーの真ん中の筆ボタンをクリック
Edit Textures... -> Add Texture...
 Add Terrain Texture ダイアログで Texture -> Select
  Select Texture2D ダイアログで好きな Texture(ここでは Grass (Hill))をダブルクリック
Add



Texture で塗る

同様に Edit Textures... -> Add Texture... で Texture を追加し、Textures から Texture を選択し、ブラシで塗ることができます。

木を植える

 Inspector タブ内の Terrain (Script) パネルの中のボタンが並んでいるツールバーの木ボタンをクリック
Edit Trees... -> Add Tree...
 Add Tree ダイアログで Tree の右端の○ボタンをクリック
  Select GameObject ダイアログで好きな Texture(ここでは Palm)をダブルクリック
Add
これで地形をクリックしていくことで木を植えることができます。ただ、この状態だと木が大きすぎると思います。その場合は Inspector タブの Settings の値を調節してください。



砲台を作る

階層化されたオブジェクトを作る

球と筒を組み合わせて砲台を作ります。このように、複数のオブジェクトを組み合わせてキャラクターを作る場合は、オブジェクトを階層化すると扱い易くなります。この場合、空のオブジェクトを親にして球や筒を子にする方法や、球を親にして筒を子にする方法がありますが、ここでは前者の方法で作ってみます。
 メニューの GameObject -> Create Empty
 Hierachy タブ -> GameObject を右クリック -> Rename -> cannon に変更
 Hierachy タブ -> Create -> Sphere
 Hierachy タブ -> Create -> Cylinder
Sphere と Cylinder を cannon にドロップ
 Sphere を選択して大きさを変更
  Scale: X:10 Y:10 Z:10
 Sphere を選択して位置と大きさを変更
  Position: X:0 Y:6 Z:0
  Scale: X:2 Y:2 Z:2



当たり判定をつける

スクリプトでの当たり判定のために canon に Ragidbody をつけます。ただし、重力で下に移動しないように Use Gravity を off、地形などに接触したときにはじかれないように Is Kinematic を on にします。
 Hierarchy タブ -> cannon を選択
 Inspector タブ -> Add Component -> Physics -> Rigidbody
 Rigidbody -> Use Gravity のチェックを外す
 Rigidbody -> Is Kinematic のチェックを入れる

敵砲台を作る

砲台をコピーして敵砲台を作ります。スクリプトで自分と敵との判別のために、自分の砲台に Player の Tag をつけます。
 Hierarchy タブ -> cannon を右クリック -> Duplicate
 Hierarchy タブ -> cannon(コピー元) を選択
 Inspector タブ -> Tag -> Player



砲弾を作る

Sphere で砲弾を作ります。
 Hierachy タブ -> Create -> Sphere
Sphere を右クリック -> Rename -> ball に変更
 大きさを変更
  Scale: X:2 Y:2 Z:2

重力をつける

 Inspector タブ -> Add Component -> Physics -> Rigidbody
 Rigidbody -> Use Gravity にチェックが入っていることを確認

Prefab 化する

砲弾はゲーム中にスクリプトで実体化するので、Prefab 化し、Scene から外します。
 Hierachy タブ -> ball を Project タブ -> Assets にドロップ
 Hierachy タブ -> ball を右クリック -> Delete



炎エフェクトを作る

Particle System で砲台を破壊したときの炎エフェクトを作ります。
 Hierachy タブ -> Create -> Particle System
Particle の大きさを変更します。
 Inspector タブ -> Particle System -> Start Size: 10
色を変更します。
 Inspector タブ -> Particle System -> Color over Lifetime をクリックしてパネルを広げ、さらにチェックをつける
 Color 右のカラーバーをクリック
 Gradient Editor ダイアログで
  カラーバー下の矢印をクリックして選択
  (矢印はスライドでき、矢印横のスペースをクリックして矢印を追加できる)
  Color 右のカラーバーをクリック
 Color ダイアログで Colors 下のをクリック
 ダイアログ右上の×ボタンで閉じる

Prefab 化する

Particle System はゲーム中にスクリプトで実体化するので、Prefab 化し、Scene から外します。
 Hierachy タブ -> Particle System を Project タブ -> Assets にドロップ
 Hierachy タブ -> Particle System を右クリック -> Delete



テキストラベルを作る

ゲーム開始、終了時に表示するテキストのラベルを作ります。
 Hierachy タブ -> Create -> GUI Text
中央に大きく表示するように設定を変更します。
 Inspector タブ
  Position: X:0.5 Y:0.5 Z:0
  Anchor: middle center
  Alignment: center
  Font Size: 50
  Color: 好きな色



→ 2-2. Unity で砲台ゲームを作る - スクリプトの作成
← 1-1. Unity のインストールから簡単な使い方まで
↑ 一覧

2014年8月15日金曜日

1-1. Unity のインストールから簡単な使い方まで

インストール

Unity のサイト
http://japan.unity3d.com/
のダウンロードページ
http://japan.unity3d.com/unity/download/
から Windows 版をダウンロード、インストール。
初回起動時
 [v] Activate the free version of Unity -> OK
 Email, Password を入力 -> Create Account
 Name, Password, Password confirmation を入力、[v] I agree ... -> OK
 メールが届いたら、メール本文の Confirm email をクリック -> ブラウザで Unitify Account Confirmed と表示
 Password を入力 -> OK
 Answer a few questions でアンケートに答えて OK、または Not right now
 Start using Unity

プロジェクトの作成

Unity - Project Wizard の画面で(表示されなければ File -> New Project...)
Create New Project タブ -> Create
空のプロジェクトが作成されます。



まず床を作る

左上の Hierarchy タブ -> Create -> Plane
Scene に床が追加されます。




ボールを作る

Hierarchy タブ -> Create -> Sphere
Scene にSphere(球体)が追加されます。Sphere を上にドラッグして宙に浮かせます。



再生する

この状態で、メニュー下中央の再生ボタン(右三角)を押して再生してみます。



この状態だと、画面が暗いのと、ボールが宙に浮いたまま動かないのが分かります。
Scene タブをクリックして編集画面に戻ります。

ライトの追加

ライトには何種類かありますが、ここでは一方向に光を当てる Directional Light を追加します。
Hierarchy タブ -> Create -> Directional light
Scene にライトが追加されます。左側から光が当たるようにドラッグして移動します。



RagidBody の設定

ボールが重力で下に落ちるようにします。
Sphere をクリックして選択
メニューの Component -> Physics -> RagidBody
右下に RagidBody パネルが表示されます。
Use Gravity にチェックが入っていることを確認します。

Physic Material の追加

床に当たったら跳ね返るようにします。
左下の Project タブ -> Create -> Physic Material
追加された New Physic Material をクリック
右側の Inspector タブの Bounciness を 1 に変更
New Physic Material を 左上の Sphere にドラッグして関連付ける



これで再度再生してみます。ボールが床に落ちて跳ね返るようになりました。



Script を作成して動きをつける

スクリプトでボールに独自の動きをつけます。
Project タブ -> Create -> Javascript
Assets にスクリプトが追加され、Insector にスクリプトが表示されます。
Inspector タブの Open... をクリックするとエディタが起動し、スクリプトを編集できます。
ここでは、オブジェクトを定期的に移動するように以下のように修正します。

#pragma strict

function Start () {

}

function Update () {
	transform.x += 0.01;
}

Assets -> NewBehaviourScript を Hierachy -> Sphere にドラッグして関連付けます。



この状態で再生してみます。ボールが横方向に移動するのが分かります。

ビルド

ビルドして公開用のファイルを作成します。ここでは Windows 向けと Android 向けを試してみます。

Windows

メニューの File -> Build Settings...
Build Settings 画面で
 Platform: PC, Mac & Linux Standalone
 Target platform: Windows
 Archetecture: x86
 Build ボタンをクリック
保存ダイアログで好きなフォルダに移動してファイル名を入力し、保存ボタンをクリック

Android

あらかじめ Android SDK をインストールしておく必要があります。
/2014/08/1-1-android.html
メニューの File -> Build Settings...
Build Settings 画面で
 Platform: Android
 ほかはそのまま
 Player Settings... ボタンをクリック
 メイン画面の Inspector タブ
  Product Name: 先頭が数字またはアンダーバー(_)でないこと
  Other Settings -> Bundle Identifer: com.unity3d.プロジェクト名
 Build ボタンをクリック
 保存ダイアログで好きなフォルダに移動してファイル名を入力し、保存ボタンをクリック
 Select Android SDK root folder の画面で、SDK をインストールしたフォルダを選択し(私の場合は C:\Users\matsushima\AppData\Local\Android\android-sdk)、フォルダーの選択ボタンをクリック
保存フォルダに apk ができていますので、実際にスマートフォンにインストールして確認してみます。



→ 2-1. Unity で砲台ゲームを作る
↑ 一覧

2014年8月13日水曜日

1-5. Andriod + OpenGL でゲームを作ってみる

先ほどのサンプルをベースに、簡単なゲームを作ってみます。
ソースはこちらで公開しています。
https://github.com/matsushima-terunao/android_ringmuns
apk はこちら。
https://github.com/matsushima-terunao/android_ringmuns/raw/master/Ringmuns/bin/Ringmuns.apk



操作方法

Start ボタンを押してゲームスタート
< > ボタン、左右のフリック、スワイプで地面を回転
v ボタン、下へのフリック、スワイプでピースを下へ移動
* ボタン、タップでピースを入れ替え
同じピースが縦横ななめ3つ並ぶと消えます。

MainActivity.java


画面をタッチしたときの処理 OnTouchEvent() が追加されています。
package com.example.ringmuns;

import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.LinearLayout;

/**
 * メインアクティビティー。
 * 
 * @author 2014/08 matsushima
 *
 */
public class MainActivity extends Activity {

	public static MainActivity activity;

	/** ビュー */
	private MyView view;

	private static final int FP = LinearLayout.LayoutParams.FILL_PARENT;
	//private static final int WC = LinearLayout.LayoutParams.WRAP_CONTENT;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main); // アクティビティーにビューを設定
		activity = this;
		// myView
		this.view = new MyView(this);
		((LinearLayout)findViewById(R.id.layout)).addView(this.view, new LinearLayout.LayoutParams(FP, FP));
		// buttonKey
		findViewById(R.id.buttonLeft).setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				GameMain.keyLeft = true;
			}
		});
		findViewById(R.id.buttonRight).setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				GameMain.keyRight = true;
			}
		});
		findViewById(R.id.buttonDown).setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				GameMain.keyDown = true;
			}
		});
		findViewById(R.id.buttonFlip).setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				GameMain.keyFlip = true;
			}
		});
		findViewById(R.id.buttonStart).setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				GameMain.keyStart = true;
			}
		});
	}

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

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

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		//System.out.println(event.toString() + "\r\n");
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
			GameMain.mouseLeft = (MotionEvent.ACTION_DOWN == event.getAction());
		case MotionEvent.ACTION_MOVE:
			GameMain.mouseTime = event.getEventTime();
			GameMain.mouseX = (int)(event.getX() * 30 / view.getWidth());
			GameMain.mouseY = (int)(event.getY() * 30 / view.getWidth());
			break;
		}
		return true;
	}
}

MyView.java


今までと大きく変わりません。
package com.example.ringmuns;

import android.content.Context;
import android.opengl.GLSurfaceView;

/**
 * ビュー。
 * 
 * @author 2014/08 matsushima
 *
 */
public class MyView extends GLSurfaceView {

	public MyRenderer renderer;

	public MyView(Context context) {
		super(context);
		renderer = new MyRenderer();
		setRenderer(renderer); // レンダラーの設定
	}
}

MyRenderer.java


今までと大きく変わりません。
package com.example.ringmuns;

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

import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLU;

/**
 * レンダラー。
 * 
 * @author 2014/08 matsushima
 *
 */
public class MyRenderer implements Renderer {

	private GameMain controller = new GameMain();

	/** ビューポート */
	private int width, height;

	// option
	/** 視野 */
	public static float perspectiveFovy = 45, perspectiveNearZ = 1, perspectiveFarZ = 100;
	/** 視点 */
	public static float lookAtEyeX = 0, lookAtEyeY = 0, lookAtEyeZ = 0;
	/** 視点 */
	public static float lookAtCenterX = 0, lookAtCenterY = -0.35f, lookAtCenterZ = -1;
	/** 視点 */
	public static float lookAtUpperX = 0, lookAtUpperY = 1, lookAtUpperZ = 0;
	/** 移動 */
	public static float translateX = 0, translateY = 0, translateZ = -50;
	/** 回転 */
	public static float rotateX = 0, rotateY = 0, rotateZ = 0;
	/** 照明の位置 */
	public static boolean isLightPosition = true;
	/** 照明の位置 */
	public static float[] lightPosition = {-20.0f, 20.0f, 100.0f, 1.0f};
	/** 環境光 */
	public static boolean isLightAmbient = true;
	/** 環境光 */
	public static float[] lightAmbient = {0.5f, 0.5f, 0.5f, 1.0f};
	/** 拡散光 */
	public static boolean isLightDiffuse = true;
	/** 拡散光 */
	public static float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
	/** 鏡面光 */
	public static boolean isLightSpecular = true;
	/** 鏡面光 */
	public static float[] lightSpecular = {1.0f, 1.0f, 1.0f, 1.0f};
	/** 光無効 */
	public static float[] lightZero = {0, 0, 0, 0};
	/** 面法線 */
	public static boolean planeNormal = false;
	/** 頂点法線 */
	public static boolean vertexNormal = true;
	/** option が変更された */
	public static volatile boolean optionChanged = false;

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		gl.glEnable(GL10.GL_DEPTH_TEST); // デプスバッファを有効
		gl.glDepthFunc(GL10.GL_LEQUAL); // デプスバッファ比較: 以下
		if (isLightPosition) {
			gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_POSITION, lightPosition, 0); // 照明の位置
			gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_AMBIENT, isLightAmbient ? lightAmbient : lightZero, 0); // 環境光
			gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_DIFFUSE, isLightDiffuse ? lightDiffuse : lightZero, 0); // 拡散光
			gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_SPECULAR, isLightSpecular ? lightSpecular : lightZero, 0); // 鏡面光
			gl.glEnable(GL10.GL_LIGHTING); // ライティング有効
			gl.glEnable(GL10.GL_LIGHT0); // ライティング0有効
		}
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		this.width = width;
		this.height = height;
		gl.glViewport(0, 0, width, height); // ビューポートの設定
		gl.glMatrixMode(GL10.GL_PROJECTION); // 行列の選択: 射影
		gl.glLoadIdentity(); // 単位行列に置き換え
		GLU.gluPerspective(gl, perspectiveFovy, (float)width / height, perspectiveNearZ, perspectiveFarZ); // 視野の設定
		gl.glMatrixMode(GL10.GL_MODELVIEW); // 行列の選択: モデルビュー
		gl.glLoadIdentity(); // 単位行列に置き換え
		GLU.gluLookAt(gl, lookAtEyeX, lookAtEyeY, lookAtEyeZ, lookAtCenterX, lookAtCenterY, lookAtCenterZ, lookAtUpperX, lookAtUpperY, lookAtUpperZ); // 視点の設定
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		// メイン処理。
		controller.proc();
		// 描画
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // バッファクリア: カラーバッファ, デプスバッファ
		if (optionChanged) {
			optionChanged = false;
			gl.glMatrixMode(GL10.GL_PROJECTION); // 行列の選択: 射影
			gl.glLoadIdentity(); // 単位行列に置き換え
			GLU.gluPerspective(gl, perspectiveFovy, (float)width / height, perspectiveNearZ, perspectiveFarZ); // 視野の設定
			gl.glMatrixMode(GL10.GL_MODELVIEW); // 行列の選択: モデルビュー
			gl.glLoadIdentity(); // 単位行列に置き換え
			GLU.gluLookAt(gl, lookAtEyeX, lookAtEyeY, lookAtEyeZ, lookAtCenterX, lookAtCenterY, lookAtCenterZ, lookAtUpperX, lookAtUpperY, lookAtUpperZ); // 視点の設定
			if (isLightPosition) {
				gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_POSITION, lightPosition, 0); // 照明の位置
				gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_AMBIENT, isLightAmbient ? lightAmbient : lightZero, 0); // 環境光
				gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_DIFFUSE, isLightDiffuse ? lightDiffuse : lightZero, 0); // 拡散光
				gl.glLightfv(GL10.GL_LIGHT0,  GL10.GL_SPECULAR, isLightSpecular ? lightSpecular : lightZero, 0); // 鏡面光
				gl.glEnable(GL10.GL_LIGHTING); // ライティング有効
				gl.glEnable(GL10.GL_LIGHT0); // ライティング0有効
			} else {
				gl.glDisable(GL10.GL_LIGHTING); // ライティング無効
				gl.glDisable(GL10.GL_LIGHT0); // ライティング0無効
			}
		}
		// 描画。
		controller.draw(gl);
	}
}

MyModel.java


プリミティブの頂点の持ち方を変え、色も持てるようにしました。
package com.example.ringmuns;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

/**
 * モデル。
 * 
 * @author 2014/08 matsushima
 *
 */
public class MyModel {

	/** 頂点座標 */
	private static final float[][] vertices = {
		{
			-0.8f,-0.8f, 0.8f,  0.8f,-0.8f, 0.8f, -0.8f,0.8f, 0.8f,  0.8f,0.8f, 0.8f,
			 0.8f,-0.8f,-0.8f, -0.8f,-0.8f,-0.8f,  0.8f,0.8f,-0.8f, -0.8f,0.8f,-0.8f,
		}, {
			0,1.2f,0, -1,0,1, 1,0,1, 1,0,-1, -1,0,-1, 0,-1.2f,0,
		}, {
			-1.0f,1.2f,0, -1.0f,-1.2f,0, 1.0f,-1.2f,0, 1.0f,1.2f,0,
			-0.6f,0.7f, 0.5f, -0.6f,-0.7f, 0.5f, 0.6f,-0.7f, 0.5f, 0.6f,0.7f, 0.5f,
			-0.6f,0.7f,-0.5f, -0.6f,-0.7f,-0.5f, 0.6f,-0.7f,-0.5f, 0.6f,0.7f,-0.5f,
		}, {
			-0.6f,1.0f,-0.6f, -0.6f,1.0f,0.6f, 0.6f,1.0f,0.6f, 0.6f,1.0f,-0.6f,
			-1.0f,0.5f,-1.0f, -1.0f,0.5f,1.0f, 1.0f,0.5f,1.0f, 1.0f,0.5f,-1.0f,
			0.0f,-1.2f,0.0f,
		}, {
			-0.6f, 1.0f,-0.6f, -0.6f, 1.0f,0.6f, 0.6f, 1.0f,0.6f, 0.6f, 1.0f,-0.6f,
			-1.0f, 0.0f,-1.0f, -1.0f, 0.0f,1.0f, 1.0f, 0.0f,1.0f, 1.0f, 0.0f,-1.0f,
			-0.6f,-1.0f,-0.6f, -0.6f,-1.0f,0.6f, 0.6f,-1.0f,0.6f, 0.6f,-1.0f,-0.6f,
		}, { // KIND_FLUSH
			0,0.6f,0, -0.5f,0,0.5f, 0.5f,0,0.5f, 0.5f,0,-0.5f, -0.5f,0,-0.5f, 0,-0.6f,0,
		}, { // KIND_FLOOR
			0,0,0,
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 0.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 0.5 / 7),
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 1.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 1.5 / 7),
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 2.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 2.5 / 7),
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 3.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 3.5 / 7),
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 4.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 4.5 / 7),
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 5.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 5.5 / 7),
			3 * GameMain.WIDTH * (float)Math.sin(2 * Math.PI * 6.5 / 7), 0, 3 * GameMain.WIDTH * (float)Math.cos(2 * Math.PI * 6.5 / 7),
		}
	};
	/** 頂点インデックス */
	private static final int[][][] points = {
		{{0,1,2,3}, {4,5,6,7}, {5,0,7,2}, {1,4,3,6}, {2,3,7,6}, {5,4,0,1}},
		{{0,1,2}, {0,2,3}, {0,3,4}, {0,4,1}, {1,5,2}, {2,5,3}, {3,5,4}, {4,5,1}},
		{{0,4,3,7}, {1,5,0,4}, {2,6,1,5}, {3,7,2,6}, {4,5,7,6},  {3,11,0,8}, {0,8,1,9}, {1,9,2,10}, {2,10,3,11}, {9,8,10,11}},
		{{0,1,3,2},  {0,4,1,5}, {1,5,2,6}, {2,6,3,7}, {3,7,0,4},  {4,8,5}, {5,8,6}, {6,8,7}, {7,8,4}},
		{{0,1,3,2},  {0,4,1,5}, {1,5,2,6}, {2,6,3,7}, {3,7,0,4},  {4,8,5,9}, {5,9,6,10}, {6,10,7,11}, {7,11,4,8}},
		{{0,1,2}, {0,2,3}, {0,3,4}, {0,4,1}, {1,5,2}, {2,5,3}, {3,5,4}, {4,5,1}}, // KIND_FLUSH
		{{0,1,2}, {0,2,3}, {0,3,4}, {0,4,5}, {0,5,6}, {0,6,7}, {0,7,1}}, // KIND_FLOOR
	};
	/** 面の色 */
	private static final float[][][] colors = {
		{{0.5f,0,0,1}},
		{{0,0.5f,0,1}},
		{{0,0,0.5f,1}},
		{{0.5f,0,0.5f,1}},
		{{0,0.5f,0.5f,1}},
		{{1,1,0,1}}, // KIND_FLUSH
		{{0.5f,0,0,1}, {0.5f,0.5f,0,1}, {0,0.5f,0,1}, {0,0.5f,0.5f,1}, {0,0,0.5f,1}, {0.5f,0,0.5f,1}, {0.5f,0.5f,0.5f,1}}, // KIND_FLOOR
	};
	/** 頂点バッファ */
	private static FloatBuffer[] vertexBuffer = new FloatBuffer[vertices.length];
	/** 面法線ベクトル */
	private static float[][] normalVectors = new float[vertices.length][];

	// 種別
	public static final int KIND_DROP_CNT = 5;
	public static final int KIND_FLUSH = 5;
	public static final int KIND_FLOOR = 6;

	/** 種別 */
	public int kind;
	/** 消去済み */
	public boolean deleted;
	/** 座標 */
	public float x, y, z;
	/** 角度 */
	public float b;

	/**
	 * モデルを構築。
	 */
	{
		for (int k = 0; k < vertices.length; ++ k) {
			// 頂点数
			int vert_cnt = 0;
			for (int p = 0; p < points[k].length; ++ p) {
				vert_cnt += points[k][p].length;
			}
			// 頂点バッファ
			ByteBuffer buf = ByteBuffer.allocateDirect(vert_cnt * 3 * 4);
			buf.order(ByteOrder.nativeOrder());
			vertexBuffer[k] = buf.asFloatBuffer();
			// 面法線ベクトル
			normalVectors[k] = new float[points[k].length * 3];
			for (int p = 0; p < points[k].length; ++ p) {
				// 頂点バッファ
				for (int v = 0; v < points[k][p].length; ++ v) {
					vertexBuffer[k].put(vertices[k][points[k][p][v] * 3 + 0]);
					vertexBuffer[k].put(vertices[k][points[k][p][v] * 3 + 1]);
					vertexBuffer[k].put(vertices[k][points[k][p][v] * 3 + 2]);
				}
				// 面法線ベクトル
				float vx1 = vertices[k][points[k][p][1] * 3 + 0] - vertices[k][points[k][p][0] * 3 + 0];
				float vx2 = vertices[k][points[k][p][2] * 3 + 0] - vertices[k][points[k][p][0] * 3 + 0];
				float vy1 = vertices[k][points[k][p][1] * 3 + 1] - vertices[k][points[k][p][0] * 3 + 1];
				float vy2 = vertices[k][points[k][p][2] * 3 + 1] - vertices[k][points[k][p][0] * 3 + 1];
				float vz1 = vertices[k][points[k][p][1] * 3 + 2] - vertices[k][points[k][p][0] * 3 + 2];
				float vz2 = vertices[k][points[k][p][2] * 3 + 2] - vertices[k][points[k][p][0] * 3 + 2];
				float nx = vy1 * vz2 - vy2 * vz1;
				float ny = vz1 * vx2 - vz2 * vx1;
				float nz = vx1 * vy2 - vx2 * vy1;
				float nr = (float)Math.sqrt(nx * nx + ny * ny + nz * nz);
				normalVectors[k][p * 3 + 0] = nx / nr;
				normalVectors[k][p * 3 + 1] = ny / nr;
				normalVectors[k][p * 3 + 2] = nz / nr;
			}
			vertexBuffer[k].position(0);
		}
	}

	/**
	 * モデルを描画。
	 * 
	 * @param gl
	 */
	public void draw(GL10 gl) {
		gl.glPushMatrix();
		gl.glTranslatef(x + MyRenderer.translateX, y + MyRenderer.translateY, z + MyRenderer.translateZ); // 移動
		gl.glRotatef(MyRenderer.rotateX, 1, 0, 0); // 回転: x軸
		gl.glRotatef(b + MyRenderer.rotateY, 0, 1, 0); // 回転: y軸
		gl.glRotatef(MyRenderer.rotateZ, 0, 0, 1); // 回転: z軸
		gl.glFrontFace(GL10.GL_CCW); // 全面: 反時計回り
		gl.glEnable(GL10.GL_CULL_FACE); // 片面を表示しない
		gl.glCullFace(GL10.GL_BACK); // 裏面を表示しない
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 頂点配列を有効
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer[kind]); // xyz, float, padding なし, vertexBuffer
		if (MyRenderer.vertexNormal) {
			gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); // 法線配列を有効
			gl.glNormalPointer(GL10.GL_FLOAT, 0, vertexBuffer[kind]); // xyz, float, padding なし, vertexBuffer
		} else {
			gl.glDisableClientState(GL10.GL_NORMAL_ARRAY); // 法線配列を無効
		}
		int point = 0;
		for (int p = 0; p < points[kind].length; ++ p) {
			if (p < colors[kind].length) {
				gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT_AND_DIFFUSE, colors[kind][p], 0);
			}
			if (MyRenderer.planeNormal) {
				gl.glNormal3f(normalVectors[kind][p * 3 + 0], normalVectors[kind][p * 3 + 1], normalVectors[kind][p * 3 + 2]); // 面法線ベクトル
			}
			gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, point, points[kind][p].length); // 0120 1231 // プリミティブを描画
			point += points[kind][p].length;
		}
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); // 頂点配列を無効
		gl.glDisable(GL10.GL_CULL_FACE); // 片面を表示しないを無効
		gl.glPopMatrix();
	}
}

GameMain.java


ゲーム本体の処理です。
package com.example.ringmuns;

import javax.microedition.khronos.opengles.GL10;

import android.os.Handler;
import android.widget.TextView;

/**
 * ゲームメイン。
 * 
 * @author 2014/08 matsushima
 *
 */
public class GameMain {

	/** タッチ時間のしきい値 */
	private static final long TOUCH_TIME = 300;
	/** タッチ移動量のしきい値 */
	private static final float TOUCH_DISTANCE = 1;
	/** models のカラム数 */
	public static final int COLS = 7;
	/** models の行数 */
	public static final int ROWS = 15;
	/** model の幅 */
	public static final float WIDTH = 2.5f;

	/** ゲーム状態 */
	enum State {
		/** game over */
		GAME_OVER,
		/** 落下中 */
		DROPPING,
		/** 着地後固定前 */
		DROPPED,
		/** 判定中 */
		JUDGING,
		/** 判定後 */
		JUDGED,
	}

	/** ゲーム状態 */
	private State state = State.GAME_OVER; // game over
	/** ゲーム状態基準時刻 */
	private long stateTime = 0;
	/** 判定回数 */
	private int judgeCount;
	/** 得点 */
	private int score;
	/** スピード */
	private int speed;

	/** キー */
	public static boolean keyLeft, keyRight, keyDown, keyFlip, keyStart;
	/** マウス */
	public static boolean mouseLeft, mouseLeftPrev;
	/** マウス */
	public static long mouseTime, mouseLeftDownTime;
	/** マウス */
	public static int mouseXLeftDown, mouseYLeftDown, mouseXPrev, mouseYPrev, mouseX, mouseY;

	/** 行列 */
	private MyModel[][] models = new MyModel[COLS][ROWS];
	/** ドロップ */
	private MyModel[] dropModels = new MyModel[3];
	/** 次のドロップ */
	private MyModel[] nextModels = new MyModel[3];
	/** 床 */
	private MyModel floor;
	/** ドロップカウント */
	private int dropCount;
	/** ドロップ座標 */
	private int dropX;
	/** ドロップ座標 */
	private float dropY;
	Handler handler = new Handler();

	public GameMain() {
		floor = new MyModel();
		floor.kind = MyModel.KIND_FLOOR;
		floor.y = -ROWS * WIDTH + WIDTH / 4;
	}

	/**
	 * メイン処理。
	 */
	public void proc() {
		try {
			long curTime = System.currentTimeMillis();
			// 移動処理
			int dx = 0, dy = 0;
			boolean flip = false;
			// key
			if (keyLeft) {
				keyLeft = false;
				dx = -1;
			}
			if (keyRight) {
				keyRight = false;
				dx = 1;
			}
			if (keyDown) {
				keyDown = false;
				dy = 1;
			}
			if (keyFlip) {
				keyFlip = false;
				flip = true;
			}
			// mouse up down
			if (mouseLeft) {
				if (!mouseLeftPrev) {
					// ダウン
					mouseLeftDownTime = mouseTime;
					mouseXLeftDown = mouseXPrev = mouseX;
					mouseYLeftDown = mouseYPrev = mouseY;
				}
			} else {//if (!mouseLeft) {
				if (mouseLeftPrev) {
					// アップ
					if (mouseTime - mouseLeftDownTime < TOUCH_TIME
							&& Math.abs(mouseX - mouseXLeftDown) < TOUCH_DISTANCE
							&& Math.abs(mouseY - mouseYLeftDown) < TOUCH_DISTANCE) {
						// タッチ時間のしきい値未満 and 移動なし -> タップ
						flip = true;
					}
				}
			}
			// mouse move
			if ((mouseLeft && mouseLeftPrev) || (!mouseLeft && mouseLeftPrev)) {
				if (Math.abs(mouseX - mouseXLeftDown) >= Math.abs(mouseY - mouseYLeftDown)) {
					int x = (mouseX - mouseXLeftDown) / 3;
					// タッチ時間のしきい値未満 ? フリック : スワイプ
					if (mouseTime - mouseLeftDownTime < TOUCH_TIME) {
						x = (0 == x ? 0 : x > 0 ? 1 : -1);
					}
					dx = -(x - (mouseXPrev - mouseXLeftDown) / 3);
					mouseX = mouseXLeftDown + x * 3;
				} else {
					int y = mouseY - mouseYLeftDown;
					// タッチ時間のしきい値未満 ? フリック : スワイプ
					if (mouseTime - mouseLeftDownTime < TOUCH_TIME) {
						y = (0 == y ? 0 : y > 0 ? 1 : -1);
					}
					dy = y - (mouseYPrev - mouseYLeftDown);
					mouseY = mouseYLeftDown + y;
				}
			}
			mouseLeftPrev = mouseLeft;
			mouseXPrev = mouseX;
			mouseYPrev = mouseY;
			// 移動
			float dropYPrev = dropY;
			for (int i = 0; i < dy; ++ i) {
				if (State.DROPPING == state) { // 落下中
					dropYPrev = dropY;
					dropY = (curTime - stateTime) / speed;
					if (dropY > dropYPrev + 1) {
						dropY = dropYPrev + 1;
					}
					if ((dropY + 2 >= ROWS - 1) || (null != models[dropX][(int)dropY + 2 + 1])) {
						// 着地
						dropY = (float)(int)dropY;
						state = State.DROPPED; // 着地後固定前
						stateTime = curTime;
					} else {
						stateTime -= speed;
					}
				} else if (State.DROPPED == state) { // 着地後固定前
					// 着地後1秒経過
					stateTime = curTime - 1000;
					break;
				}
			}
			for (int i = 0; i < Math.abs(dx); ++ i) {
				if ((State.DROPPING == state) || (State.DROPPED == state)) { // 落下中 // 着地後固定前
					if (null != models[(dropX + COLS + (dx >= 0 ? 1 : -1)) % COLS][(int)dropY + 2]) {
						break;
					}
				}
				dropX = (dropX + COLS + (dx >= 0 ? 1 : -1)) % COLS;
			}
			if (flip) {
				MyModel model = dropModels[2];
				dropModels[2] = dropModels[1];
				dropModels[1] = dropModels[0];
				dropModels[0] = model;
			}

			// ゲーム状態別処理
			switch (state) {
			case GAME_OVER: // game over
				if (keyStart) {
					state = State.DROPPING; // 落下中
					stateTime = curTime;
					mouseLeft = mouseLeftPrev = false;
					score = 0;
					speed = 1000;
					dropCount = 1;
					dropX = 0;
					dropY = 0;
					models = new MyModel[COLS][ROWS];
					for (int i = 0; i < 3; ++ i) {
						dropModels[i] = new MyModel();
						dropModels[i].kind = (int)(Math.random() * MyModel.KIND_DROP_CNT);
						dropModels[i].b = i * 50;
						nextModels[i] = new MyModel();
						nextModels[i].kind = (int)(Math.random() * MyModel.KIND_DROP_CNT);
					}
				}
				break;
			case DROPPING: // 落下中
				// 着地判定
				dropY = (curTime - stateTime) / speed;
				if (dropY > dropYPrev + 1) {
					dropY = dropYPrev + 1;
				}
				if ((dropY + 2 >= ROWS - 1) || (null != models[dropX][(int)dropY + 2 + 1])) {
					// 着地
					dropY = (float)(int)dropY;
					state = State.DROPPED; // 着地後固定前
					stateTime = curTime;
				}
				break;
			case DROPPED: // 着地後固定前
				if (stateTime + 1000 > curTime) {
					if ((dropY + 2 >= ROWS - 1) || (null != models[dropX][(int)dropY + 2 + 1])) {
						// 着地後1秒未満
					} else {
						// 着地後再離陸
						state = State.DROPPING; // 落下中
						stateTime = curTime - (long)(dropY * speed);
					}
				} else {
					// 着地後1秒経過
					System.out.println("d4:" + dropY);
					for (int i = 0; i < 3; ++ i) {
						models[dropX][(int)dropY + i] = dropModels[i];
						dropModels[i] = null;
					}
					state = State.JUDGING; // 判定中
					stateTime = curTime - 500;
					judgeCount = 1;
				}
				break;
			case JUDGING: // 判定中
				if (stateTime + 500 > curTime) {
					break;
				}
				// 消去判定
				int deleteCnt = 0;
				if ((null != models[dropX][(int)dropY]) && (MyModel.KIND_FLUSH == models[dropX][(int)dropY].kind)) {
					// drop flush
					if (dropY + 2 + 1 >= ROWS) {
						// 地面
						deleteCnt = 100;
					} else {
						// 直下と同じ種類を消去
						int kind = models[dropX][(int)dropY + 2 + 1].kind;
						for (int x = 0; x < COLS; ++ x) {
							for (int y = 0; y < ROWS; ++ y) {
								if ((null != models[x][y]) && (kind == models[x][y].kind)) {
									models[x][y].deleted = true;
									++ deleteCnt;
								}
							}
						}
					}
				} else {
					final int[] dirs = {1,0, 0,1, 1,1, -1,1}; // 右, 下, 右下, 左下
					for (int d = 0; d < dirs.length; d += 2) {
						for (int x = 0; x < COLS; ++ x) {
							for (int y = 0; y < ROWS; ++ y) {
								int x1 = (x + COLS + dirs[d] * 1) % COLS;
								int x2 = (x + COLS + dirs[d] * 2) % COLS;
								int y1 = y + dirs[d + 1] * 1;
								int y2 = y + dirs[d + 1] * 2;
								if (y2 < ROWS
										&& null != models[x][y]
										&& null != models[x1][y1]
										&& null != models[x2][y2]
										&& models[x][y].kind == models[x1][y1].kind
										&& models[x][y].kind == models[x2][y2].kind) {
									deleteCnt += 3;
									models[x][y].deleted = true;
									models[x1][y1].deleted = true;
									models[x2][y2].deleted = true;
								}
							}
						}
					}
				}
				// 判定後消去あり ? flush : game over or new drop
				if (deleteCnt > 0) {
					// flush
					for (int x = 0; x < COLS; ++ x) {
						for (int y = 0; y < ROWS; ++ y) {
							if ((null != models[x][y]) && (models[x][y].deleted)) {
								models[x][y].kind = MyModel.KIND_FLUSH;
							}
						}
					}
					state = State.JUDGED; // 判定後
					stateTime = curTime;
				} else {
					// game over 判定
					for (int x = 0; x < COLS; ++ x) {
						if (null != models[x][2]) {
							state = State.GAME_OVER; // game over
							return;
						}
					}
					// new drop
					++ dropCount;
					boolean flush = (0 == dropCount % 100);
					for (int i = 0; i < 3; ++ i) {
						dropModels[i] = nextModels[i];
						dropModels[i].b = i * 50;
						nextModels[i] = new MyModel();
						nextModels[i].kind = (flush ? MyModel.KIND_FLUSH : (int)(Math.random() * MyModel.KIND_DROP_CNT));
					}
					state = State.DROPPING; // 落下中
					stateTime = curTime;
					mouseLeft = mouseLeftPrev = false;
					dropY = 0;
				}
				score += deleteCnt * 100 * judgeCount;
				speed = Math.max(100, 1000 - score / 100);
				final String textScore = "" + score;// + "," + speed;
				handler.post(new Runnable() {
					@Override
					public void run() {
						((TextView)MainActivity.activity.findViewById(R.id.textView1)).setText(textScore);
					}
				});
				break;
			case JUDGED: // 判定後
				if (stateTime + 500 > curTime) {
					break;
				}
				// 消されたものを詰める
				for (int x = 0; x < COLS; ++ x) {
					for (int y = ROWS - 1; y >= 0; ) {
						if (null == models[x][y]) {
							break;
						} else if (MyModel.KIND_FLUSH != models[x][y].kind) {
							-- y;
						} else {
							for (int i = y; i >= 1; -- i) {
								models[x][i] = models[x][i - 1];
							}
							models[x][0] = null;
						}
					}
				}
				state = State.JUDGING; // 判定中
				stateTime += 500;
				++ judgeCount;
				break;
			}
			keyLeft = keyRight = keyDown = keyFlip = keyStart = false;
		} catch (RuntimeException e) {
			System.out.println(e.toString());
			e.printStackTrace(System.out);
			throw e;
		}
	}

	private long drawTime = System.currentTimeMillis();

	/**
	 * 描画。
	 * 
	 * @param gl
	 */
	public void draw(GL10 gl) {
		floor.b = -360f * dropX / COLS;
		floor.draw(gl);
		for (int x = 0; x < COLS; ++ x) {
			float tx = (float)Math.sin(2 * Math.PI * (x - dropX) / COLS) * WIDTH * 2;
			float tz = (float)Math.cos(2 * Math.PI * (x - dropX) / COLS) * WIDTH * 2;
			for (int y = 0; y < ROWS; ++ y) {
				if (null != models[x][y]) {
					models[x][y].x = tx;
					models[x][y].y = -y * WIDTH;
					models[x][y].z = tz;
					models[x][y].b += floor.b;
					models[x][y].draw(gl);
					models[x][y].b -= floor.b;
				}
			}
		}
		long bTime = System.currentTimeMillis();
		float vb = (bTime - drawTime) * 360f / 2000f;
		drawTime = bTime;
		for (int i = 0; i < 3; ++ i) {
			if (null != dropModels[i]) {
				dropModels[i].x = 0;
				dropModels[i].y = -(dropY + i) * WIDTH;
				dropModels[i].z = WIDTH * 2;
				dropModels[i].b += vb;
				dropModels[i].b += floor.b;
				dropModels[i].draw(gl);
				dropModels[i].b -= floor.b;
			}
		}
		for (int i = 0; i < 3; ++ i) {
			if (null != nextModels[i]) {
				nextModels[i].x = WIDTH * 4;
				nextModels[i].y = -i * WIDTH;
				nextModels[i].z = 0;
				nextModels[i].b = 0;
				nextModels[i].draw(gl);
			}
		}
	}
}

activity_main.xml


特に大きく変わりません。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="0dp"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.ringmuns.MainActivity" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_margin="0dp"
        android:layout_marginLeft="34dp"
        android:layout_marginTop="21dp"
        android:orientation="vertical"
        android:padding="0dp" >

        <LinearLayout
            android:id="@+id/layout"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:orientation="vertical" >
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal" >

            <TextView
                android:id="@+id/textView1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1" />

            <TextView
                android:id="@+id/textView2"
                android:layout_width="30dp"
                android:layout_height="wrap_content"
                android:layout_weight="0" />

            <Button
                android:id="@+id/buttonStart"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:minHeight="48dp"
                android:text="start" />

            <TextView
                android:id="@+id/TextView01"
                android:layout_width="30dp"
                android:layout_height="wrap_content"
                android:layout_weight="0" />

            <Button
                android:id="@+id/buttonLeft"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:text="&lt;" />

            <Button
                android:id="@+id/buttonDown"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:text="v" />

            <Button
                android:id="@+id/buttonFlip"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:text="*" />

            <Button
                android:id="@+id/buttonRight"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="0"
                android:text=">" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/optionLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

        </LinearLayout>
    </LinearLayout>

</RelativeLayout>

← 1-4. Android で OpenGL その2
↑ 一覧