看了别人写的2048游戏代码,感觉蛮简单,自己手动敲了一遍,才发现有点难度啊,不过了解也加深了很多,现把学到的知识写成博客,加深理解。
首先,参考的是大神的微博:http://blog.csdn.net/lmj623565791/article/details/40020137
先上一张最后实现的实际效果图:
游戏的规则:
(1)开始游戏,随机位置,随机生成2或4。
(2)每移动一次:
1.发生合并,不生成数字。
2.没有发生合并,随机生成一个2或4.
(3)如果所有格子都已经填满,那就游戏结束。
实现原理:
(1)自定义一个View(Game2048Item),用来作为格子的View.
(2)自定义另一个View(Game2048Layout),用来放所有的格子。
详细实现的过程:
一、Game2048Item
Game2048Item它的主要功能就是用来显示数字的,所以需要一个number 属性来记录当前格子上显示的数字,为了更加美观,我们可以设置根据不同的数字格子显示不同的背景色。
public class Game2048Item extends View{
private Paint paint; //画笔工具
private int mNumber; //View上的数字。
private String mNumberVal; //View上的数字,String类型
private int fontSize = 50; //保存View上显示的数字的大小
public Game2048Item(Context context) {
this(context,null);
}
public Game2048Item(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public Game2048Item(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
}
//返回当前格子上的数字
public int getNumber(){
return mNumber;
}
//设置格子上的数字
public void setNumber(int mNumber){
this.mNumber = mNumber;
mNumberVal = mNumber + “”;
}
/**
* 根据格子上的数字,绘制不同的背景色
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String mBgColor = "#EA7821"; //保存默认绘制的背景色
switch (mNumber){
case 0: //没有数字时候格子的颜色
mBgColor = "#CCC0B3";
break;
case 2:
mBgColor = "#EEE4DA";
break;
case 4:
mBgColor = "#EDE0C8";
break;
case 8:
mBgColor = "#F2B179";
break;
case 16:
mBgColor = "#F49563";
break;
case 32:
mBgColor = "#F57940";
break;
case 64:
mBgColor = "#F55D37";
break;
case 128:
mBgColor = "#EEE863";
break;
case 256:
mBgColor = "#EDB040";
break;
case 512:
mBgColor = "#ECB040";
break;
case 1024:
mBgColor = "#EB9437";
break;
case 2048:
mBgColor = "EA7821";
break;
default:
mBgColor = "#EA7821";
break;
}
//设置画笔颜色
paint.setColor(Color.parseColor(mBgColor));
//有三种样式:
// Paint.Style.STROKE 描边。
// Paint.Style.FILL 填充。
// Paint.Style.FILL_AND_STROKE 描边并填充
paint.setStyle(Paint.Style.FILL);
/**
* 画一个矩形
* 第一个参数:矩形的左边位置
* 第二个参数:矩形的上边位置
* 第三个参数:矩形的右边位置
* 第四个参数:矩形的下边位置
* 第五个参数:画笔工具
*/
canvas.drawRect(0,0,getWidth(),getHeight(),paint);
}
//设置字体大小。
public void setFontSize(int size){
fontSize = size;
}
}
画好正方形之后,我们需要在上面绘制数字,而绘制数字的位置,我们可以通过Paint.getTextBounds()方法获取到所绘制的字符串的对应的最小距形。并且把它放到Rect对象内。 这样我们就可以使用Rect对象来设置文字显示的位置了。
。。。
//设置当前格子的数字
public void setNumber(int mNumber) {
。。。。
mBound = new Rect();
/**
* 获取指定字符串所对应的最小矩形,以(0,0)点所在位置为基线
* @param text 要测量最小矩形的字符串
* @param start 要测量起始字符在字符串中的索引
* @param end 所要测量的字符的长度
* @param bounds 接收测量结果
*/
paint.getTextBounds(mNumberVal,0,mNumberVal.length(),mBound);
//强制重绘
invalidate();
}
//绘制文字
private void drawText(Canvas canvas) {
paint.setColor(Color.BLACK);
float x = (getWidth() - mBound.width())/2;
float y = getHeight()/2 + mBound.height()/2;
canvas.drawText(mNumberVal,x,y,paint);
}
protected void onDraw(Canvas canvas) {
。。。
//如果数字的值不为0,那就绘制数字。
if(mNumber != 0){
drawText(canvas);
}
}
在onDraw()方法里面,我们先判断mNumber是否为0,如果是0,那就不绘制出来了。这样Game2048Item就准备完了。下面来看一下放格子的View类。
二、Game2048Layout
Game2048Layout是用来放所有格子的View,首先需要一个属性来设置每行格子的数量。然后我们根据这个变量,new出相应数量的Game2048Item,并且把它们放到一个二维数组里面,这样也方便后面的管理。然后我们还需要两个boolean对象,一个用来检查是否发生了合并,一个用来检查是否发生了移动。最后我们根据这两个变量来做出相应的操作。
//设置每行格子的数量
private int mColumn = 5;
//存放所有的格子
private Game2048Item[][] gameItems ;
//格子间横向和纵向的边距
private int mMargin = 10;
//面板之间的边距
//用来检查是否需要生成一个新的值。
//检查是否发生了合并
private boolean isMergeHappen = false;
//检查是否发生了移动
private boolean isMoveHappen = false;
//记录分数
private int mScore = 0;
//用了保存格子的宽度
private int childWidth;
//判断是否是第一次或者是重新开始游戏,如果是,就随机生成四个数字。
private boolean isFirst = true;
然后我们可以在初始化的时候把格子以及格子上的数字绘制出来。首先在onMeasure()方法里设置格子数量,格子的宽度。
/**
* 设置Layout的宽和高,
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取正方形的边长,取layout中宽和高的最小值
int length = Math.min(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.getSize(heightMeasureSpec));
//设置游戏内格子的宽度,
//游戏格子宽度 = (屏幕宽度 - 容器内边距离 * 2 - 格子之间的边距 * (格子数量 - 1) ) / 每行的格子数量
childWidth = (length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn;
//设置layout(容器)的大小
setMeasuredDimension(length,length);
}
然后在onLayout()方法里根据上面设置的变量来绘制格子以及格子上的数字。
//防止多次调用
private boolean once = false;
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(!once){
if(gameItems == null){
//初始化好数组
gameItems = new Game2048Item[mColumn][mColumn];
}
for(int i = 0; i < mColumn; i++){
for(int j = 0; j < mColumn; j++){
//初始化好格子
Game2048Item item = new Game2048Item(getContext());
gameItems[i][j] = item;
//定义该格子在布局中的那个位置
Spec x = GridLayout.spec(i);
Spec y = GridLayout.spec(j);
GridLayout.LayoutParams lp = new LayoutParams(x,y);
//设置view的宽和高
lp.height = childWidth;
lp.width = childWidth;
if( (j + 1) != mColumn){
//如果不是最后一列,就添加右边距离
lp.rightMargin = mMargin;
}
if( i > 0){
//如果不是第一行,就添加上边距
lp.topMargin = mMargin;
}
//设置填充满整个容器
lp.setGravity(Gravity.FILL);
addView(item,lp);
}
}
//随机生成数字
generateNum();
}
once = true;
}
generateNum()是一个随机位置随机生成数字的方法
/**
* 随机生成一个数字
*/
private void generateNum() {
//随机位置生成。
int x = new Random().nextInt(mColumn);
int y = new Random().nextInt(mColumn);
Game2048Item item = gameItems[x][y];
while (item.getNumber() != 0){
//如果随机生成的格子上有数字,那在随机生成另一个
x = new Random().nextInt(mColumn);
y = new Random().nextInt(mColumn);
item = gameItems[x][y];
}
//Math.random()随机生成一个大于0,小于1的数。
item.setNumber(Math.random() > 0.75 ? 4:2 );
}
绘制好格子之后,我们就需要对格子格子进行监听。根据用户的手势,来进行相应的操作。
虽然系统给我们提供了监听触摸事件的接口,即OnTouchListener,通过它,我们可以进行监听一些简单的操作,比如down、up等。但是如果我们要监听的手势比较复杂的时候,用这个接口就比较不方便了。这时候我们可以使用Android提供的GestureDetector类,通过该类,我们可以监听一些复杂的手势。
/**
* 枚举,定义用户的手势,值有
* 上、下、左、右
*/
private enum ACTION {
LEFT,RIGHT,UP,DOWM
}
/**
* 继承GestureDetector,自己来实现手势的监听。
*/
private class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {
//定义一个最小距离,如果用户移动的距离大于这个最少距离才会执行方法
final int FLING_MIN_DISTANCE = 50;
/**
* 用户手指在触摸屏上迅速移动,并松开的动作触法该方法
* @param e1 第一次按下时的MotionEvent.
* @param e2 最后一次移动时候的MotionEvent.
* @param velocityX X轴上的移动速度,像素/秒
* @param velocityY y轴上的移动速度,像素/秒
* @return
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//获取移动的x、y坐标
float x = e2.getX() - e1.getX();
float y = e2.getY() - e1.getY();
//获取x轴和y轴的移动速度的决定值。
float absX = Math.abs(velocityX);
float absY = Math.abs(velocityY);
//如果x轴移动距离大于正数的最小响应距离。且x轴的移动速度比y轴快,那就认为用户在向右滑动
if(x > FLING_MIN_DISTANCE && absX > absY){
action(ACTION.RIGHT);
}else if( x < -FLING_MIN_DISTANCE && absX > absY){
//x大于负数的最小响应距离,且x轴的移动速度比y轴快,那就认为用户在向左滑动
action(ACTION.LEFT);
}else if( y > FLING_MIN_DISTANCE && absX < absY){
//下滑动
action(ACTION.DOWM);
}else if(y < -FLING_MIN_DISTANCE && absX < absY){
//向上滑动
action(ACTION.UP);
}
return true;
}
}
/**
* 把触摸事件交由我们自己定义的类来监听
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
return true;
}
我先准备了一个枚举类,用来保存相应的手势,然后在继承GestureDetector的类的onFling()方法里面通过逻辑判断用户的手势。然后把手势传入action()方法内进行具体的移动合并操作。
/**
* 根据用户手势,进行相应的合并操作
*/
private void action(ACTION action) {
System.out.println("action:" + action);
//获取到View上有数字的格子。
for(int i = 0 ; i < mColumn; i++){
//用来保存每行不为0的格子的数组。
List<Game2048Item> rowTemp = new ArrayList<>();
for(int j = 0;j < mColumn; j++){
//根据手势提供数组下标。
int rowIndex = getRowIndexByAction(action,i,j);
int colindex = getColIndexByAction(action,i,j);
Game2048Item item = gameItems[rowIndex][colindex];
//判断该格子内是否有数字,有就存到临时数组内。
if(item.getNumber() != 0){
rowTemp.add(item);
}
}
//判断是否进行了移动,防止用户往同一方向多次滑动(这实际只是一次滑动事件),然后会自动生成随机数字
for(int j = 0 ; j < rowTemp.size(); j++){
int rowIndex = getRowIndexByAction(action,i,j);
int colIndex = getColIndexByAction(action,i,j);
//获取对应位置上数字
Game2048Item item = gameItems[rowIndex][colIndex];
//如果原位置上的数字和数组内的数字对应不上(数组能的数字已经去掉了0的项),那就说明已经发生了移动
if(item.getNumber() != rowTemp.get(j).getNumber()){
isMoveHappen = true;
}
}
//进行合并操作
mergeItem(rowTemp);
//将合并后的数组添进列表内,剩余位置用0补充
for(int j = 0; j < mColumn; j++){
if(rowTemp.size() > j){
int number = rowTemp.get(j).getNumber();
switch (action){
case LEFT:
gameItems[i][j].setNumber(number);
break;
case RIGHT:
//往右滑动,因为之前获取数据的时候,是从左往右获取的,所以赋值的时候应该是从数组后取
gameItems[i][mColumn - j - 1].setNumber(number);
break;
case UP:
gameItems[j][i].setNumber(number);
break;
case DOWM:
gameItems[mColumn - j - 1][i].setNumber(number);
break;
}
}else{
//补零
switch (action){
case LEFT:
gameItems[i][j].setNumber(0);
break;
case RIGHT:
gameItems[i][mColumn - j - 1].setNumber(0);
break;
case UP:
gameItems[j][i].setNumber(0);
break;
case DOWM:
gameItems[mColumn - j - 1][i].setNumber(0);
break;
}
}
}
}
//随机生成一个数字。
generateNum();
}
这段代码虽然很长,但理解起来其实也容易。主要是下面几个步骤:
(1)获取不为0的格子数值。放到一个临时数组内。
(2)用临时数组和老数组相比较,判断是否已经移动。
(3)进行合并操作。
(4)把新值赋值到格子内,不足位补0;
(5)如果发生移动,且没有进行合并,那就随机位置生成一个随机数字。
(6)如果发生移动,但已合并,那就不生成随机数字。
比如:有一组数组:
2 0 0 4
2 2 0 0
0 2 0 2
0 0 2 0
如果用户发生了左滑,那就会变成:
2 4 0 0
4 0 0 0
4 0 0 0
2 0 0 0
具体实现逻辑是:
使用for循环一行/一列,取出数组的值。通过getRowIndexByAction()和getColIndexByAction()两个方法,根据用户手势,遍历取出格子内不为0的数组的x和y轴下标。然后根据这两个下标,取出该数组的值,并且放到临时数组内。比如上面的数组,如果是左滑,会先取出
[2,4]
然后比较该数组和原来数组是否一致,不一致那就把isMoveHappen 设置为true。然后比较比较两个相邻的值是否相同,如果相同,那就进行合并操作,并且把isMergeHappen设置为true。
进行合并操作之后,把该临时数组的值放到原行/列,不足位就补0。
最后循环完一行/列 之后,再次进入下一个循环。重复上面的操作。
但循环完之后,在根据isMoveHappen和isMergeHappen来判断是否需要生成随机数字。
/**
* 根基手势来确定获取的是哪一个方向上的数组行下标。
* @param action : 手势
* @return : 返回行下标
* 返回数据经过排序,保证返回的数据和取的数据顺序一致。
*/
private int getRowIndexByAction(ACTION action,int i,int j){
int rowIndex = -1;
switch (action){
case UP:
//向上滑动,从上往下填充数组,包装后面取数据的时候位置一一对应。
rowIndex = j;
break;
case DOWM:
//向下滑动,从下往上填充数组,保证后面取数据的时候位置一一对应。
rowIndex = mColumn - j - 1;
break;
//左滑动、右滑动。变动的是列,所以行不变动。
case LEFT:
case RIGHT:
rowIndex = i;
break;
}
return rowIndex;
}
/**
* 根据手势来确定获取的是哪一个方向上的数组列下标。
* @param action : 手势
* @return :返回列的下标
*/
private int getColIndexByAction(ACTION action,int i,int j){
int ColIndex = -1;
switch (action){
case UP:
case DOWM:
//上滑动、下滑动,列固定
//向下滑动,从上到下,一列一列获取数组。
ColIndex = i;
break;
case RIGHT:
//向右滑动,从右往左取数据
ColIndex = mColumn - j - 1;
break;
case LEFT:
//向左滑动,从左往右取数据
ColIndex = j;
break;
}
return ColIndex;
}
代码到这里基本差不多了,我们最后还需要一个方法来判断游戏是否已经结束。那如何判断呢?很简单,只要检查格子是否已经填满就可以了。
/**
* 检查是否已填满数字
* @return true 是,false 否
*/
private boolean ifFull() {
for(int i = 0; i < mColumn; i++){
for(int j = 0 ; j < mColumn; j++){
Game2048Item item = gameItems[i][j];
if(item.getNumber() == 0){
return false;
}
}
}
return true;
}
Game2048Layout写好了,那如何使用呢?很简单,只需要把它当成普通的布局文件就可以了
<example.com.a2048game.view.Game2048Layout
android:id="@+id/id_game2048"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="#ffffff"
android:padding="10dp"
</example.com.a2048game.view.Game2048Layout>