15パズル
画像などを4×4などに区切り1マスだけ空欄にし、バラバラになった画像を空欄を使ってもとの画像に戻すゲームです。
最初に紹介ずるゲームにこれを選んだ理由は、書籍の最初がこれだったかr・・・えー今までやってきた基本のコードの組み合わせでできるからです。
ゲーム自体は、画像さえ変えたり、分割数を変えたりすれば無限に種類はできますので、お手軽でいいですね。
アプリコード
今回からは、コードを書いてからどういうことをやっているのか順番に簡単な説明をいれていきます。
プロジェクト名「Puzzle」
使用画像 |
||
背景画像 |
パズル画像 |
|
スタートボタン(通常) |
スタートボタン(押下) |
ファイル名「main.xml」 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/and,roid" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <and.roid.PView android:id="@+id/View01" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </FrameLayout> |
||
レイアウトするオブジェクトの修正ですね。<FrameLayout></FrameLayout>に変更し、xmlns:app="http://schemas.android.com/apk/res/and,roid"のようにパッケージパスを追加。 android:id="@+id/View01"でオブジェクトidを設定しました。 |
ファイル名「PuzzleManifest.xml」 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="and.roid" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="4" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:screenOrientation="portrait" android:name=".PuzzleActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |
||
ここでは早速おぼえたての、回転機能を縦で固定にしています。 |
ファイル名「PuzzleActivity.java」 package and.roid; import android.app.Activity; import android.os.Bundle; import android.view.*; public class PuzzleActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.main); } } |
||
このファイルはプロジェクト作成時にすでにあるファイルです。赤字の所でフルスクリーンにするようにしています。このコードはそのままおぼえましょう。 |
ファイル名「PView.ja」このファイルはView クラスをスーパークラスに持たせて作成します。 package and.roid; import android.content.Context; import android.content.res.Resources; import android.graphics.*; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.*; import android.widget.Toast; public class PView extends View { private static final int btn_x = 50; //ボタン横位置(1) private static final int btn_y = 770; //ボタン縦位置 private static final int btn_w = 390; //ボタン横幅 private static final int btn_h = 50; //ボタン縦幅 private static final int board_x = 40; //ゲーム版の横位置 private static final int board_y = 126; //ゲーム版の縦位置 private static final int score_x = 60; //テキストの表示横位置 private static final int score_y = 73; //テキストの表示縦位置 private PuzzleActivity puzzle; //PuzzleActivityクラス(ファイル) private PBoard board; //PBoardクラス(ファイル) private Drawable back,btn1,btn2; //使用するイメージ private boolean btn_down,isPlaying; //ボタンの状態、プレイ中の状態 private int pressX,pressY,upX,upY; //ボタンを押した時、離した時の位置 public PView(Context context) { super(context); init(context); } public PView(Context context, AttributeSet attrs){ super(context,attrs); init(context); } private void init(Context context){ (2) puzzle =(PuzzleActivity)context; Resources resources = context.getResources(); back = resources.getDrawable(R.drawable.back1); btn1 = resources.getDrawable(R.drawable.start1); btn1.setBounds(btn_x,btn_y,btn_x+btn_w,btn_y+btn_h); btn2 = resources.getDrawable(R.drawable.start2); btn2.setBounds(btn_x,btn_y,btn_x+btn_w,btn_y+btn_h); Bitmap img = BitmapFactory.decodeResource(resources, R.drawable.image1); board = new PBoard(board_x,board_y,img); btn_down = false; isPlaying = false; } protected void onDraw(Canvas c){ (3) c.drawColor(Color.BLACK); int w = this.getWidth(); int h = this.getHeight(); back.setBounds(0, 0, w, h); back.draw(c); board.draw(c); if(btn_down){ btn2.draw(c); }else{ btn1.draw(c); } Paint p = new Paint(); p.setTextSize(30f); p.setColor(Color.WHITE); c.drawText("count:"+board.count, score_x, score_y, p); } public boolean onTouchEvent(MotionEvent event){ (4) int action = event.getAction(); int x = (int)event.getX(); int y = (int)event.getY(); switch(action){ case MotionEvent.ACTION_DOWN: pressX = x; pressY = y; if (isIn(x,y,btn1.getBounds())){ btn_down = true; isPlaying = true; board.init(); Toast toast = Toast.makeText(puzzle, "スタート!",Toast.LENGTH_LONG); toast.show(); } break; case MotionEvent.ACTION_UP: btn_down = false; upX = x; upY = y; if(isPlaying) checkMove(); break; } invalidate(); return true; } public boolean isIn(int x, int y, Rect rect){ (5) return x>rect.left && x<rect.right && y>rect.top && y<rect.bottom; } public void checkMove(){ (6) int dx = upX - pressX; int dy = upY - pressY; if(dx < -100) board.move(PBoard.WEST); if(dx > 100) board.move(PBoard.EAST); if(dy < -100) board.move(PBoard.NORTH); if(dy > 100) board.move(PBoard.SOUTH); if(board.checkFinish()){ isPlaying = false; Toast toast = Toast.makeText(puzzle, "完成!", Toast.LENGTH_LONG); toast.show(); } } } |
||
このファイルクラスでは主に全体の画面や初期設定など、アプリの心臓部となるクラスです。 (1) private static final int で 設定した変数に数値を代入すると、その数値の変更ができなくなり、固定値のようになります。 (2) private void init(Context context) の「init」は初期化を意味します。初期化するクラスをここで作り、画像の取り込みや初期位置情報などを設定しています。 (3) protected void onDraw(Canvas c)はいろいろ表示するクラスです。画面全体を黒で塗りつぶし、画面の横幅縦幅を知り、その座標を使い背景画像をセット。のちに作る別ファイルクラスにある表示クラスを呼び出し、ボタン画像を表示。ここでボタンが押されているかいないかで、ボタンの画像を変えて表示。ここからはテキスト表示のためにPaintオブジェクトを生成し、テキストサイズを設定。色を設定して変数countを設定座標に表示しています。 (4) ここはタッチパネルのイベント処理です。MotionEvent eventでタッチした時にアクションイベント値が自動的にeventにはいります。それをint action = event.getAction();で受け取り、次で座標も受け取ります。switchでさきほど受け取ったタッチイベントによって処理を変えます。 (5) public boolean isIn(int x, int y, Rect rectの説明を先に。座標と短形情報をうけとり、渡された座標(今回はタッチ座標)と短形座標(今回はボタン座標)をくらべて、短形エリアでタッチされたかいないか(boolean)をtrue、falseで返します。 MotionEvent.ACTION_DOWN:はタッチした時です。まずタッチした時の座標をpressX、Yに入れます。isInに値を渡し(上記(5)参照)それが真なら(短形エリアでタッチされたら)btn_down = true(押された)、isPlaying = true(スタート開始ok)にし、のちに設定するちがうファイルのボード(動かす写真)クラスの初期化を飛び出し、最後に下部にテキストトーストを表示します。 MotionEvent.ACTION_UP:はタッチしたあとにタッチが離れた時の処理です。ボタン上であればタッチは離れるわけですから元にもどします(btn_down = false)。つまり押した時だけボタン画像を変更させて、あとはfalseの状態にしておくということです。そのご、タッチを離した時の座標をupX、Yに入れ、if(isPlaying) checkMove();でゲーム中なら checkMove()クラスを呼び出しゲーム中でないならそのまま下に移動。 最後に画面を更新(invalidate();)してreturn trueでこのクラスを出ます。 (6) public void checkMove()はタッチを話した時に呼び出されるクラスです。ここではパネルを移動させるための条件が整っていれば、違うファイルのパネルを動かすクラスを呼び出す処理を最初にしています。タッチon、off時にそれぞれ座標を格納しているので、タッチした時の座標からタッチが離れた時の座標を差し引く事で、どれだけタッチしたまま移動して離れたかを出します。その差が100ドット以上の差があり、xならプラス100かマイナス100か、yも同じ感じで調べてそれぞれ違う数値を渡して呼び出しています。。 最後にこの処理が終わった瞬間に、ゲーム終了かどうかのチェックをして、終了ならテキストトーストを表示しています。 |
ファイル名「PBoard.java」 package and.roid; import java.util.*; import android.graphics.*; public class PBoard { public static final int CENTER = 0; //移動方向を示す値 public static final int NORTH = 1; //移動方向を示す値 public static final int SOUTH = 2; //移動方向を示す値 public static final int EAST = 3; //移動方向を示す値 public static final int WEST = 4; //移動方向を示す値 private Bitmap image; //表示イメージ private int x,y; //ボードの表示位置 private int[] data; //表示位置データを管理する配列 public int place; //現在の空きスペース位置 public static final int row = 6; //ピースの縦の個数 public static final int col = 4; //ピースの横の個数 public static final int pW = 100; //ピースの横幅 public static final int pH = 100; //ピースの縦幅 public int count = 0; //動かした回数 public PBoard(int x, int y, Bitmap image){ (1) super(); this.x = x; this.y = y; this.image = image; data = new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,-1}; place = 23; } public void init(){ (2) for(int i=0;ii++){ Random r = new Random(new Date().getTime()); int a = r.nextInt(data.length); int b = r.nextInt(data.length); int val = data[a]; data[a] = data[b]; data[b] = val; } for(int i=0;i<data.length;i++) if(data[i] == -1) place = i; count = 0; } public void draw(Canvas canvas){ (3) int n = 0; for(int i=0;i<row;i++){ for(int j=0;j<col;j++){ int c = data[n] % col; int r = (int)(data[n]/col); if(data[n] != -1){ canvas.drawBitmap(image, new Rect(x+c*pW, y+r*pH, x+c*pW+pW, y+r*pH+pH), new Rect(x+j*pW, y+i*pH, x+j*pW+pW, y+i*pH+pH), new Paint()); } n++; } } } public void move(int move){ (4) int c = place % col; int r = (int)(place / col); int c2 = c; int r2 = r; switch(move){ case NORTH: if(r2 < row-1) r2++; break; case SOUTH: if(r2 > 0) r2--; break; case WEST: if(c2 < col-1) c2++; break; case EAST: if(c2 > 0) c2--; break; } int n = data[r * col + c]; data[r * col + c] = data[r2 * col + c2]; data[r2 * col + c2] = n; for(int i=0;i<data.length;i++) if(data[i] == -1) place = i; count++; } public boolean checkFinish(){ (5) boolean flg = true; for(int i=0;i<data.length-1;i++) if(data[i] != i) flg = false; return flg; } } |
||
このクラスは表示するパネルに関する処理が書かれています。 (1) PBoard(int x, int y, Bitmap image)はこのファイルの主クラスとなります。このクラスを使用する時には、座標とイメージをこのクラスに渡すようにしてます。このクラスの座標メンバxyに渡された座標を入れ、パネルを分割するための準備としてdataの配列を作成し、中にに24個の数値を襦袢に1から入れ、最後24個目を-1(空欄)と設定します。空欄パネル数の場所をplaceに入れます。 (2) public void init() でパネルの初期化処理をしています。簡単にいいますと、ランダムな数値0から24を2つ作り、3つの変数を使い(例えばA=B B=C C=A)としながら入れ替える処理を200回繰り返します。 Random r = new Random(new Date().getTime());はランダム数値を使うための準備です。 int a = r.nextInt(data.length);は下のコードbと同じで、ランダム値をdata.lengthのdataの大きさ、ここでは24で作成しています。そして3つの変数を使用して数値を入れ替えています。 最後にfor文でdataの中にある配列数分だけ中の数値をチェックし、その数値が-1であれば空欄を表すplaceに-1を入れます。そしてcountを0に設定。 (3) public void draw(Canvas canvas) ではパネル表示を画面に表示させる処理をしているクラスです。ここはほとんどが数学の応用を使用してますね。 (4) public void move(int move) ではパネルの移動処理をしています。渡された値によって処理を変えます。最後に空欄を探してplaceに-1を入れます。 (5) public boolean checkFinish() はパネルの絵が元の絵に戻ったかどうかの判定処理です。最初の設定で元の画像が0から22まで順番に並んでいます。for分を成分iも順番に0から増えていく処理をして、元絵の数値と比べておなじゅ数値に並んでいれば完成というわけです。最初のboolean flg = true;は完成したかどうかのフラグで、完成と一度設定しています。そのわけは逆の処理をするよりこちらのほうが簡単な処理ができるからです。(たぶん) |
これで完成ですが、エミュレータを色々試したらわかると思いますがXperiaでしか表示がしっかりしません。 |
||
Xperiaでしかちゃんと表示されないのは問題です。ちょっと改造します。
ファイル名「PuzzleActivity.java」 package and.roid; import android.app.Activity; import android.os.Bundle; import android.view.*; public class PuzzleActivity extends Activity { public float disp_w,disp_h; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); WindowManager manager = window.getWindowManager(); Display disp = manager.getDefaultDisplay(); disp_w = disp.getWidth(); disp_h = disp.getHeight(); setContentView(R.layout.main); } } |
||
ファイル名「PView.java」 package and.roid; import android.content.Context; import android.content.res.Resources; import android.graphics.*; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.*; import android.widget.Toast; public class PView extends View { private static final float XPERIA_W = 480; private static final float XPERIA_H = 854; private float btn_x = 50; //ボタン横位置 private float btn_y = 770; //ボタン縦位置 private float btn_w = 390; //ボタン横幅 private float btn_h = 50; //ボタン縦幅 private float board_x = 40; //ゲーム版の横位置 private float board_y = 126; //ゲーム版の縦位置 private float score_x = 60; //テキストの表示横位置 private float score_y = 73; //テキストの表示縦位置 private PuzzleActivity puzzle; //PuzzleActivityクラス(ファイル) private PBoard board; //PBoardクラス(ファイル) private Drawable back,btn1,btn2; //使用するイメージ private boolean btn_down,isPlaying; //ボタンの状態、プレイ中の状態 private int pressX,pressY,upX,upY; //ボタンを押した時、離した時の位置 public PView(Context context) { super(context); init(context); } public PView(Context context, AttributeSet attrs){ super(context,attrs); init(context); setPuzzleSize(); } private void init(Context context){ puzzle =(PuzzleActivity)context; btn_down = false; isPlaying = false; } private void setPuzzleSize(){ float w = puzzle.disp_w; float h = puzzle.disp_h; float dw = w / XPERIA_W; float dh = h / XPERIA_H; btn_x = btn_x * dw; btn_y = btn_y * dh; btn_w = btn_w * dw; btn_h = btn_h * dh; board_x = board_x * dw; board_y = board_y * dh; score_x = score_x * dw; score_y = score_y * dh; Resources resources = puzzle.getResources(); back = resources.getDrawable(R.drawable.back1); back.setBounds(0, 0, (int)w, (int)h); btn1 = resources.getDrawable(R.drawable.start1); btn1.setBounds((int)btn_x,(int)btn_y,(int)(btn_x+btn_w),(int)(btn_y+btn_h)); btn2 = resources.getDrawable(R.drawable.start2); btn2.setBounds((int)btn_x,(int)btn_y,(int)(btn_x+btn_w),(int)(btn_y+btn_h)); Bitmap img = BitmapFactory.decodeResource(resources, R.drawable.image1); board = new PBoard(board_x,board_y,dw,dh,img); } protected void onDraw(Canvas c){ c.drawColor(Color.BLACK); back.draw(c); board.draw(c); if(btn_down){ btn2.draw(c); }else{ btn1.draw(c); } Paint p = new Paint(); p.setTextSize(30f); p.setColor(Color.WHITE); c.drawText("count:"+board.count, score_x, score_y, p); } public boolean onTouchEvent(MotionEvent event){ int action = event.getAction(); int x = (int)event.getX(); int y = (int)event.getY(); switch(action){ case MotionEvent.ACTION_DOWN: pressX = (int)event.getX(); pressY = (int)event.getY(); if (isIn(x,y,btn1.getBounds())){ btn_down = true; isPlaying = true; board.init(); Toast toast = Toast.makeText(puzzle, "スタート!",Toast.LENGTH_LONG); toast.show(); } break; case MotionEvent.ACTION_UP: btn_down = false; upX = (int)event.getX(); upY = (int)event.getY(); if(isPlaying) checkMove(); break; case MotionEvent.ACTION_MOVE: break; } invalidate(); return true; } public boolean isIn(int x, int y, Rect rect){ return x>rect.left && x<rect.right && y>rect.top && y<rect.bottom; } public void checkMove(){ int dx = upX - pressX; int dy = upY - pressY; if(dx < -100) board.move(PBoard.WEST); if(dx > 100) board.move(PBoard.EAST); if(dy < -100) board.move(PBoard.NORTH); if(dy > 100) board.move(PBoard.SOUTH); if(board.checkFinish()){ isPlaying = false; Toast toast = Toast.makeText(puzzle, "完成!", Toast.LENGTH_LONG); toast.show(); } } }
|
||
ファイル名「PBoard.java」 package and.roid; import java.util.*; import android.graphics.*; public class PBoard { public static final int CENTER = 0; //移動方向を示す値 public static final int NORTH = 1; //移動方向を示す値 public static final int SOUTH = 2; //移動方向を示す値 public static final int EAST = 3; //移動方向を示す値 public static final int WEST = 4; //移動方向を示す値 private Bitmap image; //表示イメージ private float x,y; //ボードの表示位置 private int[] data; //表示位置データを管理する配列 public int place; //現在の空きスペース位置 public static final int row = 6; //ピースの縦の個数 public static final int col = 4; //ピースの横の個数 private float pW = 100f; //ピースの横幅 private float pH = 100f; //ピースの縦幅 public int count = 0; //動かした回数 public PBoard(float x, float y, float dw, float dh, Bitmap image){ super(); this.x = x; this.y = y; pW *= dw; pH *= dh; this.image = image; data = new int[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,-1}; place = 23; } public void init(){ for(int i=0;ii++){ Random r = new Random(new Date().getTime()); int a = r.nextInt(data.length); int b = r.nextInt(data.length); int val = data[a]; data[a] = data[b]; data[b] = val; } for(int i=0;i<data.length;i++){ if(data[i] == -1) place = i; count = 0; } } public void draw(Canvas canvas){ int n = 0; for(int i=0;i<row;i++){ for(int j=0;j<col;j++){ int c = data[n] % col; int r = (int)(data[n]/col); if(data[n] != -1){ canvas.drawBitmap(image, new Rect((int)(x+c*pW), (int)(y+r*pH), (int)(x+c*pW+pW), (int)(y+r*pH+pH)), new Rect((int)(x+j*pW), (int)(y+i*pH), (int)(x+j*pW+pW), (int)(y+i*pH+pH)), new Paint()); } n++; } } } public void move(int move){ int c = place % col; int r = (int)(place / col); int c2 = c; int r2 = r; switch(move){ case NORTH: if(r2 < row-1) r2++; break; case SOUTH: if(r2 > 0) r2--; break; case WEST: if(c2 < col-1) c2++; break; case EAST: if(c2 > 0) c2--; break; } int n = data[r * col + c]; data[r * col + c] = data[r2 * col + c2]; data[r2 * col + c2] = n; for(int i=0;i<data.length;i++) if(data[i] == -1) place = i; count++; } public boolean checkFinish(){ boolean flg = true; for(int i=0;i<data.length-1;i++) if(data[i] != i) flg = false; return flg; } }
|
||
変更場所は赤字にしました。
Xperiaの画面に合わせているので、実際の画面との縮尺を計算するための変更です。
初めて使うメソッドなど出てきても、日本語でゆっくり考えてみると理解しやすいとおもいます。
javaはオブジェクト思考プログラミングということで、処理をクラスごとに細かく分けてパーツを組み合わせる感じです。
最初に作成されるファイルはそれがないと動かないファイルという感じで覚えて、携帯端末機自体のオブジェクトに、ゲームの背景やボタン、テキストを設定、パネルの処理だけ別のオブジェクトとしてクラス作成をする。という感じでしょうか。
オブジェクト思考はパーツごとに細かく分かれていて、ゆっくり調べることができ、見やすいのがいいですよね。