Android 帶你玩轉實現游戲2048 其實2048只是個普通的控件
轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/40020137,本文出自:【張鴻洋的博客】
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~~~
---------------------------------------------------------------------------------------------------------
我建了一個QQ群,方便大家交流。群號:55032675
----------------------------------------------------------------------------------------------------------
博主部分視頻已經上線,如果你不喜歡枯燥的文本,請猛戳(初錄,期待您的支持):
來自: http://blog.csdn.net//lmj623565791/article/details/40020137