【Android实战】2048游戏

看了别人写的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>

完整的代码在这里

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值