Androidプログラミング初心者のためのゲームアプリ開発入門

47,559 views

Published on

「Androidプログラミング初心者のためのゲームアプリ開発」
コピペで気が付けばゲームアプリが作れるようになる(かも)。

サンプルコードはコチラ↓
http://mizmon21.jp/news/archives/7
https://github.com/mizmon21 (工事中)

Published in: Technology
0 Comments
20 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
47,559
On SlideShare
0
From Embeds
0
Number of Embeds
4,126
Actions
Shares
0
Downloads
0
Comments
0
Likes
20
Embeds 0
No embeds

No notes for slide

Androidプログラミング初心者のためのゲームアプリ開発入門

  1. 1. 第17回Android勉強会in札幌Androidプログラミング初心者のための ゲームアプリ開発入門 2012年11月17日 みずもん @mizmon21 水田 雅彦 1
  2. 2. 目次1. MIZMON21とは2. はじめに3. 開発4. 仕上げ5. 完成6. まとめ 2
  3. 3. MIZMON21とは 3
  4. 4. 自己紹介氏名 水田雅彦HN みずもん(英語ではmizmon21)、MMQ大臣誕生日 5月18日おうし座 A型出身地 北海道旭川市職暦 1991年4月~ 某重電機メーカー勤務 FAコンピューター等の開発・FAE業務に従事 2001年4月~ 某電子機器メーカー勤務 LSI製品の開発・FAE業務に従事保有資格 自動車運転免許普通1種、第四級アマチュア無線技士、 1級電気工事施工管理技士、第3種電気主任技術者、 情報処理技術者第2種、初級システムアドミニストレータ、 JSTQBテスト技術者資格認定Foundation Level出願特許 20件モットー 仕事も遊びも全力全開(壊)!好きなもの クルマ、野球、アニメ、萌え系など 4
  5. 5. Android的な活動Mizmon21のアンドロイドなwebhttp://mizmon21.jp/ 今日の資料やアプリ(apk)もここに保管Googleプレイにて数本のAndroidアプリを公開中https://market.android.com/developer?pub=mizmon21アンドロイダー公認デベロッパhttp://androider.jp/developer/4180c245837998ccd98f12d1147032e1/ 5
  6. 6. 中の人について 6
  7. 7. はじめに 7
  8. 8. 本題 Androidでオリジナルな ゲームを作ろう! 8
  9. 9. こんな症状の方に• とりあえずEclipseをインストールしてみたもの の何を作っていいか迷ってる人• よくわからないけどアプリを自作したい人• なかなか重たい腰があげられない人• 飽きっぽい人 そういう自分はまず @override でつまづいたwとにかく一本アプリをつくってみて自信をつける まずはそこからはじまるのです 9
  10. 10. 目的 飽きずに最後まで作りきる• 楽しみながら開発を進めていく• 難しい話はまずは置いといて• コピペでまずは動かしてみる• Androidの文法とかはGoogle先生に聞く 10
  11. 11. どんなゲームを作る? 最終的な完成の姿を想像力を 働かせて妄想する• ストレスの多い日常を解消したい• 2Dアクションゲーム• 「もぐらたたき」的な何かをつくってみる【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch ( みずもんタッチ ) 11
  12. 12. 開発フロー 要求確認 要件定義 外部設計 詳細設計 実装 テストウオーターフォール開発は 忍耐が必要 リリース 12
  13. 13. コツコツ積み上げる 設計要件 実装 画像を動かす 時間制限 検査 タッチを検出する 音をいれる 当たり判定 バランス調整 スコアをつける リリース 反復開発でアプリの 成長を楽しむ 13
  14. 14. 開発 14
  15. 15. ゲームの基本(SurfaceviewとThread)【概要】 ゲームの基本であるSurfaceviewとThreadの導入【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch1 ( 01MizmonTouch )【作成クラス】 MizmonTouch:Activityの派生クラス GameView:Surfaceviewの派生クラス SurfaceHolder.Callbackインターフェース GameViewThead:Threadの派生クラス 15
  16. 16. ゲームの基本(高速描画) ダブルバッファリング View Surfaceview ・高速描画に不向き ・高速描画可能 ・onDraw()で描画 ・定期的な再描画可 ↑ 能 Invalidateで呼び出し ・独立した描画 ・数fps程度 ・数十fps程度• View アプリケーション内で描画 ⇒ 高速描画に不向き 次の画像の準備• Surfaceview アプリケーションのスレッドと描画のスレッドが独立 ⇒ 高速で定期的な描画が容易 Surfaceviewの活用 16
  17. 17. ゲームの基本(定期的な動作) イベントドリブン(イベント駆動型) 外部から何かしらのイベントを発生時に実行 例)タッチ、ボタン押下など アクションゲームなアプリには不向き タッチ処理() { …… } 定期定期に処理させたい ※ タッチされることに より実行される スレッド(処理の実行単位) イベント入力せずとも何かしら処理をさせることができる(ループ処理) ゲームアプリに最適 スレッド処理() { ……. } ※継続して実行 17
  18. 18. ゲームの基本(コード解説)public class MizmonTouch01 extends Activity { // ************** SurfaceHolder.Callbackの3兄弟 ********************** @Override private onCreate(Bundle public GameViewThread extends Thread {mThread;// スレッドのインスタンス class void GameViewThreadsavedInstanceState) { // surface生成時にコールバックされる メインループ動作フラグ(外部アクセス) … private boolean bRunning= false;// @Override ///*********************** 外部から呼ばれるメソッド ***********************/ GameViewを画面としてセットする public void surfaceCreated(SurfaceHolder holder) { GameView gameView = new GameView(this); public GameViewThread(SurfaceHolder surfaceHolder) { mThread = new GameViewThread(holder); setContentView(gameView); this.mHolder = surfaceHolder; // スレッド生成しインスタンス化 } } mThread.enableRunning(true); // スレッド内のメインループ動作を許可する} mThread.start(); // メインループの動作許可設定 // スレッドを起動する(try~catchで囲む) } public void enableRunning(boolean flag) { // surface変更時にコールバックされるclass GameView extends SurfaceView implements SurfaceHolder.Callback { this.bRunning = flag; // メインループ動作許可 @Override public GameView(Context context) { } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { … /*********************** メインループ ***********************/ // なにもしない //サーフェイスホルダーを取得(SurfaceView) // スレッドを起動すると呼ばれる } SurfaceHolder holder = getHolder(); @Override コールバックを設定(SurfaceHolder) ////public void run() { surface破棄時にコールバックされる @Override holder.addCallback(this); { while(bRunning) } public void surfaceDestroyed(SurfaceHolder holder) { … // canvasにテキストを描画する mThread.enableRunning(false); … // スレッド内のメインループ動作を停止する // ループが一定時間の間隔で回るための処理 mThread.join(); //スリープ … // スレッドを停止させる(try~catchで囲む) } } } } 18 }
  19. 19. キャラクターを動かす【概要】 1体のキャラクター(敵)を固定位置で動かす もぐら叩きのモグラの用に下からせりあがってくる感じ【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch2 ( 02MizmonTouch )【修正メソッド】 GameViewThreadクラス内 GameViewThread():初期化する変数を追加【新規メソッド】 GameViewThreadクラス内 moveEnemy() : 敵の動作処理 genVirtualDisplay() : 仮想画面を生成 doDraw() : bmpを画面(View)に貼り付け main() : メイン処理(Threadのループから呼ばれる) 19
  20. 20. キャラクターを動かす(仮想画面の導入) ① キャラクターの元画像を時間に応じサイズの上部から切り 出す ② 仮想画面への貼り付けは画像サイズの底辺を基準に①画 像を張り付ける ③ 時間とともに①の切り出しサイズを変化させる キャラクターの元画像 仮想画面 (rctOriginalCurrentSize) (rctCurrentArea)(left, top) 時間とともに 変化させる (left, top) (right, bottom) (right, bottom) 20
  21. 21. キャラクターを動かす(コード解説)/*********************** 外部から呼ばれるメソッド ***********************/// コンストラクタ final intENEMY_COUNT_MAX = 10;// 敵の最大動作回数 private staticpublic// ***** 画面関係の処理 ***** surfaceHolder, Context context) { GameViewThread(SurfaceHolder private void moveEnemy() { … 仮想画面(ビットマップ)を生成する // if(0 >= nCount) { private void genVirtualDisplay() { // nAddition = +1; // リソースのインスタンスを取得 Resources r = context.getResources(); 引っ込みきったので折り返して出現させる // 仮想画面をCreate= new Canvas(imgVdGame); <= nCount) { } else if(0 <canvas Canvas nAddition && ENEMY_COUNT_MAX // 仮想画面の下地を生成 nAddition = -1; Paint paint =new Paint(); // 最大サイズまで出現した場合、増分をマイナスにする imgVdGame = Bitmap.createBitmap(VD_WIDTH, VD_HEIGHT, Bitmap.Config.ARGB_8888); // Paintをインスタンス化 // 表示する敵画像をインスタンス化 } else { paint.setAntiAlias(true); // 特に何もしない imgEnemy = BitmapFactory.decodeResource(r, 255)); paint.setColor(Color.argb(255, 255, 255, R.drawable.enemy1); ; 敵画像サイズを取得 // } canvas.drawColor(Color.argb(255, 0, 0, 32)); // 背景を塗りつぶし rctEnemyOriginalSize = new Rect(0, // rctOriginalCurrentSize, rctCurrentArea, paint); nCount += nAddition; 0, imgEnemy.getWidth(), imgEnemy.getHeight()); canvas.drawBitmap(imgEnemy, nCountを加算する // 敵の貼り付け} } // オリジナルサイズの敵画像のサイズを更新 // ***** 画面関係の処理 *****/*********************** 内部処理 ***********************/ // nCountから高さを算出 // bmpを画面に貼り付け 敵の処理 (int)(rctEnemyOriginalSize.bottom * nCount / ENEMY_COUNT_MAX);// *****int height doDraw(Canvas canvas) { private void =*****// 敵の動作 rctOriginalCurrentSize = new Rect(rctEnemyOriginalSize.left, rctEnemyOriginalSize.top, Paint paint=new Paint(); // Paintをインスタンス化public Rect paint.setAntiAlias(true); // オリジナルサイズの敵画像の現在のnCountに応じたサイズ rctOriginalCurrentSize; rctEnemyOriginalSize.right, height); // 仮想画面上の現在の占有座標public Rect paint.setColor(Color.argb(255, 255, 255, 255)); rctCurrentArea;public int 現在の敵の座標を更新 // (0~MAX) nCount = 1; // canvas.drawColor(Color.argb(255, 0, 0, 32)); // 背景を塗りつぶしpublic int まずはnCountから高さを算出 nAddition = +1; // nCountの増分 // canvas.drawBitmap(imgVdGame, 0, 0, paint); // 生成したbmp(仮想画面)を画面に表示 } height = (int)(rctEnemyOriginalSize.bottom * nCount / ENEMY_COUNT_MAX); 今回は仮想画面をそのまま // ***** メイン処理new Rect(rctEnemyOriginalSize.left, rctEnemyOriginalSize.bottom - height, rctCurrentArea = ***** // Thread内ループから定期的に呼ばれる(50msごと) Viewに表示してる rctEnemyOriginalSize.right, rctEnemyOriginalSize.bottom); } private void main() { moveEnemy(); // 敵情報更新 genVirtualDisplay(); // 仮想画面生成 } 21
  22. 22. ランダムに表示する【概要】 キャラクターの管理クラスを作成し一つのオブジェクトとして管理【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch3 ( 03MizmonTouch )【修正メソッド】 GameViewクラス内 surfaceChanged() : 実画面サイズをThreadへ通知 GameViewThreadクラス内 GameViewThread() : 初期化する変数を追加 genVirtualDisplay() : dispEnemy()を呼び出しを追加 moveEnemy() : キャラクターの生成と動作更新【新規メソッド】 EnemyInfoクラス 一体のキャラクターの管理クラス管理する GameViewThreadクラス内 setSurfaceSize() : 実画面サイズの取得 genEnemy() : 敵の生成 updateEnemy() : 敵の更新 dispEnemy() : 敵の表示(genVirtualDisplay()のサブ) doDraw() : 仮想画面を実画面へフィッティング 22
  23. 23. ランダムに表示する(オブジェクト化) ランダムな大きさでランダム な位置に表示 複数の変数で管理するのは大変! 1体のキャラクターを管理するクラ ス(構造体)を定義しひとつのオブ ジェクトとして管理 (EnemyInfoクラス) 23
  24. 24. ランダムに表示する(フィッティング) 仮想画面 実画面 フィッティング フィッティング解像度やサイズの違いによる機種依存性を極力排除 24
  25. 25. ランダムに表示する(コード解説)// 敵1体の情報をまとめるクラス class GameViewThread extends Thread { 画面サイズの取得 // // 敵の更新(敵が消滅したらfalseを返す)class EnemyInfo { // コンストラクタ 敵の生成 public// 敵の貼り付け private boolean bAlive; width, int height)// 生死判別フラグ void setSurfaceSize(int public boolean updateEnemy(EnemyInfo enemy){ { public GameViewThread(SurfaceHolder surfaceHolder, Context context) { private EnemyInfo genEnemy() { canvas) { synchronizeddispEnemy(Canvas private void (mHolder) { rctOriginalCurrentSize; // オリジナルサイズの敵のnCountに応じたサイズ publicif(0 >= enemy.nWaitCount) { Rect // 動作カウントの更新なのかを判断する : // enemy.nWaitCount = enemy.nSpeed; //// 生死の初期化(新規生成なのでtrue) EnemyInfo rv = new EnemyInfo(); 仮想画面と実画面の比率を算出// 仮想画面上の敵のサイズ public Rect Paintをインスタンス化 // rctEnemySize; ウェイトの初期化 仮想画面の座標を記録 rv.bAlive = true; Paint(); //float mult_width = (float)VD_WIDTH / (float)width; Paint paint =new public Rect if(0 >= enemy.nCount) { rctOccupationArea; // 仮想画面上の最大占有座標 // 動作カウントの更新 敵のサイズを決定 rctVd =mult_height = (float)VD_HEIGHT / (float)height; //float new Rect(0, 0, VD_WIDTH, VD_HEIGHT); paint.setAntiAlias(true); public Rect rctCurrentArea; return false; // 仮想画面上の現在の占有座標 // 引っ込みきったのでインスタンスを削除 int敵情報の初期化(int)(Math.random()* enemy_width = //float mult; // 動作速度(1動作あたりのスレッドサイクル数。 public int } else if(0 < enemy.nAddition &&(ENEMY_COUNT_MAX <= enemy.nCount || nSpeed; Enemy = new EnemyInfo(); - ENEMY_WIDTH_MIN) + ENEMY_WIDTH_MIN); (ENEMY_WIDTH_MAX // 実画面のサイズに引き延ばす辺(幅または高さ)を判断する // 敵の貼り付け // 0:ウェイトなし、MAX:MAXサイクルで1動作) 1 > (Math.random() * ENEMY_COUNT_MAX))) { } int enemy_heightmult_height){ if(mult_width < = (int)enemy_width// ウェイト挿入回数(初期値はnSpeed値 * rctEnemyOriginalSize.bottom / rctEnemyOriginalSize.right; public int paint.setColor(Color.argb(Enemy.nAlpha, 255, 255, 255)); nWaitCount; enemy.nAddition = -1; // 最大サイズまで出現した場合または int enemy_x = (int)(Math.random()*(VD_WIDTH - enemy_width)); mult = mult_height; canvas.drawBitmap(imgEnemy, Enemy.rctOriginalCurrentSize, Enemy.rctCurrentArea, paint); // 1サイクルごとに-1される。0になったら1コマ動かす) // ランダムで折り返す場合、 増分をマイナスにする } int: else{ { nCount; // ***** 敵の処理 ***** int enemy_y = (int)(Math.random()*(VD_HEIGHT - enemy_height)); } } else public // bAlive=true, bAppearance=true:出現時のカウント rv.rctEnemySize = new Rect(0, 0, enemy_width, enemy_height); mult = mult_width; // 敵の動作 // 敵のサイズを設定 ; // bAive=true, bAppearance=false: 引っ込む時のカウント // 特に何もしない ***** 仮想画面関係の処理 // // bmpを画面に貼り付け= ***** rv.rctOccupationArea } } private 敵の画面内の占有座標を設定 // void moveEnemy() { // bAlive=false : やれたとき(Hit時)のカウント void doDraw(Canvas canvas) { enemy_x + enemy_width,敵情報の更新 // 仮想画面(ビットマップ)を生成する new Rect(enemy_x, enemy_y, private 仮想画面を拡大して張り付ける実画面上の座標を算出する public int// enemy.nCount += enemy.nAddition; nCountの増分 nAddition; // // nCountを加算する // enemy_y + enemy_height); x2=(int)((float)VD_WIDTH / mult); 表示時の透過率 // オリジナルサイズの敵画像座標の初期化 private void genVirtualDisplay() { rv.rctOriginalCurrentSize = public intint// オリジナルサイズの敵画像のサイズを更新、nCountから高さを算出 // Paintをインスタンス化 nAlpha; // if(!updateEnemy(Enemy)){ 仮想画面の下地を生成 // 敵が消滅したら敵を生成する //int y2=(int)((float)VD_HEIGHT / mult); rctEnemyOriginalSize.top, rctEnemyOriginalSize.right, 0); new Rect(rctEnemyOriginalSize.left, Paint paint=new Paint();} enemy.rctOriginalCurrentSize.bottom = Canvas canvas = new Canvas(imgVdGame); / (float)2); // 現在の敵の大きさを設定 rv.rctCurrentArea = Enemy = genEnemy(); int x1=(int)((float)width / (float)2 - (float)x2 enemy.nCount / ENEMY_COUNT_MAX); paint.setAntiAlias(true); (int)(rctEnemyOriginalSize.bottom * 背景を塗りつぶし } //int y1=(int)((float)height / (float)2+- enemy_height, enemy_x + enemy_width, enemy_y + enemy_height); new Rect(enemy_x, enemy_y 255, 255));class GameView extends SurfaceView implements (float)y2 / (float)2); paint.setColor(Color.argb(255, 255, SurfaceHolder.Callback { // 現在の敵の座標を更新、nCountから高さを算出 // 敵の動作速度を設定 canvas.drawColor(Color.argb(255, 0, 0, 32)); } : x2int height = (int)(enemy.rctEnemySize.bottom * enemy.nCount / ENEMY_COUNT_MAX); += x1; 敵の貼り付け rv.nAddition = +1; //y2 背景を塗りつぶし // += y1; : @Override enemy.rctCurrentArea.top = enemy.rctOccupationArea.bottom - height; dispEnemy(canvas); rv.nSpeed = (int)(Math.random()*(ENEMY_SPEED_MAX + 1)); else 実画面の座標を記録 // { canvas.drawColor(Color.argb(255, 0, 0, 32)); public}void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } rv.nWaitCountRect(x1, y1, x2, y2); rctRd = new = rv.nSpeed; // まだウェイトが必要な場合の処理、ウェイトカウントを減らす mThread.setSurfaceSize(width, height); // 画面サイズをスレッドに通知する enemy.nWaitCount --; } } } rv.nCount = 1; // 生成したbmp(仮想画面)を画面に表示} bDisplayReady = true; // 敵の透過率を設定 rctRd, paint); rv.nAlpha = 255; // 画面準備完了 canvas.drawBitmap(imgVdGame, rctVd, return true; } } } return rv; } 25
  26. 26. 複数表示する【概要】 複数のキャラクターの管理 背景もついでに表示【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch4 ( 04MizmonTouch )【修正メソッド】 GameViewThreadクラス内 GameViewThread() : 初期化する変数を追加 moveEnemy() : 複数のキャラクターに対応 genEnemy() : 既存のキャラクターと 重ならない処理を追加 genVirtualDisplay() : 背景画像の表示を追加 26
  27. 27. 複数表示する(リスト化) 複数のキャラクターを管理する • オブジェクト化したキャラクター をリスト化 listEnemys = new ArrayList<EnemyInfo>(); • 複数のキャラクターを生成、更 新および表示処理を追加 全リストの処理を常に行う(for文) • 新規キャラクターは重ならないよ うに生成 rctOccupationArea変数 • 背景も表示(おまけ)キャラクターの生成は排他的に 27
  28. 28. 複数表示する(コード解説)// コンストラクタ private void moveEnemy() {public GameViewThread(SurfaceHolder surfaceHolder, Context context) { 敵の生成 //// 全ての敵を更新する this.mHolder = surfaceHolder; private EnemyInfo genEnemy()0<=i for(int i=listEnemys.size()-1 ; { // リソースのインスタンスを取得 ; i--){ : // 敵情報の更新 Resources r = context.getResources(); 敵の画面内の占有座標を設定 //if(!updateEnemy(listEnemys.get(i))){ :listEnemys.remove(i); // 敵が消滅したらリストからも削除 // 各画像をインスタンス化 //}既に出現中の敵と重なっていないか判定する imgVdGame = Bitmap.createBitmap(VD_WIDTH, VD_HEIGHT, Bitmap.Config.ARGB_8888); } for(EnemyInfo item : listEnemys) { imgBg = BitmapFactory.decodeResource(r, R.drawable.bg); if(Rect.intersects(rv.rctOccupationArea, item.rctOccupationArea)){ imgEnemy = BitmapFactory.decodeResource(r, R.drawable.enemy1); // 新規に敵を追加可能か判断する // 重なっていれば敵を生成しない return null; if(ENEMY_NUMBER_MAX > listEnemys.size()) { // 背景画像のオリジナルサイズを取得 } rctBgSize = 追加可能なら新規に出現するか判断する // new Rect(0, 0, imgBg.getWidth(), imgBg.getHeight()); } if(ENEMY_GEN_PROBABILITY > Math.random()*100) { // オリジナルサイズの敵画像座標の初期化 // 新規に敵を生成 // 敵画像のオリジナルサイズを取得 :// 敵生成に失敗の際は5回までリトライする rctEnemyOriginalSize = new Rect(0, 0, imgEnemy.getWidth(), imgEnemy.getHeight()); } for(int i=0 ; 5>i ; i++) { : EnemyInfo new_enemy = genEnemy(); // 仮想画面の座標を記録 private void genVirtualDisplay() { if(null != new_enemy) { rctVd = new Rect(0, 0, VD_WIDTH, VD_HEIGHT); : listEnemys.add(new_enemy); // 敵生成に成功したらリストに追加する // Paintをインスタンス化 break; // 敵情報の初期化 Paint paint =new Paint(); } listEnemys = new ArrayList<EnemyInfo>(); paint.setAntiAlias(true); }} //}仮想画面に背景画像を貼り付ける } canvas.drawBitmap(imgBg, rctBgSize, rctVd, paint); } : } 28
  29. 29. タッチを検出する【概要】 タッチイベントを解析してアクションと座標を検出【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch5 ( 05MizmonTouch ) (MizmonTouch1から作成)【追加メソッド】 GameViewクラス内 onTouchEvent() : タッチイベント処理 GameViewThreadクラス内 doTouchEvent() : タッチの検出処理 29
  30. 30. タッチを検出する(座標検出)• タッチの検出 – ActivityクラスのonTouchEvent()をオーバーライド – 引数としてMotionEventが渡される – Surfaceviewでタッチイベントを取得する コンストラクタで下記を宣言することにより可能 setFocusable(true); // フォーカスを受け取る• アクションと座標の解析 (0,0) MotionEventには必要な情報が詰まっている – アクションの取得 getAction():ダウン、アップ、移動アクション – 座標の取得 (X, Y) getX():画面上のX座標 X getY():画面上のY座標 他にもいろいろ便利なメソッドが備わっている 30
  31. 31. タッチを検出する(コード解説)class GameView extends SurfaceView implements SurfaceHolder.Callback { class GameViewThread extends Thread { private GameViewThread mThread; // スレッドのインスタンス : : @Override // ***** タッチ入力処理 ***** public GameView(Context context) { // タッチ処理 run() { public void : super(context); private HashMap<String,PointF> points = new HashMap<String,PointF>(); //サーフェイスホルダーを取得(SurfaceView) while(bRunning) { private String strMotion; : SurfaceHolder holder = getHolder(); public boolean doTouchEvent(MotionEvent event) { // コールバックを設定(SurfaceHolder) int action = event.getAction(); try { int count =:event.getPointerCount(); holder.addCallback(this); int index =// タッチ座標を画面に表示 (action & MotionEvent.ACTION_POINTER_ID_MASK) >> // キーイベントが取得できるようにフォーカスを受け取れるようにしておく MotionEvent.ACTION_POINTER_ID_SHIFT; setFocusable(true); paint.setColor(Color.MAGENTA); // タッチ動作を判別 } Object[] keys=points.keySet().toArray(); switch (action & MotionEvent.ACTION_MASK) { // タッチイベントを実装 case MotionEvent.ACTION_DOWN: @Override for (int i=0;i<keys.length;i++) { points.put("" + event.getPointerId(index), new PointF(event.getX(), event.getY())); public boolean onTouchEvent(MotionEvent event) { PointF pos=(PointF)points.get(keys[i]); strMotion = "ACTION_DOWN"; try { break; canvas.drawText(strMotion+"="+(int)pos.x+", "+(int)pos.y, 0, 40*j,paint); j++; : // 実処理はスレッドの中で行う } case MotionEvent.ACTION_MOVE: mThread.doTouchEvent(event); } (Exception e) { i=0;i<count;i++) { for (int } catch } catch(Exception e) { points.get("" + event.getPointerId(i)).x = event.getX(i); } } finally { points.get("" + event.getPointerId(i)).y = event.getY(i); : return true; } } } strMotion = "ACTION_MOVE"; } : break;} } return true; } 31
  32. 32. タッチを仮想画面に反映【概要】 タッチ座標を仮想画面座標に変換【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch6 ( 06MizmonTouch ) (MizmonTouch5から作成) (MizmonTouch3の関数を利用)【追加メソッド】 GameViewThreadクラス内 genVirtualDisplay():タッチ座標にアイコン表示 以下はMizmonTouch3から setSurfaceSize() : 実画面サイズの取得 doDraw() : 仮想画面を実画面へフィッティング main() : メイン処理 32
  33. 33. タッチを仮想画面に反映(座標変換) 実画面 仮想画面 (X´, Y´) 座標変換 (X, Y) fMult倍実画面上の座標→仮想画面上の座標に変換 X´ = fMult × X Y´ = fMult × Y 33
  34. 34. タッチを仮想画面に反映(コード解説)private void genVirtualDisplay() { synchronized (mHolder) { // 仮想画面の下地を生成 Canvas canvas = new Canvas(imgVdGame); 排他処理のおまじない // Paintをインスタンス化 Paint paint =new Paint(); 次の処理は別スレッドで動作 paint.setAntiAlias(true); • タッチ処理 paint.setColor(Color.argb(255, 255, 255, 255)); • 表示処理 // 背景を塗りつぶし canvas.drawColor(Color.argb(255, 32, 32, 32)); そのため処理中に値が変化す // アイコンの貼り付け Object[] keys=points.keySet().toArray(); ると都合が悪いためロックする for (int i=0 ; i<keys.length ; i++) { 例) のタイミングで座標で // 座標の取得 PointF pos=(PointF)points.get(keys[i]); タッチ座標がかわるなど // 座標の変換 よって、doTouchEvent()も int x = (int)((pos.x - rctRd.left) * fMult); int y = (int)((pos.y - rctRd.top) * fMult); Synchronizedでロックしている // アイコンの貼り付け canvas.drawBitmap(imgIcon, x - imgIcon.getWidth()/2, y - imgIcon.getHeight()/2, paint); // 座標も表示してみる paint.setTextSize(24); paint.setColor(Color.CYAN); canvas.drawText(x + ", " + y, 0, 40 * i + 200, paint); } }} 34
  35. 35. 当たり判定【概要】 タッチ座標を検出しキャラクターの有無で当たり判定【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch7 ( 07MizmonTouch ) (MizmonTouch4と6から作成)【修正メソッド】 doTouchEvent() … Downのみを検出対象【追加メソッド】 rotateImage() : BMPを指定の角度にする dispTouch() : ハンマー表示(genVirtualDisplay()) judgeHit() : 当たり判定(main()から呼ばれる) 35
  36. 36. 当たり判定(ダウンアクションだけを検出) 仮想画面 • タッチ時はハンマーをアニメーション化 画像はコンストラクタで回転して生成 • タッチ検出 コンセプトはもぐら叩き ⇒ダウンアクションのみ使用 rctCurrentArea マルチタッチは使用しないrctOccupationArea • 当たり判定 実際の表示エリアとタッチエリアで判定 rctCurrentArea - rctTouch 図の例では○が当たり判定となる Hit • Hit検出後キャラクターを消去 36
  37. 37. 当たり判定(コード解説)public GameViewThread(SurfaceHolder surfaceHolder, Context context) { // タッチ位置の画像貼り付け : 各画像をインスタンス化 private void dispTouch(Canvas canvas){ // // 敵の当たり判定 imgHammer1 = BitmapFactory.decodeResource(r, R.drawable.hammer); // Paintをインスタンス化 private void judgeHit() { // 回転したハンマー画像を生成 Paint paint =new Paint(); synchronized (mHolder) { imgHammer2if(JUDGEMENT_TIME -2 <= nJudgementCount && null != rctTouch) { = rotateImage(imgHammer1, 330); paint.setAntiAlias(true); imgHammer3 = rotateImage(imgHammer1, 300); for(int i=listEnemys.size()-1 ; 0<=i ; i--){ // タッチ位置にハンマーの貼り付け : EnemyInfo enemy = listEnemys.get(i);} if(null != rctTouch) { // 当たり判定 paint.setColor(Color.argb(255, 255, 255, 255)); if(Rect.intersects(rctTouch, enemy.rctCurrentArea)){ // 時間とともに回転させる // 重なっていれば敵を消滅し、リストからも削除// ビットマップ回転させる if(JUDGEMENT_TIME - listEnemys.remove(i); { 1 <= nJudgementCount)private Bitmap rotateImage(Bitmap bmp, float angle) { rctHammerSize, rctTouch, paint); canvas.drawBitmap(imgHammer1, } int w = bmp.getWidth(); } else if(JUDGEMENT_TIME -2 == nJudgementCount) { } int h = bmp.getHeight(); canvas.drawBitmap(imgHammer2, rctHammerSize, rctTouch, paint); // 当たり判定時間をデクリメント Matrix matrix ={ new Matrix(); } else nJudgementCount --; matrix.postRotate(angle, (float)w / 2f, (float)h{/ 2f); } else if(0 <= nJudgementCount) // 画像を回転させるおまじない canvas.drawBitmap(imgHammer3, rctHammerSize, rctTouch, paint); Bitmap dmy_img=Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); } // 当たり判定時間をデクリメント Canvas dmy_canvasnJudgementCount --; } = new Canvas(dmy_img); } dmy_canvas.drawBitmap(bmp, 0, 0, null); } else { return Bitmap.createBitmap(dmy_img, 0, 0, w, h, matrix, true); // 当たり判定時間が超過したのでタッチ情報を削除} rctTouch = null; } } } 37
  38. 38. 当たり処理【概要】 当たり検出後のキャラクタのアクションを実装【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch8 ( 08MizmonTouch ) (MizmonTouch7から作成)【追加メソッド】 updateEnemy() : 長くなったので二つに分ける updateEnemyAlive() : 通常の更新 updateEnemyGone() : あたり判定後の更新 judgeHit() : 当たり判定時の処理をremoveから 生死フラグの処理に変更 dispEnemy() :敵の生死判別しそれに 合わせた画像を貼り付ける 38
  39. 39. 当たり処理(アクション) ドロン Hit! Hit検出後消えるだけじゃ寂しい Hit検出時に当たりアクションを追加 キャラクター別画像へ差し替え 39
  40. 40. 当たり処理(コード解説)// 敵の更新(敵が消滅したらfalseを返す) // 敵の通常更新(敵が消滅したらfalseを返す)private boolean updateEnemy(EnemyInfo enemy){ // やられた敵の更新(敵が消滅したらfalseを返す) private boolean updateEnemyAlive(EnemyInfo enemy) { booleanvoid updateEnemyGone(EnemyInfo enemy) { private rv = judgeHit() { // boolean true; private動作カウントの更新 private void dispEnemy(Canvas canvas) { { if(ENEMY_OUT_COUNT_MAX <= enemy.nCount) 動作カウントの更新がどうかをウェイトから判断する synchronized (mHolder) { // if(0 >= enemy.nCount) { // 敵やれれ処理のカウントが最大時にインスタンスを削除 // return false; //引っ込みきったのでインスンスを削除 Paintをインスタンス化 -2 <= nJudgementCount && null != rctTouch) { if(JUDGEMENT_TIME if(0 >= enemy.nWaitCount) { } //// returnpaint =newi=listEnemys.size()-1 ; 0<=i ; i--){ Paint false; ウェイトの初期化Paint(); nCountを加算する for(int } else if(0 < enemy.nAddition && (ENEMY_COUNT_MAX <= enemy.nCount || 1 > (Math.random() * paint.setAntiAlias(true); enemy.nCount += enemy.nAddition; enemy = listEnemys.get(i); EnemyInfo enemy.nWaitCount = enemy.nSpeed; ENEMY_COUNT_MAX))) 当たり判定 if(ENEMY_COUNT_MAX// { enemy.nCount){ >= 最大サイズまで出現した場合またはランダムで折り返す場合 // // 敵が既定の最大サイズになるまでの処理 敵の生死を判断 判定するのはbAlive=trueの敵のみ 敵の貼り付け// //増分をマイナスにする // // // オリジナルサイズの敵画像のサイズを更新 if(enemy.bAlive) {item : listEnemys)&& Rect.intersects(rctTouch, enemy.rctCurrentArea)){ enemy.nAdditionif(enemy.bAlive { for(EnemyInfo = -1; // nCountから高さを算出 // 重なっていれば生死フラグの処理をして消滅(やられ)処理を始める } else { enemy.rctOriginalCurrentSize.bottom = (int)(rctEnemyOriginalSize.bottom * enemy.nCount / ENEMY_COUNT_MAX); //paint.setColor(Color.argb(item.nAlpha, 255, 255, 255)); 敵が生きている場合の更新処理 // 特に何もしない 現在の敵の座標を更新 ; // // 敵の生死を確認しその状態に合わせた画像を貼り付ける enemy.bAlive = false; } rv = updateEnemyAlive(enemy); // nCountから高さを算出 enemy.nAddition = +1; // 引っ込んでる最中でも引き戻す int if(item.bAlive){ } else { height = (int)(enemy.rctEnemySize.bottom * enemy.nCount / ENEMY_COUNT_MAX); enemy.nSpeed = 0; // 動作速度を最速にセットする // 敵がやられている場合の更新処理 item.rctOriginalCurrentSize, item.rctCurrentArea, paint); canvas.drawBitmap(imgEnemy, enemy.rctCurrentArea.top = enemy.rctOccupationArea.bottom - height; // nCountを加算する listEnemys.set(i, enemy); } else { rv}= +={enemy.nAddition; else enemy.nCountupdateEnemyGone(enemy); // 敵やられ処理時で既定の最大サイズを超えた後の消去処理 } canvas.drawBitmap(imgEnemyOut, item.rctOriginalCurrentSize, item.rctCurrentArea, paint); } // まずは敵画像のカレントサイズをオリジナルサイズにする // オリジナルサイズの敵画像のサイズを更新 } } } else { enemy.rctOriginalCurrentSize = rctEnemyOriginalSize; // nCountから高さを算出 // 当たり判定時間をデクリメント } // 敵の座標を更新 // まだウェイトが必要な場合の処理 enemy.rctOriginalCurrentSize.bottom = (int)(rctEnemyOriginalSize.bottom * enemy.nCount / nJudgementCount --; // nCountから高さを算出 } // ウェイトカウントを減らす ENEMY_COUNT_MAX); int}heightif(0 <= nJudgementCount) { else = (int)(enemy.rctEnemySize.bottom * enemy.nCount / ENEMY_COUNT_MAX); enemy.nWaitCount --; // 当たり判定時間をデクリメント enemy.rctCurrentArea.top = enemy.rctOccupationArea.bottom - height; } // 現在の敵の座標を更新 // nCountから幅を算出 nJudgementCount --; // nCountから高さを算出 int width = (int)((enemy.rctEnemySize.right * enemy.nCount / ENEMY_COUNT_MAX) - enemy.rctEnemySize.right); int height}= (int)(enemy.rctEnemySize.bottom * enemy.nCount / ENEMY_COUNT_MAX); else { enemy.rctCurrentArea.left = enemy.rctOccupationArea.left - (int)(width / 2); // 当たり判定時間が超過したのでタッチ情報を削除 return rv;enemy.rctCurrentArea.right = enemy.rctOccupationArea.right + (int)(width / 2); enemy.rctCurrentArea.top = enemy.rctOccupationArea.bottom - height;} // 透過率を更新(だんだん薄くしていく) rctTouch = null; return true;enemy.nAlpha = 255 - 255 * (enemy.nCount - ENEMY_COUNT_MAX) / (ENEMY_OUT_COUNT_MAX - ENEMY_COUNT_MAX); } } } } return true; } } 40
  41. 41. 得点をつける【概要】 累積得点(スコア)を実装【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch9 ( 09MizmonTouch ) (MizmonTouch8から作成)【追加メソッド】 EnemyInfoクラス : 持ち点変数を追加 genEnemy() : 持ち点登録を追加 updateEnemyAlive() : 持ち点の減算部を追加 judgeHit() :当たり判定でHitした場合に 持ち点をスコアに加算をする genVirtualDisplay() :下記を呼び出し dispGetPoint 獲得得点の表示 dispGameInfo スコアを仮想画面に表示 dispTextテキストを表示 41
  42. 42. 得点をつける(アクション) Score Score 256 スコア 272 16pts. 獲得ポイント ドロン 当たり!• キャラクター管理クラスに各キャラクター固有の持ち点を 保持する変数をEnemyInfoクラスに追加• キャラクター生成時に持ち点を算出しオブジェクトに登録• 該当の敵が1動作するたびに持ち点を減算する (とりあえず半分づつ減らす。但し、1より下回らない)• 当たり判定で獲得ポイントの表示とスコアへの加算 42
  43. 43. 得点をつける(コード解説)// 敵1体の情報をまとめるクラス // 敵の通常更新(敵が消滅したらfalseを返す)class EnemyInfo { // 倒した敵の獲得得点を表示する private boolean updateEnemyAlive(EnemyInfo enemy) { // テキストを表示する : private void dispGetPoint(Canvas canvas) { :private void dispText(Canvas canvas, String text, int x, int y, Typeface type, int alpha, int size, int align) { public int nAlpha; // 表示時の透過率 // 倒した敵の獲得得点を表示する // Paintをインスタンス化 // 持ち点の更新nPoint; public int // 持ち点 for(EnemyInfo=new :Paint(); Paint paint item listEnemys) { if(1 < enemy.nPoint) {} enemy.nPoint /= 2; ENEMY_COUNT_MAX < item.nCount) { if(!item.bAlive && paint.setAntiAlias(true); } paint.setTextSize(size); String pts = item.nPoint + "pts."; // サイズの設定 paint.setTypeface(type); // フォントを設定 returnint widthint(int)(paint.measureText(text)); // 描画幅を抽出 true; = x = (int)(VD_WIDTH / 2); } int y = (int)(VD_HEIGHT / 2 - (item.nCount) * 10);class GameViewThread extends Thread { private voidif(DISP_RIGHT == align) { pts, x, y, Typeface.DEFAULT_BOLD, item.nAlpha, DISP_PTS_SIZE, : dispText(canvas, judgeHit() { DISP_CENTER); // 敵の生成 // (mHolder) { synchronized 右寄せの場合の座標変換 } x -= private EnemyInfo (int)(width + {2); nJudgementCount && null != rctTouch) { if(JUDGEMENT_TIME -2 <= genEnemy() } } else if(DISP_CENTER == align) { ; 0<=i ; i--){ for(int i=listEnemys.size()-1 : } // センターの場合の座標変換 EnemyInfo enemy = listEnemys.get(i); // 持ち点を設定 x -= (int)(width / 2 + 1);&& Rect.intersects(rctTouch, enemy.rctCurrentArea)){ if(enemy.bAlive rv.nPoint { 100 * (ENEMY_SPEED_MAX - rv.nSpeed + 1) * } else = // スコア等ゲーム情報を表示する (10 - (int)(10 左寄せは何もしない : ; // * (enemy_width canvas) { テキストを表示 - ENEMY_WIDTH_MIN) / (ENEMY_WIDTH_MAX - // 得点を加算する private void dispGameInfo(Canvas ENEMY_WIDTH_MIN))); } // スコアタイトルを表示する += enemy.nPoint; nScore // 影の描画} String title = mContext.getString(R.string.txt_score); return rv;}== alpha) { if(255 キャラクターの大きさ、スピードにより 影付き dispText(canvas, title, 0, DIAP_TITLE_POS_Y, Typeface.DEFAULT, 255, DISP_TITLE_SIZE, DISP_LEFT); } // alphaが255のときのみ表示 // スコアを表示する : paint.setColor(Color.argb(alpha, 0, 0, 0)); 持ち点を算出する : } String score = "" + nScore; canvas.drawText(text, x + 2, y + 2, paint); } dispText(canvas, score, 0, DIAP_INFO_POS_Y, Typeface.DEFAULT_BOLD, 255, DISP_INFO_SIZE, DISP_LEFT); } } } // 本体の描画 paint.setColor(Color.argb(alpha, 255, 255, 255)); canvas.drawText(text, x, y, paint); } 43
  44. 44. スタート画面を作る【概要】 ゲーム全体の構成(状態管理)の整理【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch10 ( 10MizmonTouch ) (MizmonTouch9から作成)【追加・修正】 game.xml : 基本レイアウトを記述 MizmonTouch10クラス OnCreate() : game.xmlをviewに設定 GameViewクラス GameView () : レイアウトxmlから呼び出されるとき、AttributeSet引数を追加する setButtonInstance : レイアウトxml上のボタンインスタンスをセットする setTextViewInstancce : レイアウトxml上のテキストインスタンスをセットする gameStart() : activity上のボタンを押されたら呼び出される genThread() : 新設。スレッドのインスタンス化が長くなりそうなので surfaceCreated() : Threadのインスタンス化の代わりにgenThreadメソッドを呼び出し GameViewThreadクラス GameViewThread() : Message用の引数を追加 setStart() : ゲームステートをセットする doTouchEvent doStart() : ゲームスタート処理 setState() : ゲームステートをセットする GameViewへのメッセージを生成して送り付ける genVirtualDisplay() : ゲームステート毎の表示設定 dispReady() : ゲーム開始前のカウントダウンを表示 main() : ゲームステート応じた処理に変更 controlState() : ステートの更新確認 ゲーム開始前のカウントダウンもここでやる 44
  45. 45. スタート画面を作る(ステート管理) View GameViewクラス GameViewThreadクラス 画面全体の制御をする ゲーム自体の制御 ボタンが押された 通知(setStart) テキスト menu ボタン 状態遷移 play readyテキストやボタンは game.xmlで定義 handleMessage テキストやボタンを非表示 テキストやボタン (sendMessage) Ready画面 の制御 Play画面 Menu:ボタンを表示 GameViewThread → GameViewへは Ready:カウントダウン message Handlerを使用する Play:ゲーム実行中いきなりゲームスタートも心の準備が… 状態(ステート) メニュー画面やスタート画面の追加 の管理 45
  46. 46. スタート画面を作る(コード解説)<?xml version="1.0" encoding="utf-8"?> publicGameView extends SurfaceView implements SurfaceHolder.Callback { class GameViewThread extendsextends Activity { class MizmonTouch10<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" @Override class // ***** メイン処理 android:layout_width="fill_parent" Thread { /*********************** 内部処理 ***********************/ private 仮想画面関係の処理 ***** // Buttonのインスタンス btStart; ***** システム ***** mHandler; // TextViewのインスタンス //private Handler //// private TextView ***** ***** game.xml GameView mGameView; // ハンドラ // GameViewのインスタンス surfaceCreated(SurfaceHolder holder) { private Button public void 仮想画面(ビットマップ)を生成する> @Overridemain() { tvText; android:layout_height="fill_parent" // システムのステートのセット // スレッド生成 // ゲーム全体のステート(状態遷移) private void ゲームのスタート処理 == context, AttributeSet attrs) {setState(EnumState state)genThread(holder); //private void genVirtualDisplay() { publicif(EnumState.play eCurrentState) private ゲーム中だけの処理 GameView(Context public void onCreate(Bundle savedInstanceState) { <jp.mizmon21.android.mizmontouch10.GameView enum EnumState{{ { // void { private void doStart() (mHolder) { synchronized synchronized (mHolder) { menu, 当たり判定 super(context, attrs); // android:id="@+id/gameview" // 各変数の初期化 : : ここでGameViewを // メニュー画面 : : ready,judgeHit(); int bt_visibility = View.INVISIBLE; } switch(eCurrentState) { android:layout_width="fill_parent" // game.xmlを画面としてセットする // ゲーム開始カウントダウン } nScore = 0; 敵情報更新 // case menu: play,moveEnemy(); // 特に何もしない(game.xmlを表示) // ゲーム動作中 : 貼り付けてる int tv_visibility = View.INVISIBLE; android:layout_height="fill_parent" /> nCountSub = CYCLE_COUNT; setContentView(R.layout.game); // ゲームスレッドの生成 break; } nCount = READY_COUNT_DOWN; <RelativeLayout String tv_text = ""; } // ステートをセットする case ready: private void genThread(SurfaceHolder holder) { // GameViewのインスタンスを取得 // ゲーム開始前のカウントダウンを表示する private仮想画面生成 eCurrentState = EnumState.menu;// // EnumState android:layout_width="fill_parent" dispReady(canvas); それぞれのステート動作判断はこの中で行う 現在のゲームのステート // メソッドのインスタンス化 genVirtualDisplay(); // eCurrentState = state; mGameView = (GameView)findViewById(R.id.gameview); /*********************** 外部から呼ばれるメソッド ***********************/ android:layout_height="fill_parent" break; // 敵情報の初期化GameViewThread(holder, mContext, View // ステートの更新判定 switch(eCurrentState){ //listEnemys = new ArrayList<EnemyInfo>();敵の貼り付け new Handler() { コンストラクタ new play: mThread = case android:gravity="center_horizontal"> // gameview // メニュー表示 controlState(); @Override case menu: (FramELayout) dispEnemy(canvas); public GameViewThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { } // // 画面ボタンのインスタンス取得と動作処理の登録 bt_visibility = View.VISIBLE; ゲームステートの変更タッチ位置にアイコンの貼り付け public void handleMessage(Message message) { this.mHandler = handler;// // ハンドラーのインスタンス化 (GameView) // ステートの更新 <Button // メッセージハンドラの生成(Threadからのメッセージをここで受け取る) setState(EnumState.ready); Button bt_start = dispTouch(canvas); (Button)findViewById(R.id.button1); tv_visibility = View.VISIBLE; : } privateandroid:id="@+id/button1“ void controlState() { // 倒した敵の獲得得点を表示する { ここはこれまでと同じ (Relative Layout) bt_start.setOnClickListener(new View.OnClickListener() tv_text = mContext.getString(R.string.tv_menu); // ゲームステートの変更 switch(eCurrentState) {btStart.setVisibility(message.getData().getInt("bt_visibility")); : dispGetPoint(canvas); break; tvText.setVisibility(message.getData().getInt("tv_visibility")); // 特に何もしない(ボタン入力のため) case menu: onClick(View menuに状態を遷移にさせる android:text="@string/bt_game“// v) { public void // スコアの表示をする setState(EnumState.menu); game.xml上の } } // dispGameInfo(canvas); // カウンタの更新 case ready: tvText.setText(message.getData().getString("tv_text")); break; : case ready:ボタンがクリックされた時に呼び出されます case play: textView1 // ゲーム開始カウントダウン // ゲーム動作中 // スタートボタンが押されたときの処理。ゲームをスタートさせる break; mGameView.gameStart(); android:visibility="visible" /> }); nCountSub --; default: ボタンの処理 default: public void setStart(){ >= nCountSub) { } } doStart(); if(0 break; // readyに状態を遷移させる break; button1 <TextView nCountSub = CYCLE_COUNT;} public void}setButtonInstance(Button button_start) { } } }); nCount --; android:id="@+id/textView1" // レイアウト上のbuttonのインスタンス化 btStart = button_start; > nCount) { // メッセージを生成しGameViewに送り付ける //:テキストのインスタンスを取得 } } // タッチ処理 if(0 Message msg = mHandler.obtainMessage(); // カウントダウンがゼロになったらゲームスタート public boolean doTouchEvent(MotionEvent event) { TextView tv_text = (TextView)findViewById(R.id.textView1); publicandroid:text=""(mHolder) { setState(EnumState.play); bundle = new Bundle(); void setTextViewInstancce(TextView text_view) {Bundle // ゲーム開始前のカウントダウンを表示する synchronized //:XMLの各パーツのインスタンスをGameViewに教えてやる tvText = text_view; } bundle.putInt("bt_visibility", bt_visibility); if(EnumState.play == { // レイアウト上のtextviewをインスタンス化 private void dispReady(Canvas canvas)eCurrentState) { // 状態がplayのときのみタッチを有効にする } mGameView.setButtonInstance(bt_start); bundle.putInt("tv_visibility", tv_visibility); android:visibility="visible" /> } // カウントダウンを表示する : break; bundle.putString("tv_text", tv_text); // ゲーム開始ボタンが押された時の処理 String count = (0 == nCount) ? mContext.getString(R.string.txt_start) : "" + nCount; mGameView.setTextViewInstancce(tv_text); case play: } // とりあえず無限動作のため何もしない int x = (int)((VD_WIDTH / 2)); </RelativeLayout> msg.setData(bundle); } public void (int)(VD_HEIGHT / 2); gameStart() { int } = default: y break; mThread.setStart();</FrameLayout>return true; mHandler.sendMessage(msg); // GameViewThreadにスタートを通知 } int size = (int)(96 * (float)(1f - (nCountSub - 1f) / CYCLE_COUNT)); break; } } } dispText(canvas, count, x, y, Typeface.DEFAULT_BOLD, 255, size, DISP_CENTER); } } 46 }}
  47. 47. 終了処理【概要】 時間制限を設けて終了処理を実装【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch11 ( 11MizmonTouch ) (MizmonTouch10から作成)【追加メソッド】 GameViewThreadクラス内 setState() : ゲームオーバーの処理を追加 ganVirtualDisplay() : 各ステートの処理を追加 dispGameInfo() : 残り時間の表示を追加 dispFinish() : 終了時の演出を追加 main() : ゲームプレイ中の残り時間を更新 controlState() : ゲーム中の残り時間の更新 終了時演出のカウンタを更新 47
  48. 48. 終了処理(ゲームオーバー)無限に続き終わらない… 30秒一本勝負にする• ゲーム残り時間処理を実装 状態遷移図• ゲームオーバー時の表示を実装 menu ボタン押下• ステートを追加 タッチ game – finish:ゲーム終了時演出 over ready – gameover:タッチ検出待ち カウントダウン 演出終了 finish play 30秒経過 48
  49. 49. 終了処理(コード解説)// ゲーム全体のステート(状態遷移)enum 仮想画面(ビットマップ)を生成する // EnumState { // スコア等ゲーム情報を表示する private void genVirtualDisplay() { menu, // メニュー画面 private void メイン処理 ***** ready, ***** dispGameInfo(Canvas canvas) { // synchronized (mHolder) { private void main() // ゲーム開始カウントダウン : { play, // 残り時間タイトルを表示する // ゲーム動作中 if(EnumState.play == eCurrentState) { switch(eCurrentState) { // 当たり判定 // ゲーム終了 finish, String titlejudgeHit(); = mContext.getString(R.string.txt_time); gameover,case menu: // ゲームオーバー } // 特に何もしない dispText(canvas, title, VD_WIDTH, DIAP_TITLE_POS_Y, Typeface.DEFAULT, 255, DISP_TITLE_SIZE, DISP_RIGHT);} break; // 残り時間を表示する eCurrentState || EnumState.finish == eCurrentState || EnumState.gameover == eCurrentState) { if(EnumState.play == case ready: // ゲーム開始前のカウントダウンを表示する moveEnemy(); // 敵情報更新 String time = String.format("%.1f", nGameTime); // 小数点以下1位まで表示する// システムのステートのセット : } dispText(canvas, time, VD_WIDTH, DIAP_INFO_POS_Y, Typeface.DEFAULT_BOLD, 255, DISP_INFO_SIZE, DISP_RIGHT); : break;private void setState(EnumState state) { } case play: synchronized (mHolder) { // スコアタイトルを表示する : : ステートの更新 // switch(eCurrentState){ : break; } private void controlState() { casecase menu: finish: // メニュー表示 switch(eCurrentState) { // 敵の貼り付け : : dispEnemy(canvas); // ゲームオーバーのロゴを表示する casebreak; finish: // スコアの表示をする case ready: カウンタの更新 // // ゲーム開始カウントダウン private void dispFinish(Canvas canvas) { dispGameInfo(canvas); case play: nCount ++; // ゲーム動作中 // ゲームオーバーメッセージを表示する // ゲーム終了のロゴを表示する // ゲーム終了 case finish: if(TIME_GAME_OVER < nCount) { String text dispFinish(canvas); = mContext.getString(R.string.txt_finish); break; // カウンターが規定値に達したらメニューへ戻る int x = (int)((VD_WIDTH /setState(EnumState.gameover); case gameover: 2)); break; // ゲームオーバー int case gameover: y = (int)(VD_HEIGHT / 2);View.INVISIBLE; } bt_visibility = // 敵の貼り付け - (float)nCount break; int size = (int)(56 * (float)(1fView.VISIBLE; / CYCLE_COUNT)) + 40; tv_visibility = casetv_text mContext.getString(R.string.tv_gameover); gameover: dispEnemy(canvas); size = (40 > size) ? 40 :=size; // タッチ検出確認 // スコアの表示をする break; dispText(canvas, text, x, y, Typeface.DEFAULT_BOLD, 255, size, DISP_CENTER); } default: if(null != rctTouch) dispGameInfo(canvas); { break; break; // タッチが検出されたらMenuへ戻る setState(EnumState.menu); } default: } break; break; // メッセージを生成しGameViewに送り付ける : } default: } } break;} } } } 49
  50. 50. 効果音や音楽を鳴らす【概要】 効果音やBGMを鳴らす仕組みを実装【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch12 ( 12MizmonTouch ) (MizmonTouch11から作成)【修正・追加メソッド】 GameViewThread() : 効果音関係諸々をインスタンス化 setState() : 効果音を鳴らす依頼を追加 judgeHit() : 効果音を鳴らす依頼を追加 playSound() : 音を鳴らすかどうか判断する startBgm() : BGM再生開始 stopBgm() : BGM再生停止【音源】 「 On-Jin ~音人~」さん からお借りしました http://www.yen-soft.com/ssse/ 50
  51. 51. 効果音や音楽を鳴らす 音を鳴らす方法 soundPool と mediaPlayersoundPool mediaPlayer 単発音に向いている 尺の長い音に向いている (繰り返しが得意) (繰り返しが苦手) 効果音 効果音 • スタート音 • BGM • アタック音 • 空振りの音 • タイムアップの音 51
  52. 52. 効果音や音楽を鳴らす(コード解説)// 効果音関係 // システムのステートのセットprivate static final int SOUND_START= 0; // スタート // 敵の当たり判定 SOUND_FINISH= 1; private void setState(EnumState state) {private static final int // 終了 private void効果音関係 *****/ /***** int judgeHit() SOUND_HIT = 2; synchronized (mHolder) {private static final { // 敵あたり(当たった音) // final int : (mHolder) { synchronized SOUND_MISS = 3;private static SoundPoolを利用して効果音を鳴らす // 敵はずれ(空振りの音) switch(eCurrentState){ -2 <==nJudgementCount && null != rctTouch) { if(JUDGEMENT_TIME private void playSound() {private static final int[][] SOUND_GROUP { // 各サウンドプレイヤーで鳴らす効果音の定義 // 効果音出力許可されているか判断する ; i--){ for(int i=listEnemys.size()-1 ; 0<=i : {SOUND_START, SOUND_FINISH}, // サウンドプレイヤー0で出力するグループ if(bEnableSound) EnemyInfo enemy = listEnemys.get(i); { {SOUND_HIT, ready:// 当たり判定。判定するのはbAlive=trueの敵のみ case SOUND_MISS} // サウンドプレイヤー毎に鳴らすべき効果音があるかチェック // ゲーム開始カウントダウン // サウンドプレイヤー1で出力するグループ}; for(int i = 0 if(enemy.bAlive && Rect.intersects(rctTouch, スタート時の効果音を鳴らす bPlaySound[SOUND_START] = true; { ; i < SOUND_GROUP.length ; i++) // enemy.rctCurrentArea)){private boolean break; bEnableSound; : // 効果音出力許可フラグ // サウンドプレーヤーグループの各効果音で鳴らす依頼があるかチェックprivate MediaPlayer case play: < SOUND_GROUP[i].length ; j++)// ゲーム動作中 for(int j = 0 ; 効果音を鳴らす メディアプレイヤーのインスタンス(BGM用) mediaPlayer //j= null; // {private SoundPool soundPool 対象の効果音のIDを取り出す startBgm();bPlaySound[SOUND_HIT] = true; // BGM再生開始 // = null; // サウンドプールのインスタンス(効果音用)private int[] 敵あたり判定効果音をならしたよフラグを立てる break; int // = new = SOUND_GROUP[i][j]; nSoundIds sound_id int[4]; // それぞれの効果音のIDprivate boolean[] case finish: // 対象の効果音について鳴らす依頼があるか判断 bPlayHitSound = true; bPlaySound = new boolean[4]; // ゲーム終了 // 効果音を鳴らす依頼フラグ(Trueで鳴らす)private int[] } if(bPlaySound[sound_id]) { nLastSoundId = new int[2]; = true; bPlaySound[SOUND_FINISH] // 終了時の効果音を鳴らす // 最後に鳴らした効果音(サウンドプレイヤー2個分) } // 現在鳴らしている効果音を停止private boolean stopBgm(); bPlayHitSound; --; // 敵あたり効果音を鳴らしたよフラグ // BGM再生終了 // 当たり判定時間をデクリメント nJudgementCountsoundPool.stop(nLastSoundId[i]);/*********************** 外部から呼ばれるメソッド ***********************/ break; } else if(JUDGEMENT_TIME -新たな効果音を鳴らす { // 3 == nJudgementCount)// コンストラクタ : if(!bPlayHitSound) nLastSoundId[i] = soundPool.play(nSoundIds[sound_id], 1.0f, 1.0f, 1, 0, 1); { default: // 効果音を鳴らすpublic GameViewThread(SurfaceHolder surfaceHolder, Context context, Handler handler)第1引数:鳴らす音のID // 依頼フラグを下げる { : break; bPlaySound[SOUND_MISS] = true;= false; bPlaySound[sound_id] 第1引数:プールする最大の数(サウンドプレイヤーの数) 第2引数、第3引数:左右の音量(0.0~1.0) // 各効果音をインスタンス化 } } } else { 第2引数:Streamのタイプ(通常はSTREAM_MUSIC) 第4引数:プライオリティ(0が一番優先度が高い) bEnableSound = true; } // 敵をやっつけたフラグを下げる : 第3引数:クオリティ(デフォルトは0) 第5引数:ループ回数(-1:無限ループ、0:単発) bPlayHitSound = false; soundPool = new}SoundPool(2, AudioManager.STREAM_MUSIC, 0); } } 第6引数:再生速度(0.5~2.0倍) } } nSoundIds[SOUND_START] = soundPool.load(mContext, R.raw.start, 1); // スタート(プレイヤー0) } nJudgementCount --; // 当たり判定時間をデクリメント // BGMを停止する nSoundIds[SOUND_FINISH] = soundPool.load(mContext, R.raw.finish, 1); // 終了(プレイヤー0) } else if(0 <= nJudgementCount) { // BGMを再生開始する private void stopBgm() { nSoundIds[SOUND_HIT] = soundPool.load(mContext, R.raw.pico, 1); nJudgementCount --; private void startBgm() { // 敵あたり(プレイヤー1) // 当たり判定時間をデクリメント != mediaPlayer) { if (null nSoundIds[SOUND_MISS] = soundPool.load(mContext, R.raw.hazure, 1); // 効果音出力許可されているか判断する } else { // 敵はずれ(プレイヤー1) mediaPlayer.stop(); : if(bEnableSound) { = null; rctTouch // 当たり判定時間が超過したのでタッチ情報を削除 mediaPlayer.reset();} mediaPlayer = MediaPlayer.create(mContext, R.raw.bgm); mediaPlayer.release(); } mediaPlayer.start(); mediaPlayer = null; } mediaPlayer.setVolume(1.0f, 1.0f); } } } } 52 }
  53. 53. 仕上げ 53
  54. 54. 最終チェック とにかくやりこんで 気が付いたところを列挙する• 改善点 – 途中でやめても音が止まらない – 音量のバランス• 全体的な使い勝手 – 音なしモードが欲しい – タイトルロゴの挿入• ゲームバランスの調整 – 画像 – 敵の出現パターン – 難易度 54
  55. 55. 改善点• 途中でやめても音が終わらない – ゲーム終了処理を追加 – トップクラスにonPause()を追加し停止処理させる – GameViewクラスに停止処理を行うgameExit()を追加 – GameViewThreadクラスにsetExit(),releaseSound()を追加• 音量のバランス – 当たり時の音大きい 各効果音やBGMに – はずれ時の音聞こえない static値を設定した 55
  56. 56. 全体的な使い勝手• 音なしモードが欲しい – menuに重ねて2個ボタンを追加 「サウンド・オン」と「サウンド・オフ」を排他で表示 – ボタンの制御はbEnableSoundフラグの状態で切 り替える – bEnableSoundフラグで効果音とBGMをオン・オフ する• タイトルロゴの挿入 – タイトルロゴを表示するdispTitle()を作成 – genVirtualDisplay()からmenu状態時に呼び出し 56
  57. 57. ゲームバランスの調整• 画像 – やっぱり画像キモイ 「クリップアートファクトリー」さん から画像をお借りしました http://www.printout.jp/clipart/index.html – 画像は差し替えるだけでOK 同時表示数 サイズ• 敵の出現パターン – 開始直後はパラパラと敵が出現 – 時間経過とともに同時表示数を増加• 難易度 時間 – 開始直後のサイズはなるべく大きく表示 – 出現確立を高めに設定(時間経過でmaxを増やすため) 57
  58. 58. 完成!【概要】 ゲーム性が向上【サンプルプロジェクト名(アプリケーション名)】 MizmonTouch13 ( 13MizmonTouch ) (MizmonTouch12から作成) 58
  59. 59. まとめ 59
  60. 60. 余力があれば細かな調整や演出を加える• ポーズ機能の実装• 空振りしたらペナルティとか• 連続コンボでボーナス得点獲得とか• ステージ制を導入(Win, Lost)• ハイスコアを登録する• 世界ランキング機能もいれてみるなど 60
  61. 61. 終わりに• 環境の充実 パソコン黎明期では考えられないほど簡単にアプリ が作れる ⇒プラットフォームもイロイロ• デザインはやっぱり重要 グラフィックを違うだけでまったく雰囲気が変わる ⇒ゲームは見た目が大事• ヒットゲームはアイデアで勝負• Googleプレイでアプリ公開 ⇒そして日々のアプリDL数をみてニヤニヤする 61
  62. 62. 面白いアプリを開発して世界の海原に漕ぎ出そう! -以 上- 62

×