Android 帶你玩轉實現游戲2048 其實2048只是個普通的控件
原文出處: 【張鴻洋的博客】
1、概述
博主本想踏入游戲開放行業,無奈水太深,不會游泳;于是乎,只能繼續開發應用,但是原生Android也能開發游戲么,2048、像素鳥、別踩什么來著;今天給大家帶來一篇2048的開發篇,別怕不分上下文,或者1、2、3、4,一篇包你能玩happy~雖然我從來沒有玩到過2048!!!其實大家也可以當作自定義控件來看~~~
特別說明一下,游戲2048里面的方塊各種顏色來源于:http://download.csdn.net/detail/qq1121674367/7155467,這個2048的代碼中,其他代碼,太多,未參考;特此感謝分享;大家也可以下載下,對比學習下;
接下來貼個我們項目的效果圖:
ok 看完效果圖,我就準備帶領大家征服這款游戲了~~~
2、實現分析
貼一張靜態圖,開始對我們游戲的設計:
可以看到,游戲其實就是一個容器,里面很多個方塊,觸摸容器,里面的方塊的形態會發生變化。那么:
1、容器我們準備自定義ViewGroup ,叫做Game2048Layout ; 里面的塊塊自定義View ,叫做Game2048Item
接下來從簡單的開始:
2、Game2048Item
Game2048Item是個View,并且需要哪些屬性呢?
首先得有個number,顯示數字嘛,然后繪制的時候根據number繪制背景色;還需要呢?嗯,需要正方形邊長,再考慮下,這個邊長應該Item自己控制么?顯然不是的,Game2048Layout 是個n*n的面板,這個n是不確定的,所以Item的邊長肯定是Game2048Layout 計算好傳入的。這樣必須的屬性就這兩個。
3、Game2048Layout
Game2048Layout是個容器,我們觀察下,里面View是個 n*n的排列,我們準備讓其繼承RelativeLayout ; 這樣可以通過設置Item的RIGHT_OF之類的屬性進行定位;
我們在onMeasure里面得到Layout的寬和高,然后根據n*n,生成一定數目的Item,為其設置寬和高,放置到Layout中,這樣整個游戲的布局就做好了;繪制的細節上:Item間有橫向與縱向的間距,所以需要設置這個值,叫做mMargin。然后Item的邊長 = ( Layout邊長 – (n-1)*mMagin ) / n ;
剩下的就是onTouchEvent里面去判斷用戶手勢了,然后就行各種邏輯操作了~
3、代碼之旅
首先來看看我們的Game2048Item
1、Game2048Item
package com.zhy.game2048.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * 2048的每個Item * * @author zhy * */ public class Game2048Item extends View { /** * 該View上的數字 */ private int mNumber; private String mNumberVal; private Paint mPaint; /** * 繪制文字的區域 */ private Rect mBound; public Game2048Item(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mPaint = new Paint(); } public Game2048Item(Context context) { this(context, null); } public Game2048Item(Context context, AttributeSet attrs) { this(context, attrs, 0); } public void setNumber(int number) { mNumber = number; mNumberVal = mNumber + ""; mPaint.setTextSize(30.0f); mBound = new Rect(); mPaint.getTextBounds(mNumberVal, 0, mNumberVal.length(), mBound); invalidate(); } public int getNumber() { return mNumber; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); String mBgColor = ""; switch (mNumber) { case 0: mBgColor = "#CCC0B3"; break; case 2: mBgColor = "#EEE4DA"; break; case 4: mBgColor = "#EDE0C8"; break; case 8: mBgColor = "#F2B179";// #F2B179 break; case 16: mBgColor = "#F49563"; break; case 32: mBgColor = "#F5794D"; break; case 64: mBgColor = "#F55D37"; break; case 128: mBgColor = "#EEE863"; break; case 256: mBgColor = "#EDB04D"; break; case 512: mBgColor = "#ECB04D"; break; case 1024: mBgColor = "#EB9437"; break; case 2048: mBgColor = "#EA7821"; break; default: mBgColor = "#EA7821"; break; } mPaint.setColor(Color.parseColor(mBgColor)); mPaint.setStyle(Style.FILL); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); if (mNumber != 0) drawText(canvas); } /** * 繪制文字 * * @param canvas */ private void drawText(Canvas canvas) { mPaint.setColor(Color.BLACK); float x = (getWidth() - mBound.width()) / 2; float y = getHeight() / 2 + mBound.height() / 2; canvas.drawText(mNumberVal, x, y, mPaint); } }
很簡單,基本就一個onDraw通過number來繪制背景和數字;number通過調用setNumer進行設置;它的寬和高都是固定值,所以我們并不需要自己進行測量~~
2、Game2048Layout
1、成員變量
這就是我們最主要的一個類了,首先我們看看這個類的成員變量,先看看各個成員變量的作用:
/** * 設置Item的數量n*n;默認為4 */ private int mColumn = 4; /** * 存放所有的Item */ private Game2048Item[] mGame2048Items; /** * Item橫向與縱向的邊距 */ private int mMargin = 10; /** * 面板的padding */ private int mPadding; /** * 檢測用戶滑動的手勢 */ private GestureDetector mGestureDetector; // 用于確認是否需要生成一個新的值 private boolean isMergeHappen = true; private boolean isMoveHappen = true; /** * 記錄分數 */ private int mScore;
主要的成員變量就這些,直接看注釋也比較容易理解~~
了解了成員變量,接下來我們需要在構造方法里面得到一些值和初始化一些變量
2、構造方法
public Game2048Layout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMargin, getResources().getDisplayMetrics()); // 設置Layout的內邊距,四邊一致,設置為四內邊距中的最小值 mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); mGestureDetector = new GestureDetector(context , new MyGestureDetector()); }
我們在構造方法里面得到Item間的邊距(margin)和我們容器的內邊距(padding,),這個值應該四邊一致,于是我們取四邊的最小值;這兩個屬性可以抽取為自定義的屬性;然后初始化了我們的mGestureDetector
有了margin和padding,我們就可以計算我們item的邊長了。這個計算過程肯定在onMeasure里面,因為我們需要在onMeasure獲取容器的寬和高
3、onMeasure
private boolean once; /** * 測量Layout的寬和高,以及設置Item的寬和高,這里忽略wrap_content 以寬、高之中的最小值繪制正方形 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 獲得正方形的邊長 int length = Math.min(getMeasuredHeight(), getMeasuredWidth()); // 獲得Item的寬度 int childWidth = (length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn; if (!once) { if (mGame2048Items == null) { mGame2048Items = new Game2048Item[mColumn * mColumn]; } // 放置Item for (int i = 0; i < mGame2048Items.length; i++) { Game2048Item item = new Game2048Item(getContext()); mGame2048Items[i] = item; item.setId(i + 1); RelativeLayout.LayoutParams lp = new LayoutParams(childWidth, childWidth); // 設置橫向邊距,不是最后一列 if ((i + 1) % mColumn != 0) { lp.rightMargin = mMargin; } // 如果不是第一列 if (i % mColumn != 0) { lp.addRule(RelativeLayout.RIGHT_OF,// mGame2048Items[i - 1].getId()); } // 如果不是第一行,//設置縱向邊距,非最后一行 if ((i + 1) > mColumn) { lp.topMargin = mMargin; lp.addRule(RelativeLayout.BELOW,// mGame2048Items[i - mColumn].getId()); } addView(item, lp); } generateNum(); } once = true; setMeasuredDimension(length, length); }
首先設置容器的邊長為寬高中的最小值;然后(length – mPadding * 2 – mMargin * (mColumn – 1)) / mColumn ; 去計算Item的邊長;
拿到以后,根據我們的mColumn初始化我們的Item數組,然后遍歷生成Item,設置Item的LayoutParams以及Rule(RIGHT_OF , BELOW),最后添加到我們的容器中;
最后我們通過setMeasuredDimension(length, length);改變我們布局占據的空間;
到此,我們整個面板繪制完成了;
接下來,就是根據用戶的手勢,去進行游戲邏輯操作了,手勢那么肯定是onTouchEvent了:
4、onTouchEvent
@Override public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); return true; }
我們把觸摸事件交給了mGestureDetector,我們去看看我們的mGestureDetector,在構造方法中有這么一句:
mGestureDetector = new GestureDetector(context , new MyGestureDetector());
so,我們需要去看看MyGestureDetector:
class MyGestureDetector extends GestureDetector.SimpleOnGestureListener { final int FLING_MIN_DISTANCE = 50; @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { float x = e2.getX() - e1.getX(); float y = e2.getY() - e1.getY(); if (x > FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) { action(ACTION.RIGHT); } else if (x < -FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) { action(ACTION.LEFT); } else if (y > FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) { action(ACTION.DOWM); } else if (y < -FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) { action(ACTION.UP); } return true; } }
很簡單,就是判讀用戶上、下、左、右滑動;然后去調用action(ACTION)方法;ACTION是個枚舉:
/** * 運動方向的枚舉 * * @author zhy * */ private enum ACTION { LEFT, RIGHT, UP, DOWM }
這么看,核心代碼都在action方法里面了:
5、根據用戶手勢重繪Item
看代碼前,先考慮下,用戶從右向左滑動時,面板應該如何變化;取其中一行,可能性為:
0 0 0 2 -> 2 0 0 0
2 0 4 0 -> 2 4 0 0
2 2 4 0 -> 4 4 0 0
大概就這3中可能;
我們算法是這么做的:
拿2 2 4 0 來說:
1、首先把每行有數字的取出來,臨時存儲下來,即[ 2, 2, 4 ];
2、然后遍歷合并第一個相遇的相同的,即[ 4, 4 ,0 ]
3、然后直接放置到原行,不足補0,即[ 4, 4, 0 ,0 ];
中間還有幾個操作:
1、生成一個新的數字,游戲在每次用戶滑動時,可能會生成一個數字;我們的生成策略:如果發生移動或者合并,則生成一個數字;
移動的判斷,拿原數據,即【 2 ,2,4,0】和我們第一步臨時存儲的做比較,一一對比(遍歷臨時表),發現不同,則認為移動了;
合并的判斷,在合并的時候會設置合并的標志位為true;
2、加分,如果發生合并,則加分,分值為合并得到的數字,比如 4,4 -> 8 ,即加8分 ; 也只需要在合并的時候進行相加就行了;
介紹完了,來看我們的代碼:
/** * 根據用戶運動,整體進行移動合并值等 */ private void action(ACTION action) { // 行|列 for (int i = 0; i < mColumn; i++) { List<Game2048Item> row = new ArrayList<Game2048Item>(); // 行|列 for (int j = 0; j < mColumn; j++) { // 得到下標 int index = getIndexByAction(action, i, j); Game2048Item item = mGame2048Items[index]; // 記錄不為0的數字 if (item.getNumber() != 0) { row.add(item); } } for (int j = 0; j < mColumn && j < row.size(); j++) { int index = getIndexByAction(action, i, j); Game2048Item item = mGame2048Items[index]; if (item.getNumber() != row.get(j).getNumber()) { isMoveHappen = true; } } // 合并相同的 mergeItem(row); // 設置合并后的值 for (int j = 0; j < mColumn; j++) { int index = getIndexByAction(action, i, j); if (row.size() > j) { mGame2048Items[index].setNumber(row.get(j).getNumber()); } else { mGame2048Items[index].setNumber(0); } } } generateNum(); }
大體上是兩層循環,外層循環代碼循環次數,內層有3個for循環;
第一個for循環,對應上述:首先把每行有數字的取出來,臨時存儲下來,即[ 2, 2, 4 ];
第二個for循環,判斷是否發生移動;
// 合并相同的
mergeItem(row); 是去進行合并操作,對應上述:然后遍歷合并第一個相遇的相同的,即[ 4, 4 ,0 ];以及加分和設置合并標志位都在方法中;
第三個for循環:設置合并后的值,對應上述:然后直接放置到原行,不足補0,即[ 4, 4, 0 ,0 ];
最后生成數字,方法內部會進行判斷游戲是否結束,是否需要生成數字;
那么先看mergeItem的代碼:
/** * 合并相同的Item * * @param row */ private void mergeItem(List<Game2048Item> row) { if (row.size() < 2) return; for (int j = 0; j < row.size() - 1; j++) { Game2048Item item1 = row.get(j); Game2048Item item2 = row.get(j + 1); if (item1.getNumber() == item2.getNumber()) { isMergeHappen = true; int val = item1.getNumber() + item2.getNumber(); item1.setNumber(val); // 加分 mScore += val; if (mGame2048Listener != null) { mGame2048Listener.onScoreChange(mScore); } // 向前移動 for (int k = j + 1; k < row.size() - 1; k++) { row.get(k).setNumber(row.get(k + 1).getNumber()); } row.get(row.size() - 1).setNumber(0); return; } } }
也比較簡單,循環查找相同的number,發現合并數字,加分;
加分我們設置了一個回調,把分數回調出去:
if (mGame2048Listener != null) { mGame2048Listener.onScoreChange(mScore); }
最后看我們生成數字的代碼:
/** * 產生一個數字 */ public void generateNum() { if (checkOver()) { Log.e("TAG", "GAME OVER"); if (mGame2048Listener != null) { mGame2048Listener.onGameOver(); } return; } if (!isFull()) { if (isMoveHappen || isMergeHappen) { Random random = new Random(); int next = random.nextInt(16); Game2048Item item = mGame2048Items[next]; while (item.getNumber() != 0) { next = random.nextInt(16); item = mGame2048Items[next]; } item.setNumber(Math.random() > 0.75 ? 4 : 2); isMergeHappen = isMoveHappen = false; } } }
首先判斷是否結束,如果結束了,依然是回調出去,得讓玩的人知道結束了;
然后判斷當然面板是有木有空的格子,如果沒有,在判斷需要生成新的數字么,需要則隨機生成一個新的2或4;
那么如何判斷是否結束呢?
首先肯定是沒有空格了,然后四個方向上沒有相同的數字就結束了:
/** * 檢測當前所有的位置都有數字,且相鄰的沒有相同的數字 * * @return */ private boolean checkOver() { // 檢測是否所有位置都有數字 if (!isFull()) { return false; } for (int i = 0; i < mColumn; i++) { for (int j = 0; j < mColumn; j++) { int index = i * mColumn + j; // 當前的Item Game2048Item item = mGame2048Items[index]; // 右邊 if ((index + 1) % mColumn != 0) { Log.e("TAG", "RIGHT"); // 右邊的Item Game2048Item itemRight = mGame2048Items[index + 1]; if (item.getNumber() == itemRight.getNumber()) return false; } // 下邊 if ((index + mColumn) < mColumn * mColumn) { Log.e("TAG", "DOWN"); Game2048Item itemBottom = mGame2048Items[index + mColumn]; if (item.getNumber() == itemBottom.getNumber()) return false; } // 左邊 if (index % mColumn != 0) { Log.e("TAG", "LEFT"); Game2048Item itemLeft = mGame2048Items[index - 1]; if (itemLeft.getNumber() == item.getNumber()) return false; } // 上邊 if (index + 1 > mColumn) { Log.e("TAG", "UP"); Game2048Item itemTop = mGame2048Items[index - mColumn]; if (item.getNumber() == itemTop.getNumber()) return false; } } } return true; }
/** * 是否填滿數字 * * @return */ private boolean isFull() { // 檢測是否所有位置都有數字 for (int i = 0; i < mGame2048Items.length; i++) { if (mGame2048Items[i].getNumber() == 0) { return false; } } return true; }
到此,我們的代碼介紹完畢~~~完成了我們的Game2048Layout ; 接下來看如何使用呢?
寫游戲的過程很艱辛,但是用起來,看看什么叫so easy ; 當成普通的View即可:
4、實踐
1、布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.zhy.game2048.view.Game2048Layout android:id="@+id/id_game2048" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_centerInParent="true" android:background="#ffffff" android:padding="10dp" > </com.zhy.game2048.view.Game2048Layout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/id_game2048" android:layout_centerHorizontal="true" android:layout_marginBottom="20dp" android:background="#EEE4DA" android:orientation="horizontal" > <TextView android:id="@+id/id_score" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:text="Score: 0" android:textColor="#EA7821" android:textSize="30sp" android:textStyle="bold" /> </LinearLayout> </RelativeLayout>
2、MainActivity
package com.zhy.game2048; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import android.widget.TextView; import com.zhy.game2048.view.Game2048Layout; import com.zhy.game2048.view.Game2048Layout.OnGame2048Listener; public class MainActivity extends Activity implements OnGame2048Listener { private Game2048Layout mGame2048Layout; private TextView mScore; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mScore = (TextView) findViewById(R.id.id_score); mGame2048Layout = (Game2048Layout) findViewById(R.id.id_game2048); mGame2048Layout.setOnGame2048Listener(this); } @Override public void onScoreChange(int score) { mScore.setText("SCORE: " + score); } @Override public void onGameOver() { new AlertDialog.Builder(this).setTitle("GAME OVER") .setMessage("YOU HAVE GOT " + mScore.getText()) .setPositiveButton("RESTART", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mGame2048Layout.restart(); } }).setNegativeButton("EXIT", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }).show(); } }
很簡單,代碼主要就是設置個接口,當發生加分已經游戲結束時會交給Activity去處理~~~如果喜歡,你可以在一個界面放4個游戲~~~
當然了游戲Item的個數也可以動態設置~~~最后貼一個5*5游戲的截圖~~
好了,2048到此結束,拿只筆開始設計,然后根據自定義View的經驗去寫,相信你可以學會不少東西~~~
并且我們的View是抽取出來的,其實換成圖片也很簡單~~
今天又看了war3十大經典戰役,獻上war3版,代碼就不貼了,改動也就幾行代碼,貼個截圖,紀念我們曾經的war3~~~:
額,咋都弄成5*5了~大家可以把mColumn改為4~~~